Merge pull request #1802 from balena-os/service-config-helpers

Check config for networks and volumes inside Service
This commit is contained in:
bulldozer-balena[bot] 2021-10-28 16:07:52 +00:00 committed by GitHub
commit 98a53767c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 85 additions and 78 deletions

View File

@ -145,17 +145,13 @@ export class App {
// Generate volume steps // Generate volume steps
steps = steps.concat( steps = steps.concat(
this.generateStepsForComponent(volumeChanges, servicePairs, (v, svc) => this.generateStepsForComponent(volumeChanges, servicePairs, (v, svc) =>
// TODO: Volumes are stored without the appId prepended, but networks are stored svc.hasVolume(v.name),
// with it prepended. Sort out this inequality
svc.config.volumes.includes(v.name),
), ),
); );
// Generate network steps // Generate network steps
steps = steps.concat( steps = steps.concat(
this.generateStepsForComponent( this.generateStepsForComponent(networkChanges, servicePairs, (n, svc) =>
networkChanges, svc.hasNetwork(n.name),
servicePairs,
(n, svc) => `${this.appId}_${n.name}` in svc.config.networks,
), ),
); );
@ -570,21 +566,14 @@ export class App {
networkPairs: Array<ChangingPair<Network>>, networkPairs: Array<ChangingPair<Network>>,
volumePairs: Array<ChangingPair<Volume>>, volumePairs: Array<ChangingPair<Volume>>,
): boolean { ): boolean {
const serviceVolumes = svc.config.volumes; return (
for (const { current } of volumePairs) { volumePairs.some(
if (current && serviceVolumes.includes(`${this.appId}_${current.name}`)) { ({ current }) => current && svc.hasVolume(current.name),
return true; ) ||
} networkPairs.some(
} ({ current }) => current && svc.hasNetwork(current.name),
)
const serviceNetworks = svc.config.networks; );
for (const { current } of networkPairs) {
if (current && `${this.appId}_${current.name}` in serviceNetworks) {
return true;
}
}
return false;
} }
private generateContainerStep(current: Service, target: Service) { private generateContainerStep(current: Service, target: Service) {
@ -657,33 +646,20 @@ export class App {
return false; return false;
} }
// Wait for networks to be created before starting the service
if ( if (
_.some( networkPairs.some(
networkPairs, (pair) => pair.target && target.hasNetworkMode(pair.target.name),
(pair) =>
`${this.appId}_${pair.target?.name}` === target.config.networkMode,
) )
) { ) {
return false; return false;
} }
// Wait for volumes to be created before starting the service
if ( if (
_.some(target.config.volumes, (volumeDefinition) => { volumePairs.some(
const [sourceName, destName] = volumeDefinition.split(':'); (pair) => pair.target && target.hasVolume(pair.target.name),
if (destName == null) { )
// If this is not a named volume, ignore it
return false;
}
if (sourceName[0] === '/') {
// Absolute paths should also be ignored
return false;
}
return _.some(
volumePairs,
(pair) => `${target.appId}_${pair.target?.name}` === sourceName,
);
})
) { ) {
return false; return false;
} }

View File

@ -877,32 +877,6 @@ export class Service {
); );
} }
public getNamedVolumes() {
const defaults = Service.defaultBinds(
this.appId || 0,
this.serviceName || '',
);
const validVolumes = _.map(this.config.volumes, (volume) => {
if (_.includes(defaults, volume) || !_.includes(volume, ':')) {
return null;
}
const bindSource = volume.split(':')[0];
if (!path.isAbsolute(bindSource)) {
const match = bindSource.match(/[0-9]+_(.+)/);
if (match == null) {
log.error(
`Error: There was an error parsing a volume bind source, ignoring.\nBind source: ${bindSource}`,
);
return null;
}
return match[1];
}
return null;
});
return _.reject(validVolumes, _.isNil);
}
public handoverCompleteFullPathsOnHost(): string[] { public handoverCompleteFullPathsOnHost(): string[] {
const lockPath = updateLock.lockPath( const lockPath = updateLock.lockPath(
this.appId || 0, this.appId || 0,
@ -995,6 +969,32 @@ export class Service {
return env; return env;
} }
public hasNetwork(networkName: string) {
// TODO; we could probably export network naming methods to another
// module to avoid duplicate code
return `${this.appId}_${networkName}` in this.config.networks;
}
public hasNetworkMode(networkName: string) {
return `${this.appId}_${networkName}` === this.config.networkMode;
}
public hasVolume(volumeName: string) {
return this.config.volumes.some((volumeDefinition) => {
const [sourceName, destName] = volumeDefinition.split(':');
if (destName == null) {
// If this is not a named volume, ignore it
return false;
}
if (sourceName[0] === '/') {
// Absolute paths should also be ignored
return false;
}
return `${this.appId}_${volumeName}` === sourceName;
});
}
private isSameNetwork( private isSameNetwork(
current: ServiceConfig['networks'][0], current: ServiceConfig['networks'][0],
target: ServiceConfig['networks'][0], target: ServiceConfig['networks'][0],
@ -1086,6 +1086,8 @@ export class Service {
log.warn(`Ignoring invalid bind mount ${volume}`); log.warn(`Ignoring invalid bind mount ${volume}`);
} }
} else { } else {
// Push anonymous volume. Anonymous volumes can only be
// set by using the `VOLUME` instruction inside a dockerfile
volumes.push(volume); volumes.push(volume);
} }
}); });

View File

@ -76,7 +76,13 @@ function createImage(
...extra ...extra
} = {} as Partial<Image>, } = {} as Partial<Image>,
) { ) {
return { appId, dependent, name, serviceName, ...extra } as Image; return {
appId,
dependent,
name,
serviceName,
...extra,
} as Image;
} }
const expectSteps = ( const expectSteps = (
@ -240,11 +246,19 @@ describe('compose/app', () => {
it('should kill dependencies of a volume before changing config', async () => { it('should kill dependencies of a volume before changing config', async () => {
const current = createApp({ const current = createApp({
services: [await createService({ volumes: ['test-volume'] })], services: [
await createService({
volumes: ['test-volume:/data'],
}),
],
volumes: [Volume.fromComposeObject('test-volume', 1, {})], volumes: [Volume.fromComposeObject('test-volume', 1, {})],
}); });
const target = createApp({ const target = createApp({
services: [await createService({ volumes: ['test-volume'] })], services: [
await createService({
volumes: ['test-volume:/data'],
}),
],
volumes: [ volumes: [
Volume.fromComposeObject('test-volume', 1, { Volume.fromComposeObject('test-volume', 1, {
labels: { test: 'test' }, labels: { test: 'test' },
@ -276,7 +290,9 @@ describe('compose/app', () => {
}); });
it('should not output a kill step for a service which is already stopping when changing a volume', async () => { it('should not output a kill step for a service which is already stopping when changing a volume', async () => {
const service = await createService({ volumes: ['test-volume'] }); const service = await createService({
volumes: ['test-volume:/data'],
});
service.status = 'Stopping'; service.status = 'Stopping';
const current = createApp({ const current = createApp({
services: [service], services: [service],
@ -298,7 +314,7 @@ describe('compose/app', () => {
it('should generate the correct step sequence for a volume purge request', async () => { it('should generate the correct step sequence for a volume purge request', async () => {
const service = await createService({ const service = await createService({
volumes: ['db-volume'], volumes: ['db-volume:/data'],
image: 'test-image', image: 'test-image',
}); });
const volume = Volume.fromComposeObject('db-volume', service.appId, {}); const volume = Volume.fromComposeObject('db-volume', service.appId, {});
@ -364,7 +380,7 @@ describe('compose/app', () => {
volumes: [], volumes: [],
}); });
// Step 3: start & createVolume // Step 3: createVolume
service.status = 'Running'; service.status = 'Running';
const target = createApp({ const target = createApp({
services: [service], services: [service],
@ -372,13 +388,26 @@ describe('compose/app', () => {
volumes: [volume], volumes: [volume],
isTarget: true, isTarget: true,
}); });
const finalSteps = currentWithVolumesRemoved.nextStepsForAppUpdate( const recreateVolumeSteps = currentWithVolumesRemoved.nextStepsForAppUpdate(
contextWithImages, contextWithImages,
target, target,
); );
expect(finalSteps).to.have.length(2);
expectSteps('start', finalSteps); expect(recreateVolumeSteps).to.have.length(1);
expectSteps('createVolume', finalSteps); expectSteps('createVolume', recreateVolumeSteps);
// Final step: start service
const currentWithVolumeRecreated = createApp({
services: [],
networks: [defaultNetwork],
volumes: [volume],
});
const createServiceSteps = currentWithVolumeRecreated.nextStepsForAppUpdate(
contextWithImages,
target,
);
expectSteps('start', createServiceSteps);
}); });
}); });