2021-05-05 20:32:31 +00:00
import { expect } from 'chai' ;
import * as sinon from 'sinon' ;
2022-11-09 00:06:10 +00:00
import * as Docker from 'dockerode' ;
2022-08-17 23:35:08 +00:00
import * as applicationManager from '~/src/compose/application-manager' ;
import * as imageManager from '~/src/compose/images' ;
import * as serviceManager from '~/src/compose/service-manager' ;
import Network from '~/src/compose/network' ;
import * as networkManager from '~/src/compose/network-manager' ;
import Volume from '~/src/compose/volume' ;
2022-08-25 22:48:10 +00:00
import * as config from '~/src/config' ;
2022-11-16 03:26:41 +00:00
import { createDockerImage } from '~/test-lib/docker-helper' ;
2022-12-08 19:38:11 +00:00
import {
createService ,
createImage ,
createApps ,
createCurrentState ,
DEFAULT_NETWORK ,
} from '~/test-lib/state-helper' ;
2021-05-05 20:32:31 +00:00
2022-08-25 22:48:10 +00:00
// TODO: application manager inferNextSteps still queries some stuff from
// the engine instead of receiving that information as parameter. Refactoring
// the method to be more of a pure function would allow us to move a lot of these tests
// to unit tests, leaving the need of integration tests just for more complex stuff that
// the application-manager also does and that is not currently tested.
// TODO: also, there is some redundancy between what is tested here and what is tested in
// the app spec, remove that redundancy to simplify the tests
2021-05-05 20:32:31 +00:00
describe ( 'compose/application-manager' , ( ) = > {
before ( async ( ) = > {
2022-08-25 22:48:10 +00:00
// Service.fromComposeObject gets api keys from the database
// which also depend on the local mode. This ensures the database
// is initialized. This can be removed when ApplicationManager and Service
// a refactored to work as pure functions
await config . initialized ( ) ;
2021-05-05 20:32:31 +00:00
} ) ;
2022-11-09 00:06:10 +00:00
beforeEach ( async ( ) = > {
// Set up network by default
await networkManager . ensureSupervisorNetwork ( ) ;
2021-05-05 20:32:31 +00:00
} ) ;
2022-11-09 00:06:10 +00:00
afterEach ( async ( ) = > {
// Delete any created networks
const docker = new Docker ( ) ;
const allNetworks = await docker . listNetworks ( ) ;
await Promise . all (
allNetworks
// exclude docker default networks from the cleanup
. filter ( ( { Name } ) = > ! [ 'bridge' , 'host' , 'none' ] . includes ( Name ) )
. map ( ( { Name } ) = > docker . getNetwork ( Name ) . remove ( ) ) ,
) ;
2021-05-05 20:32:31 +00:00
} ) ;
2022-08-25 22:48:10 +00:00
// TODO: we don't test application manager initialization as it sets up a bunch of timers
// and listeners that may affect other tests. This is a bad pattern and it needs to be purged
// from the codebase
it . skip ( 'should init' , async ( ) = > {
2022-09-06 18:03:23 +00:00
await applicationManager . initialized ( ) ;
2021-05-05 20:32:31 +00:00
} ) ;
// TODO: missing tests for getCurrentApps
2022-04-12 06:05:56 +00:00
it ( 'should not infer a start step when all that changes is a running state' , async ( ) = > {
2021-05-05 20:32:31 +00:00
const targetApps = createApps (
{
2021-07-23 16:48:27 +00:00
services : [ await createService ( { running : true , appId : 1 } ) ] ,
2021-05-05 20:32:31 +00:00
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [ await createService ( { running : false , appId : 1 } ) ] ,
networks : [ DEFAULT_NETWORK ] ,
} ) ;
2021-05-05 20:32:31 +00:00
2022-04-12 06:05:56 +00:00
const steps = await applicationManager . inferNextSteps (
2021-05-05 20:32:31 +00:00
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
2022-04-12 06:05:56 +00:00
// There should be no steps since the engine manages restart policy for stopped containers
expect ( steps . length ) . to . equal ( 0 ) ;
2021-05-05 20:32:31 +00:00
} ) ;
it ( 'infers a kill step when a service has to be removed' , async ( ) = > {
const targetApps = createApps (
{
services : [ ] ,
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [ await createService ( ) ] ,
networks : [ DEFAULT_NETWORK ] ,
} ) ;
2021-05-05 20:32:31 +00:00
const [ killStep ] = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
expect ( killStep ) . to . have . property ( 'action' ) . that . equals ( 'kill' ) ;
expect ( killStep )
. to . have . property ( 'current' )
. that . deep . includes ( { serviceName : 'main' } ) ;
} ) ;
it ( 'infers a fetch step when a service has to be updated' , async ( ) = > {
const targetApps = createApps (
{
2021-07-23 16:48:27 +00:00
services : [ await createService ( { image : 'image-new' , appId : 1 } ) ] ,
2021-05-05 20:32:31 +00:00
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [ await createService ( { appId : 1 } ) ] ,
networks : [ DEFAULT_NETWORK ] ,
images : [ ] ,
} ) ;
2021-05-05 20:32:31 +00:00
const [ fetchStep ] = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
expect ( fetchStep ) . to . have . property ( 'action' ) . that . equals ( 'fetch' ) ;
expect ( fetchStep )
. to . have . property ( 'image' )
. that . deep . includes ( { name : 'image-new' } ) ;
} ) ;
it ( 'does not infer a fetch step when the download is already in progress' , async ( ) = > {
const targetApps = createApps (
{
2021-07-23 16:48:27 +00:00
services : [ await createService ( { image : 'image-new' , appId : 1 } ) ] ,
2021-05-05 20:32:31 +00:00
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [ await createService ( { appId : 1 } ) ] ,
networks : [ DEFAULT_NETWORK ] ,
downloading : [ 'image-new' ] ,
} ) ;
2021-05-05 20:32:31 +00:00
const [ noopStep , . . . nextSteps ] = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
expect ( noopStep ) . to . have . property ( 'action' ) . that . equals ( 'noop' ) ;
expect ( nextSteps ) . to . have . lengthOf ( 0 ) ;
} ) ;
it ( 'infers a kill step when a service has to be updated but the strategy is kill-then-download' , async ( ) = > {
const labels = {
'io.balena.update.strategy' : 'kill-then-download' ,
} ;
const targetApps = createApps (
{
services : [
2021-07-23 16:48:27 +00:00
await createService ( {
image : 'image-new' ,
labels ,
appId : 1 ,
commit : 'new-release' ,
} ) ,
2021-05-05 20:32:31 +00:00
] ,
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [
await createService (
{
image : 'image-old' ,
labels ,
appId : 1 ,
commit : 'old-release' ,
} ,
{ options : { imageInfo : { Id : 'sha256:image-old-id' } } } ,
) ,
] ,
networks : [ DEFAULT_NETWORK ] ,
} ) ;
2021-05-05 20:32:31 +00:00
const [ killStep ] = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
expect ( killStep ) . to . have . property ( 'action' ) . that . equals ( 'kill' ) ;
expect ( killStep )
. to . have . property ( 'current' )
. that . deep . includes ( { serviceName : 'main' } ) ;
} ) ;
2021-11-11 19:28:02 +00:00
it ( 'infers a kill step when a service has to be updated but the strategy is delete-then-download' , async ( ) = > {
const labels = {
'io.balena.update.strategy' : 'delete-then-download' ,
} ;
const targetApps = createApps (
{
services : [
await createService ( {
image : 'image-new' ,
labels ,
appId : 1 ,
commit : 'new-release' ,
} ) ,
] ,
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [
await createService (
{
image : 'image-old' ,
labels ,
appId : 1 ,
commit : 'old-release' ,
} ,
{ options : { imageInfo : { Id : 'sha256:image-old-id' } } } ,
) ,
] ,
networks : [ DEFAULT_NETWORK ] ,
} ) ;
2021-11-11 19:28:02 +00:00
const [ killStep ] = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
expect ( killStep ) . to . have . property ( 'action' ) . that . equals ( 'kill' ) ;
expect ( killStep )
. to . have . property ( 'current' )
. that . deep . includes ( { serviceName : 'main' } ) ;
} ) ;
it ( 'infers a remove step when the current service has stopped and the strategy is delete-then-download' , async ( ) = > {
const labels = {
'io.balena.update.strategy' : 'delete-then-download' ,
} ;
const targetApps = createApps (
{
services : [
await createService ( {
image : 'image-new' ,
labels ,
appId : 1 ,
serviceName : 'main' ,
commit : 'new-release' ,
} ) ,
] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [ ] ,
images : [
createImage ( {
appId : 1 ,
name : 'image-old' ,
serviceName : 'main' ,
dockerImageId : 'image-old-id' ,
} ) ,
] ,
networks : [ DEFAULT_NETWORK ] ,
} ) ;
2021-11-11 19:28:02 +00:00
const [ removeImage ] = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
// First we should see a kill
expect ( removeImage ) . to . have . property ( 'action' ) . that . equals ( 'removeImage' ) ;
expect ( removeImage )
. to . have . property ( 'image' )
. that . deep . includes ( { name : 'image-old' } ) ;
} ) ;
2021-05-05 20:32:31 +00:00
it ( 'does not infer to kill a service with default strategy if a dependency is not downloaded' , async ( ) = > {
const targetApps = createApps (
{
services : [
2021-07-23 16:48:27 +00:00
await createService ( {
image : 'main-image' ,
appId : 1 ,
commit : 'new-release' ,
serviceName : 'main' ,
2021-08-03 23:12:47 +00:00
composition : {
depends_on : [ 'dep' ] ,
} ,
2021-07-23 16:48:27 +00:00
} ) ,
await createService ( {
image : 'dep-image' ,
appId : 1 ,
commit : 'new-release' ,
serviceName : 'dep' ,
} ) ,
2021-05-05 20:32:31 +00:00
] ,
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [
await createService ( {
appId : 1 ,
commit : 'old-release' ,
serviceName : 'main' ,
composition : {
depends_on : [ 'dep' ] ,
} ,
} ) ,
await createService ( {
appId : 1 ,
commit : 'old-release' ,
serviceName : 'dep' ,
} ) ,
] ,
networks : [ DEFAULT_NETWORK ] ,
downloading : [ 'dep-image' ] , // dep-image is still being downloaded
images : [
// main-image was already downloaded
createImage ( {
appId : 1 ,
name : 'main-image' ,
serviceName : 'main' ,
} ) ,
] ,
} ) ;
2021-05-05 20:32:31 +00:00
const steps = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
// Only noop steps should be seen at this point
expect ( steps . filter ( ( s ) = > s . action !== 'noop' ) ) . to . have . lengthOf ( 0 ) ;
} ) ;
it ( 'infers to kill several services as long as there is no unmet dependency' , async ( ) = > {
const targetApps = createApps (
{
services : [
2021-07-23 16:48:27 +00:00
await createService ( {
image : 'main-image' ,
appId : 1 ,
2021-08-25 23:25:47 +00:00
appUuid : 'appuuid' ,
2021-07-23 16:48:27 +00:00
commit : 'new-release' ,
serviceName : 'main' ,
2021-08-03 23:12:47 +00:00
composition : {
depends_on : [ 'dep' ] ,
} ,
2021-07-23 16:48:27 +00:00
} ) ,
await createService ( {
image : 'dep-image' ,
appId : 1 ,
2021-08-25 23:25:47 +00:00
appUuid : 'appuuid' ,
2021-07-23 16:48:27 +00:00
commit : 'new-release' ,
serviceName : 'dep' ,
} ) ,
2021-05-05 20:32:31 +00:00
] ,
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [
await createService ( {
appId : 1 ,
appUuid : 'appuuid' ,
commit : 'old-release' ,
serviceName : 'main' ,
composition : {
depends_on : [ 'dep' ] ,
} ,
} ) ,
await createService ( {
appId : 1 ,
appUuid : 'appuuid' ,
commit : 'old-release' ,
serviceName : 'dep' ,
} ) ,
] ,
networks : [ DEFAULT_NETWORK ] ,
images : [
// Both images have been downloaded
createImage ( {
appId : 1 ,
appUuid : 'appuuid' ,
name : 'main-image' ,
serviceName : 'main' ,
commit : 'new-release' ,
} ) ,
createImage ( {
appId : 1 ,
appUuid : 'appuuid' ,
name : 'dep-image' ,
serviceName : 'dep' ,
commit : 'new-release' ,
} ) ,
] ,
} ) ;
2021-05-05 20:32:31 +00:00
const steps = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
// We should see kill steps for both currently running services
expect (
steps . filter (
( s : any ) = > s . action === 'kill' && s . current . serviceName === 'dep' ,
) ,
) . to . have . lengthOf ( 1 ) ;
expect (
steps . filter (
( s : any ) = > s . action === 'kill' && s . current . serviceName === 'main' ,
) ,
) . to . have . lengthOf ( 1 ) ;
} ) ;
it ( 'infers to start the dependency first' , async ( ) = > {
const targetApps = createApps (
{
services : [
2021-07-23 16:48:27 +00:00
await createService ( {
image : 'main-image' ,
serviceName : 'main' ,
commit : 'new-release' ,
2021-08-03 23:12:47 +00:00
composition : {
depends_on : [ 'dep' ] ,
} ,
2021-07-23 16:48:27 +00:00
} ) ,
await createService ( {
image : 'dep-image' ,
serviceName : 'dep' ,
commit : 'new-release' ,
} ) ,
2021-05-05 20:32:31 +00:00
] ,
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [ ] ,
networks : [ DEFAULT_NETWORK ] ,
images : [
// Both images have been downloaded
createImage ( {
name : 'main-image' ,
serviceName : 'main' ,
commit : 'new-release' ,
} ) ,
createImage ( {
name : 'dep-image' ,
serviceName : 'dep' ,
commit : 'new-release' ,
} ) ,
] ,
} ) ;
2021-05-05 20:32:31 +00:00
const [ startStep , . . . nextSteps ] = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
2021-07-07 22:01:48 +00:00
// A start step should happen for the depended service first
2021-05-05 20:32:31 +00:00
expect ( startStep ) . to . have . property ( 'action' ) . that . equals ( 'start' ) ;
expect ( startStep )
. to . have . property ( 'target' )
. that . deep . includes ( { serviceName : 'dep' } ) ;
// No more steps until the first container has been started
expect ( nextSteps ) . to . have . lengthOf ( 0 ) ;
} ) ;
it ( 'infers to start a service once its dependency has been met' , async ( ) = > {
const targetApps = createApps (
{
services : [
2021-07-23 16:48:27 +00:00
await createService ( {
image : 'main-image' ,
serviceName : 'main' ,
commit : 'new-release' ,
2021-08-03 23:12:47 +00:00
composition : {
depends_on : [ 'dep' ] ,
} ,
2021-07-23 16:48:27 +00:00
} ) ,
await createService ( {
image : 'dep-image' ,
serviceName : 'dep' ,
commit : 'new-release' ,
} ) ,
2021-05-05 20:32:31 +00:00
] ,
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [
await createService ( {
image : 'dep-image' ,
serviceName : 'dep' ,
commit : 'new-release' ,
} ) ,
] ,
networks : [ DEFAULT_NETWORK ] ,
images : [
// Both images have been downloaded
createImage ( {
name : 'main-image' ,
serviceName : 'main' ,
commit : 'new-release' ,
} ) ,
createImage ( {
name : 'dep-image' ,
serviceName : 'dep' ,
commit : 'new-release' ,
} ) ,
] ,
} ) ;
2021-05-05 20:32:31 +00:00
const [ startStep , . . . nextSteps ] = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
// A start step shoud happen for the depended service first
expect ( startStep ) . to . have . property ( 'action' ) . that . equals ( 'start' ) ;
expect ( startStep )
. to . have . property ( 'target' )
. that . deep . includes ( { serviceName : 'main' } ) ;
expect ( nextSteps ) . to . have . lengthOf ( 0 ) ;
} ) ;
it ( 'infers to remove spurious containers' , async ( ) = > {
const targetApps = createApps (
{
services : [ await createService ( { image : 'main-image' } ) ] ,
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [
await createService ( { appId : 5 , serviceName : 'old-service' } ) ,
] ,
networks : [ DEFAULT_NETWORK ] ,
images : [
// Image has been downloaded
createImage ( {
name : 'main-image' ,
serviceName : 'main' ,
} ) ,
] ,
} ) ;
2021-05-05 20:32:31 +00:00
const steps = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
// Start the new service
expect (
steps . filter (
( s : any ) = > s . action === 'start' && s . target . serviceName === 'main' ,
) ,
) . to . have . lengthOf ( 1 ) ;
// Remove the leftover service
expect (
steps . filter (
( s : any ) = >
s . action === 'kill' && s . current . serviceName === 'old-service' ,
) ,
) . to . have . lengthOf ( 1 ) ;
} ) ;
it ( 'should not remove an app volumes when they are no longer referenced' , async ( ) = > {
const targetApps = createApps ( { networks : [ DEFAULT_NETWORK ] } , true ) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [ ] ,
networks : [ DEFAULT_NETWORK ] ,
volumes : [ Volume . fromComposeObject ( 'test-volume' , 1 , 'deadbeef' ) ] ,
} ) ;
2021-05-05 20:32:31 +00:00
const steps = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
expect ( steps . filter ( ( s ) = > s . action === 'removeVolume' ) ) . to . be . empty ;
} ) ;
it ( 'should remove volumes from previous applications' , async ( ) = > {
const targetApps = createApps ( { networks : [ DEFAULT_NETWORK ] } , true ) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [ ] ,
networks : [ ] ,
// Volume with different id
volumes : [ Volume . fromComposeObject ( 'test-volume' , 2 , 'deadbeef' ) ] ,
} ) ;
2021-05-05 20:32:31 +00:00
const steps = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
expect ( steps . filter ( ( s ) = > s . action === 'removeVolume' ) ) . to . not . be . empty ;
} ) ;
2023-04-06 17:50:10 +00:00
it ( 'should remove volumes from previous applications except if keepVolumes is set' , async ( ) = > {
const targetApps = createApps ( { networks : [ DEFAULT_NETWORK ] } , true ) ;
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [ ] ,
networks : [ ] ,
// Volume with different id
volumes : [ Volume . fromComposeObject ( 'test-volume' , 2 , 'deadbeef' ) ] ,
} ) ;
const steps = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
keepVolumes : true ,
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
expect ( steps . filter ( ( s ) = > s . action === 'removeVolume' ) ) . to . be . empty ;
} ) ;
2021-05-05 20:32:31 +00:00
it ( 'should infer that we need to create the supervisor network if it does not exist' , async ( ) = > {
2022-11-09 00:06:10 +00:00
const docker = new Docker ( ) ;
await docker . getNetwork ( 'supervisor0' ) . remove ( ) ;
2021-05-05 20:32:31 +00:00
const targetApps = createApps (
2021-07-23 16:48:27 +00:00
{ services : [ await createService ( ) ] , networks : [ DEFAULT_NETWORK ] } ,
2021-05-05 20:32:31 +00:00
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [ ] ,
networks : [ DEFAULT_NETWORK ] ,
} ) ;
2021-05-05 20:32:31 +00:00
2022-09-19 15:33:52 +00:00
const [ ensureNetworkStep , . . . nextSteps ] =
await applicationManager . inferNextSteps ( currentApps , targetApps , {
downloading ,
availableImages ,
containerIdsByAppId ,
} ) ;
2021-05-05 20:32:31 +00:00
expect ( ensureNetworkStep ) . to . deep . include ( {
action : 'ensureSupervisorNetwork' ,
} ) ;
expect ( nextSteps ) . to . have . lengthOf ( 0 ) ;
} ) ;
it ( 'should kill a service which depends on the supervisor network, if we need to create the network' , async ( ) = > {
2022-11-09 00:06:10 +00:00
const docker = new Docker ( ) ;
await docker . getNetwork ( 'supervisor0' ) . remove ( ) ;
2021-05-05 20:32:31 +00:00
const labels = { 'io.balena.features.supervisor-api' : 'true' } ;
const targetApps = createApps (
{
services : [
await createService ( { labels } , { options : { listenPort : '48484' } } ) ,
] ,
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [
await createService ( { labels } , { options : { listenPort : '48484' } } ) ,
] ,
networks : [ DEFAULT_NETWORK ] ,
} ) ;
2021-05-05 20:32:31 +00:00
const [ killStep ] = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
// A start step shoud happen for the depended service first
expect ( killStep ) . to . have . property ( 'action' ) . that . equals ( 'kill' ) ;
expect ( killStep )
. to . have . property ( 'current' )
. that . deep . includes ( { serviceName : 'main' } ) ;
} ) ;
it ( 'should infer a cleanup step when a cleanup is required' , async ( ) = > {
2022-11-16 03:26:41 +00:00
// Create a dangling image; this is done by building an image again with
// some slightly different metadata, leaving the old image with no metadata.
const docker = new Docker ( ) ;
const dockerImageIdOne = await createDockerImage (
'some-image:some-tag' ,
[ 'io.balena.testing=1' ] ,
docker ,
) ;
const dockerImageIdTwo = await createDockerImage (
'some-image:some-tag' ,
[ 'io.balena.testing=2' ] ,
docker ,
) ;
// Remove the tagged image, leaving only the dangling image
await docker . getImage ( dockerImageIdTwo ) . remove ( ) ;
2021-05-05 20:32:31 +00:00
const targetApps = createApps (
{
services : [ await createService ( ) ] ,
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [ await createService ( ) ] ,
networks : [ DEFAULT_NETWORK ] ,
} ) ;
2021-05-05 20:32:31 +00:00
const [ cleanupStep , . . . nextSteps ] = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
// Cleanup needs to happen first
expect ( cleanupStep ) . to . deep . include ( {
action : 'cleanup' ,
} ) ;
expect ( nextSteps ) . to . have . lengthOf ( 0 ) ;
2022-11-16 03:26:41 +00:00
await docker . getImage ( dockerImageIdOne ) . remove ( ) ;
2021-05-05 20:32:31 +00:00
} ) ;
2021-07-07 22:01:48 +00:00
it ( 'should infer that an image should be removed if it is no longer referenced in current or target state (only target)' , async ( ) = > {
2021-05-05 20:32:31 +00:00
const targetApps = createApps (
{
services : [
await createService (
{ image : 'main-image' } ,
2021-07-07 22:01:48 +00:00
// Target has a matching image already
2021-05-05 20:32:31 +00:00
{ options : { imageInfo : { Id : 'sha256:bbbb' } } } ,
) ,
] ,
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [ ] ,
networks : [ DEFAULT_NETWORK ] ,
images : [
// An image for a service that no longer exists
createImage ( {
name : 'old-image' ,
appId : 5 ,
serviceName : 'old-service' ,
dockerImageId : 'sha256:aaaa' ,
} ) ,
createImage ( {
name : 'main-image' ,
appId : 1 ,
serviceName : 'main' ,
dockerImageId : 'sha256:bbbb' ,
} ) ,
] ,
} ) ;
2021-07-07 22:01:48 +00:00
const [ removeImageStep ] = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
// A start step shoud happen for the depended service first
expect ( removeImageStep )
. to . have . property ( 'action' )
. that . equals ( 'removeImage' ) ;
expect ( removeImageStep )
. to . have . property ( 'image' )
. that . deep . includes ( { name : 'old-image' } ) ;
} ) ;
2023-04-06 17:50:10 +00:00
it ( 'should infer that an image should be removed if it is no longer referenced in current or target state (only target) unless keepImages is true' , async ( ) = > {
const targetApps = createApps (
{
services : [
await createService (
{ image : 'main-image' } ,
// Target has a matching image already
{ options : { imageInfo : { Id : 'sha256:bbbb' } } } ,
) ,
] ,
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [ ] ,
networks : [ DEFAULT_NETWORK ] ,
images : [
// An image for a service that no longer exists
createImage ( {
name : 'old-image' ,
appId : 5 ,
serviceName : 'old-service' ,
dockerImageId : 'sha256:aaaa' ,
} ) ,
createImage ( {
name : 'main-image' ,
appId : 1 ,
serviceName : 'main' ,
dockerImageId : 'sha256:bbbb' ,
} ) ,
] ,
} ) ;
const steps = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
keepImages : true ,
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
expect ( steps . filter ( ( s ) = > s . action === 'removeImage' ) ) . to . be . empty ;
} ) ;
2021-07-07 22:01:48 +00:00
it ( 'should infer that an image should be removed if it is no longer referenced in current or target state (only current)' , async ( ) = > {
const targetApps = createApps (
{
services : [ ] ,
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [
await createService (
{ image : 'main-image' } ,
// Target has a matching image already
{ options : { imageInfo : { Id : 'sha256:bbbb' } } } ,
) ,
] ,
networks : [ DEFAULT_NETWORK ] ,
images : [
// An image for a service that no longer exists
createImage ( {
name : 'old-image' ,
appId : 5 ,
serviceName : 'old-service' ,
dockerImageId : 'sha256:aaaa' ,
} ) ,
createImage ( {
name : 'main-image' ,
appId : 1 ,
serviceName : 'main' ,
dockerImageId : 'sha256:bbbb' ,
} ) ,
] ,
} ) ;
2021-05-05 20:32:31 +00:00
const [ removeImageStep ] = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
// A start step shoud happen for the depended service first
expect ( removeImageStep )
. to . have . property ( 'action' )
. that . equals ( 'removeImage' ) ;
expect ( removeImageStep )
. to . have . property ( 'image' )
. that . deep . includes ( { name : 'old-image' } ) ;
} ) ;
2023-04-06 17:50:10 +00:00
it ( 'should infer that an image should be removed if it is no longer referenced in current or target state (only current) unless keepImages is true' , async ( ) = > {
const targetApps = createApps (
{
services : [ ] ,
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [
await createService (
{ image : 'main-image' } ,
// Target has a matching image already
{ options : { imageInfo : { Id : 'sha256:bbbb' } } } ,
) ,
] ,
networks : [ DEFAULT_NETWORK ] ,
images : [
// An image for a service that no longer exists
createImage ( {
name : 'old-image' ,
appId : 5 ,
serviceName : 'old-service' ,
dockerImageId : 'sha256:aaaa' ,
} ) ,
createImage ( {
name : 'main-image' ,
appId : 1 ,
serviceName : 'main' ,
dockerImageId : 'sha256:bbbb' ,
} ) ,
] ,
} ) ;
const steps = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
keepImages : true ,
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
expect ( steps . filter ( ( s ) = > s . action === 'removeImage' ) ) . to . be . empty ;
} ) ;
2021-05-05 20:32:31 +00:00
2021-07-07 22:01:48 +00:00
it ( 'should infer that an image should be saved if it is not in the available image list but it can be found on disk' , async ( ) = > {
const targetApps = createApps (
{
services : [
await createService (
{ image : 'main-image' } ,
// Target has image info
{ options : { imageInfo : { Id : 'sha256:bbbb' } } } ,
) ,
] ,
networks : [ DEFAULT_NETWORK ] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [ ] ,
networks : [ DEFAULT_NETWORK ] ,
images : [ ] , // no available images exist
} ) ;
2021-07-07 22:01:48 +00:00
const [ saveImageStep ] = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
// A start step shoud happen for the depended service first
expect ( saveImageStep ) . to . have . property ( 'action' ) . that . equals ( 'saveImage' ) ;
expect ( saveImageStep )
. to . have . property ( 'image' )
. that . deep . includes ( { name : 'main-image' } ) ;
} ) ;
2021-05-05 20:32:31 +00:00
it ( 'should correctly generate steps for multiple apps' , async ( ) = > {
const targetApps = createApps (
{
services : [
2021-07-23 16:48:27 +00:00
await createService ( {
running : true ,
image : 'main-image-1' ,
appId : 1 ,
2021-08-03 23:12:47 +00:00
appUuid : 'app-one' ,
2021-07-23 16:48:27 +00:00
commit : 'commit-for-app-1' ,
} ) ,
await createService ( {
running : true ,
image : 'main-image-2' ,
appId : 2 ,
2021-08-03 23:12:47 +00:00
appUuid : 'app-two' ,
2021-07-23 16:48:27 +00:00
commit : 'commit-for-app-2' ,
} ) ,
2021-05-05 20:32:31 +00:00
] ,
networks : [
// Default networks for two apps
2021-08-25 23:25:47 +00:00
Network . fromComposeObject ( 'default' , 1 , 'app-one' , { } ) ,
Network . fromComposeObject ( 'default' , 2 , 'app-two' , { } ) ,
2021-05-05 20:32:31 +00:00
] ,
} ,
true ,
) ;
2022-09-19 15:33:52 +00:00
const { currentApps , availableImages , downloading , containerIdsByAppId } =
createCurrentState ( {
services : [ ] ,
networks : [
// Default networks for two apps
Network . fromComposeObject ( 'default' , 1 , 'app-one' , { } ) ,
Network . fromComposeObject ( 'default' , 2 , 'app-two' , { } ) ,
] ,
images : [
createImage ( {
name : 'main-image-1' ,
appId : 1 ,
appUuid : 'app-one' ,
serviceName : 'main' ,
commit : 'commit-for-app-1' ,
} ) ,
createImage ( {
name : 'main-image-2' ,
appId : 2 ,
appUuid : 'app-two' ,
serviceName : 'main' ,
commit : 'commit-for-app-2' ,
} ) ,
] ,
} ) ;
2021-05-05 20:32:31 +00:00
const steps = await applicationManager . inferNextSteps (
currentApps ,
targetApps ,
{
downloading ,
availableImages ,
containerIdsByAppId ,
} ,
) ;
// Expect a start step for both apps
expect (
steps . filter (
( s : any ) = >
s . action === 'start' &&
s . target . appId === 1 &&
s . target . serviceName === 'main' ,
) ,
) . to . have . lengthOf ( 1 ) ;
expect (
steps . filter (
( s : any ) = >
s . action === 'start' &&
s . target . appId === 2 &&
s . target . serviceName === 'main' ,
) ,
) . to . have . lengthOf ( 1 ) ;
} ) ;
2021-09-01 22:13:27 +00:00
2022-11-16 03:26:41 +00:00
describe ( "getting application's current state" , ( ) = > {
2021-09-01 22:13:27 +00:00
let getImagesState : sinon.SinonStub ;
let getServicesState : sinon.SinonStub ;
before ( ( ) = > {
getImagesState = sinon . stub ( imageManager , 'getState' ) ;
getServicesState = sinon . stub ( serviceManager , 'getState' ) ;
} ) ;
afterEach ( ( ) = > {
getImagesState . reset ( ) ;
getServicesState . reset ( ) ;
} ) ;
after ( ( ) = > {
getImagesState . restore ( ) ;
getServicesState . restore ( ) ;
} ) ;
it ( 'reports the state of images if no service is available' , async ( ) = > {
getImagesState . resolves ( [
{
name : 'ubuntu:latest' ,
commit : 'latestrelease' ,
appUuid : 'myapp' ,
serviceName : 'ubuntu' ,
status : 'Downloaded' ,
} ,
{
name : 'alpine:latest' ,
commit : 'latestrelease' ,
appUuid : 'myapp' ,
serviceName : 'alpine' ,
status : 'Downloading' ,
downloadProgress : 50 ,
} ,
{
name : 'fedora:latest' ,
commit : 'newrelease' ,
appUuid : 'fedora' ,
serviceName : 'fedora' ,
status : 'Downloading' ,
downloadProgress : 75 ,
} ,
{
name : 'fedora:older' ,
commit : 'oldrelease' ,
appUuid : 'fedora' ,
serviceName : 'fedora' ,
status : 'Downloaded' ,
} ,
] ) ;
getServicesState . resolves ( [ ] ) ;
expect ( await applicationManager . getState ( ) ) . to . deep . equal ( {
myapp : {
releases : {
latestrelease : {
services : {
ubuntu : {
image : 'ubuntu:latest' ,
status : 'Downloaded' ,
} ,
alpine : {
image : 'alpine:latest' ,
status : 'Downloading' ,
download_progress : 50 ,
} ,
} ,
} ,
} ,
} ,
fedora : {
releases : {
oldrelease : {
services : {
fedora : {
image : 'fedora:older' ,
status : 'Downloaded' ,
} ,
} ,
} ,
newrelease : {
services : {
fedora : {
image : 'fedora:latest' ,
status : 'Downloading' ,
download_progress : 75 ,
} ,
} ,
} ,
} ,
} ,
} ) ;
} ) ;
it ( 'augments the service data with image data' , async ( ) = > {
getImagesState . resolves ( [
{
name : 'ubuntu:latest' ,
commit : 'latestrelease' ,
appUuid : 'myapp' ,
serviceName : 'ubuntu' ,
status : 'Downloaded' ,
} ,
2022-04-08 01:21:44 +00:00
{
name : 'node:latest' ,
commit : 'latestrelease' ,
appUuid : 'myapp' ,
serviceName : 'node' ,
status : 'Downloading' ,
downloadProgress : 0 ,
} ,
2021-09-01 22:13:27 +00:00
{
name : 'alpine:latest' ,
commit : 'latestrelease' ,
appUuid : 'myapp' ,
serviceName : 'alpine' ,
status : 'Downloading' ,
downloadProgress : 50 ,
} ,
{
name : 'fedora:older' ,
commit : 'oldrelease' ,
appUuid : 'fedora' ,
serviceName : 'fedora' ,
status : 'Downloaded' ,
} ,
] ) ;
getServicesState . resolves ( [
{
commit : 'latestrelease' ,
appUuid : 'myapp' ,
serviceName : 'ubuntu' ,
status : 'Running' ,
createdAt : new Date ( '2021-09-01T13:00:00' ) ,
} ,
{
commit : 'oldrelease' ,
serviceName : 'fedora' ,
status : 'Stopped' ,
createdAt : new Date ( '2021-09-01T12:00:00' ) ,
} ,
{
// Service without an image should not show on the final state
appUuid : 'debian' ,
commit : 'otherrelease' ,
serviceName : 'debian' ,
status : 'Stopped' ,
createdAt : new Date ( '2021-09-01T12:00:00' ) ,
} ,
] ) ;
expect ( await applicationManager . getState ( ) ) . to . deep . equal ( {
myapp : {
releases : {
latestrelease : {
services : {
ubuntu : {
image : 'ubuntu:latest' ,
status : 'Running' ,
} ,
alpine : {
image : 'alpine:latest' ,
status : 'Downloading' ,
download_progress : 50 ,
} ,
2022-04-08 01:21:44 +00:00
node : {
image : 'node:latest' ,
status : 'Downloading' ,
download_progress : 0 ,
} ,
2021-09-01 22:13:27 +00:00
} ,
} ,
} ,
} ,
fedora : {
releases : {
oldrelease : {
services : {
fedora : {
image : 'fedora:older' ,
status : 'Stopped' ,
} ,
} ,
} ,
} ,
} ,
} ) ;
} ) ;
it ( 'reports handover state if multiple services are running for the same app' , async ( ) = > {
getImagesState . resolves ( [
{
name : 'alpine:3.13' ,
commit : 'latestrelease' ,
appUuid : 'myapp' ,
serviceName : 'alpine' ,
status : 'Downloaded' ,
} ,
{
name : 'alpine:3.12' ,
commit : 'oldrelease' ,
appUuid : 'myapp' ,
serviceName : 'alpine' ,
status : 'Downloaded' ,
} ,
] ) ;
getServicesState . resolves ( [
{
commit : 'latestrelease' ,
appUuid : 'myapp' ,
serviceName : 'alpine' ,
status : 'Running' ,
createdAt : new Date ( '2021-09-01T13:00:00' ) ,
} ,
{
commit : 'oldrelease' ,
appUuid : 'myapp' ,
serviceName : 'alpine' ,
status : 'Running' ,
createdAt : new Date ( '2021-09-01T12:00:00' ) ,
} ,
] ) ;
expect ( await applicationManager . getState ( ) ) . to . deep . equal ( {
myapp : {
releases : {
latestrelease : {
services : {
alpine : {
image : 'alpine:3.13' ,
status : 'Awaiting handover' ,
} ,
} ,
} ,
oldrelease : {
services : {
alpine : {
image : 'alpine:3.12' ,
status : 'Handing over' ,
} ,
} ,
} ,
} ,
} ,
} ) ;
} ) ;
} ) ;
2021-05-05 20:32:31 +00:00
} ) ;