Skip to content

Commit

Permalink
WIP run builder test
Browse files Browse the repository at this point in the history
Change-type: minor
  • Loading branch information
otaviojacobi committed Sep 5, 2024
1 parent eb4ce1d commit a1c8244
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 32 deletions.
76 changes: 76 additions & 0 deletions lib/multibuild/balena-contract-features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { BuildTask } from './build-task';
import type * as Compose from '../parse';

export interface ContractFeature {
type: string;
version?: string;
}

export interface Contract {
name: string;
type: 'sw.container';
slug: string;
requires?: ContractFeature[];
}

export function insertBalenaCustomContractFeatures(
task: BuildTask,
image: Compose.ImageDescriptor,
): void {
insertDependsOnServiceHeathyFeature(task, image);
}

function insertDependsOnServiceHeathyFeature(
task: BuildTask,
image: Compose.ImageDescriptor,
): void {
const serviceNames = Object.keys(image.originalComposition?.services ?? {});
for (const serviceName of serviceNames) {
const service = image.originalComposition?.services[serviceName];
if (service?.depends_on != null) {
for (const dep of service.depends_on) {
if (dep === 'service_healthy' || dep === 'service-healthy') {
const feature: ContractFeature = {
type: 'sw.private.compose.service-healthy-depends-on',
version: '1.0.0',
};

insertContractFeature(task, feature);
return;
}
}
}
}
}

function insertContractFeature(
task: BuildTask,
feature: ContractFeature,
): void {
if (task.contract == null) {
task.contract = defaultContract();
}

if (
task.contract.requires
?.map((require) => require.type)
.includes(feature.type)
) {
return;
}

if (task.contract.requires != null) {
task.contract.requires.push(feature);
} else {
task.contract.requires = [feature];
}
}

function defaultContract(): Contract {
return {
name: 'default',
type: 'sw.container',
slug: 'default',
requires: [],
};
}
3 changes: 2 additions & 1 deletion lib/multibuild/build-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { ProgressCallback } from 'docker-progress';
import type * as Stream from 'stream';
import type * as tar from 'tar-stream';
import type BuildMetadata from './build-metadata';
import type { Contract } from './balena-contract-features';

/**
* A structure representing a list of build tasks to be performed,
Expand Down Expand Up @@ -137,7 +138,7 @@ export interface BuildTask {
/**
* The container contract for this service
*/
contract?: Dictionary<unknown>;
contract?: Contract;

/**
* Promise to ensure that build task is resolved before
Expand Down
7 changes: 4 additions & 3 deletions lib/multibuild/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as TarUtils from 'tar-utils';

import type { BuildTask } from './build-task';
import { ContractValidationError, NonUniqueContractNameError } from './errors';
import type { Contract } from './balena-contract-features';

export const CONTRACT_TYPE = 'sw.container';

Expand All @@ -28,14 +29,14 @@ export function isContractFile(filename: string): boolean {
return normalized === 'contract.yml' || normalized === 'contract.yaml';
}

export function processContract(buffer: Buffer): Dictionary<unknown> {
export function processContract(buffer: Buffer): Contract {
const parsedBuffer = jsYaml.load(buffer.toString('utf8'));

if (parsedBuffer == null || typeof parsedBuffer !== 'object') {
throw new ContractValidationError('Container contract must be an object');
}

const contractObj = parsedBuffer as Dictionary<unknown>;
const contractObj = parsedBuffer as Dictionary<any>;

if (contractObj.name == null) {
throw new ContractValidationError(
Expand All @@ -59,7 +60,7 @@ export function processContract(buffer: Buffer): Dictionary<unknown> {
);
}

return contractObj;
return contractObj as Contract;
}

export function checkContractNamesUnique(tasks: BuildTask[]) {
Expand Down
8 changes: 8 additions & 0 deletions lib/multibuild/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { posixContains } from './path-utils';
import type { RegistrySecrets } from './registry-secrets';
import { ResolveListeners, resolveTask } from './resolve';
import * as Utils from './utils';
import { insertBalenaCustomContractFeatures } from './balena-contract-features';

export { QEMU_BIN_NAME } from './build-metadata';
export * from './build-task';
Expand Down Expand Up @@ -124,6 +125,13 @@ export async function fromImageDescriptors(
task.contract = contracts.processContract(buf);
}

const image = images.find(
(i) => i.serviceName === task.serviceName,
);
if (image != null) {
insertBalenaCustomContractFeatures(task, image);
}

const newHeader = _.cloneDeep(header);
newHeader.name = relative;
task.buildStream!.entry(newHeader, buf);
Expand Down
18 changes: 13 additions & 5 deletions lib/parse/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,18 @@ function normalizeService(
if (!Array.isArray(service.depends_on)) {
// Try to convert long-form into list-of-strings
service.depends_on = _.map(service.depends_on, (dep, serviceName) => {
if (['service_started', 'service-started'].includes(dep.condition)) {
if (
[
'service_started',
'service-started',
'service_healthy',
'service-healthy',
].includes(dep.condition)
) {
return serviceName;
}
throw new ValidationError(
'Only "service_started" type of service dependency is supported',
'Only "service_started" and "service_healthy" type of service dependency are supported',
);
});
}
Expand Down Expand Up @@ -532,16 +539,17 @@ export function parse(c: Composition): ImageDescriptor[] {
throw new Error('Unsupported composition version');
}
return _.toPairs(c.services).map(([name, service]) => {
return createImageDescriptor(name, service);
return createImageDescriptor(name, service, c);
});
}

function createImageDescriptor(
serviceName: string,
service: Service,
originalComposition?: Composition,
): ImageDescriptor {
if (service.image && !service.build) {
return { serviceName, image: service.image };
return { serviceName, image: service.image, originalComposition };
}

if (!service.build) {
Expand All @@ -556,7 +564,7 @@ function createImageDescriptor(
build.tag = service.image;
}

return { serviceName, image: build };
return { serviceName, image: build, originalComposition };
}

function normalizeKeyValuePairs(
Expand Down
1 change: 1 addition & 0 deletions lib/parse/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,5 @@ export interface BuildConfig {
export interface ImageDescriptor {
serviceName: string;
image: string | BuildConfig;
originalComposition?: Composition;
}
23 changes: 0 additions & 23 deletions test/parse/all.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,29 +389,6 @@ describe('validation', () => {
expect(f).to.not.throw();
});

it('should throw when long syntax depends_on does not specify service_started condition', async () => {
const f = () => {
compose.normalize({
version: '2.4',
services: {
main: {
build: '.',
depends_on: {
dependency: { condition: 'service_healthy' },
},
},
dependency: {
build: '.',
},
},
});
};
expect(f).to.throw(
ValidationError,
'Only "service_started" type of service dependency is supported',
);
});

it('should throw when long syntax tmpfs mounts specify options', async () => {
const f = () => {
compose.normalize({
Expand Down

0 comments on commit a1c8244

Please sign in to comment.