mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-06-19 15:53:46 +00:00
Rename networks to <appUuid>_<networkName>
This is required as we are phasing out app ids and we need to be able to get app uuid from the current state of the network. The app-id now exists as a container in new networks This commit will restart containers as it needs to recreate the network.
This commit is contained in:
@ -29,6 +29,7 @@ import { pathExistsOnHost } from '../lib/fs-utils';
|
|||||||
|
|
||||||
export interface AppConstructOpts {
|
export interface AppConstructOpts {
|
||||||
appId: number;
|
appId: number;
|
||||||
|
appUuid?: string;
|
||||||
appName?: string;
|
appName?: string;
|
||||||
commit?: string;
|
commit?: string;
|
||||||
source?: string;
|
source?: string;
|
||||||
@ -52,6 +53,7 @@ interface ChangingPair<T> {
|
|||||||
|
|
||||||
export class App {
|
export class App {
|
||||||
public appId: number;
|
public appId: number;
|
||||||
|
public appUuid?: string;
|
||||||
// When setting up an application from current state, these values are not available
|
// When setting up an application from current state, these values are not available
|
||||||
public appName?: string;
|
public appName?: string;
|
||||||
public commit?: string;
|
public commit?: string;
|
||||||
@ -65,6 +67,7 @@ export class App {
|
|||||||
|
|
||||||
public constructor(opts: AppConstructOpts, public isTargetState: boolean) {
|
public constructor(opts: AppConstructOpts, public isTargetState: boolean) {
|
||||||
this.appId = opts.appId;
|
this.appId = opts.appId;
|
||||||
|
this.appUuid = opts.appUuid;
|
||||||
this.appName = opts.appName;
|
this.appName = opts.appName;
|
||||||
this.commit = opts.commit;
|
this.commit = opts.commit;
|
||||||
this.source = opts.source;
|
this.source = opts.source;
|
||||||
@ -77,6 +80,7 @@ export class App {
|
|||||||
this.networks.default = Network.fromComposeObject(
|
this.networks.default = Network.fromComposeObject(
|
||||||
'default',
|
'default',
|
||||||
opts.appId,
|
opts.appId,
|
||||||
|
opts.appUuid!, // app uuid always exists on the target state
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -160,7 +164,6 @@ export class App {
|
|||||||
target.commit != null &&
|
target.commit != null &&
|
||||||
this.commit !== target.commit
|
this.commit !== target.commit
|
||||||
) {
|
) {
|
||||||
// TODO: The next PR should change this to support multiapp commit values
|
|
||||||
steps.push(
|
steps.push(
|
||||||
generateStep('updateCommit', {
|
generateStep('updateCommit', {
|
||||||
target: target.commit,
|
target: target.commit,
|
||||||
@ -732,7 +735,7 @@ export class App {
|
|||||||
const networks = _.mapValues(
|
const networks = _.mapValues(
|
||||||
JSON.parse(app.networks) ?? {},
|
JSON.parse(app.networks) ?? {},
|
||||||
(conf, name) => {
|
(conf, name) => {
|
||||||
return Network.fromComposeObject(name, app.appId, conf ?? {});
|
return Network.fromComposeObject(name, app.appId, app.uuid, conf ?? {});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -799,6 +802,7 @@ export class App {
|
|||||||
return new App(
|
return new App(
|
||||||
{
|
{
|
||||||
appId: app.appId,
|
appId: app.appId,
|
||||||
|
appUuid: app.uuid,
|
||||||
commit: app.commit,
|
commit: app.commit,
|
||||||
appName: app.name,
|
appName: app.name,
|
||||||
source: app.source,
|
source: app.source,
|
||||||
|
@ -23,16 +23,12 @@ export function getAll(): Bluebird<Network[]> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllByAppId(appId: number): Bluebird<Network[]> {
|
async function get(network: {
|
||||||
return getAll().filter((network: Network) => network.appId === appId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function get(network: {
|
|
||||||
name: string;
|
name: string;
|
||||||
appId: number;
|
appUuid: string;
|
||||||
}): Promise<Network> {
|
}): Promise<Network> {
|
||||||
const dockerNet = await docker
|
const dockerNet = await docker
|
||||||
.getNetwork(Network.generateDockerName(network.appId, network.name))
|
.getNetwork(Network.generateDockerName(network.appUuid, network.name))
|
||||||
.inspect();
|
.inspect();
|
||||||
return Network.fromDockerNetwork(dockerNet);
|
return Network.fromDockerNetwork(dockerNet);
|
||||||
}
|
}
|
||||||
@ -41,7 +37,7 @@ export async function create(network: Network) {
|
|||||||
try {
|
try {
|
||||||
const existing = await get({
|
const existing = await get({
|
||||||
name: network.name,
|
name: network.name,
|
||||||
appId: network.appId,
|
appUuid: network.appUuid!, // new networks will always have uuid
|
||||||
});
|
});
|
||||||
if (!network.isEqualConfig(existing)) {
|
if (!network.isEqualConfig(existing)) {
|
||||||
throw new ResourceRecreationAttemptError('network', network.name);
|
throw new ResourceRecreationAttemptError('network', network.name);
|
||||||
@ -52,7 +48,7 @@ export async function create(network: Network) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!NotFoundError(e)) {
|
if (!NotFoundError(e)) {
|
||||||
logger.logSystemEvent(logTypes.createNetworkError, {
|
logger.logSystemEvent(logTypes.createNetworkError, {
|
||||||
network: { name: network.name, appId: network.appId },
|
network: { name: network.name, appUuid: network.appUuid },
|
||||||
error: e,
|
error: e,
|
||||||
});
|
});
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import * as Bluebird from 'bluebird';
|
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as dockerode from 'dockerode';
|
import * as dockerode from 'dockerode';
|
||||||
|
|
||||||
@ -11,29 +10,64 @@ import * as ComposeUtils from './utils';
|
|||||||
import { ComposeNetworkConfig, NetworkConfig } from './types/network';
|
import { ComposeNetworkConfig, NetworkConfig } from './types/network';
|
||||||
|
|
||||||
import { InvalidNetworkNameError } from './errors';
|
import { InvalidNetworkNameError } from './errors';
|
||||||
|
import { InternalInconsistencyError } from '../lib/errors';
|
||||||
|
|
||||||
export class Network {
|
export class Network {
|
||||||
public appId: number;
|
public appId: number;
|
||||||
|
public appUuid?: string;
|
||||||
public name: string;
|
public name: string;
|
||||||
public config: NetworkConfig;
|
public config: NetworkConfig;
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
|
private static deconstructDockerName(
|
||||||
|
name: string,
|
||||||
|
): { name: string; appId?: number; appUuid?: string } {
|
||||||
|
const matchWithAppId = name.match(/^(\d+)_(\S+)/);
|
||||||
|
if (matchWithAppId == null) {
|
||||||
|
const matchWithAppUuid = name.match(/^([0-9a-f-A-F]{32,})_(\S+)/);
|
||||||
|
|
||||||
|
if (!matchWithAppUuid) {
|
||||||
|
throw new InvalidNetworkNameError(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const appUuid = matchWithAppUuid[1];
|
||||||
|
return { name: matchWithAppUuid[2], appUuid };
|
||||||
|
}
|
||||||
|
|
||||||
|
const appId = parseInt(matchWithAppId[1], 10);
|
||||||
|
if (isNaN(appId)) {
|
||||||
|
throw new InvalidNetworkNameError(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
appId,
|
||||||
|
name: matchWithAppId[2],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public static fromDockerNetwork(
|
public static fromDockerNetwork(
|
||||||
network: dockerode.NetworkInspectInfo,
|
network: dockerode.NetworkInspectInfo,
|
||||||
): Network {
|
): Network {
|
||||||
const ret = new Network();
|
const ret = new Network();
|
||||||
|
|
||||||
const match = network.Name.match(/^([0-9]+)_(.+)$/);
|
// Detect the name and appId from the inspect data
|
||||||
if (match == null) {
|
const { name, appId, appUuid } = Network.deconstructDockerName(
|
||||||
throw new InvalidNetworkNameError(network.Name);
|
network.Name,
|
||||||
|
);
|
||||||
|
|
||||||
|
const labels = network.Labels ?? {};
|
||||||
|
if (!appId && isNaN(parseInt(labels['io.balena.app-id'], 10))) {
|
||||||
|
// This should never happen as supervised networks will always have either
|
||||||
|
// the id or the label
|
||||||
|
throw new InternalInconsistencyError(
|
||||||
|
`Could not read app id from network: ${network.Name}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the regex match succeeds `match[1]` should be a number
|
ret.appId = appId ?? parseInt(labels['io.balena.app-id'], 10);
|
||||||
const appId = parseInt(match[1], 10);
|
ret.name = name;
|
||||||
|
ret.appUuid = appUuid;
|
||||||
ret.appId = appId;
|
|
||||||
ret.name = match[2];
|
|
||||||
|
|
||||||
const config = network.IPAM?.Config || [];
|
const config = network.IPAM?.Config || [];
|
||||||
|
|
||||||
@ -51,7 +85,7 @@ export class Network {
|
|||||||
},
|
},
|
||||||
enableIPv6: network.EnableIPv6,
|
enableIPv6: network.EnableIPv6,
|
||||||
internal: network.Internal,
|
internal: network.Internal,
|
||||||
labels: _.omit(ComposeUtils.normalizeLabels(network.Labels ?? {}), [
|
labels: _.omit(ComposeUtils.normalizeLabels(labels), [
|
||||||
'io.balena.supervised',
|
'io.balena.supervised',
|
||||||
]),
|
]),
|
||||||
options: network.Options ?? {},
|
options: network.Options ?? {},
|
||||||
@ -63,6 +97,7 @@ export class Network {
|
|||||||
public static fromComposeObject(
|
public static fromComposeObject(
|
||||||
name: string,
|
name: string,
|
||||||
appId: number,
|
appId: number,
|
||||||
|
appUuid: string,
|
||||||
network: Partial<Omit<ComposeNetworkConfig, 'ipam'>> & {
|
network: Partial<Omit<ComposeNetworkConfig, 'ipam'>> & {
|
||||||
ipam?: Partial<ComposeNetworkConfig['ipam']>;
|
ipam?: Partial<ComposeNetworkConfig['ipam']>;
|
||||||
},
|
},
|
||||||
@ -70,6 +105,7 @@ export class Network {
|
|||||||
const net = new Network();
|
const net = new Network();
|
||||||
net.name = name;
|
net.name = name;
|
||||||
net.appId = appId;
|
net.appId = appId;
|
||||||
|
net.appUuid = appUuid;
|
||||||
|
|
||||||
Network.validateComposeConfig(network);
|
Network.validateComposeConfig(network);
|
||||||
|
|
||||||
@ -95,12 +131,13 @@ export class Network {
|
|||||||
},
|
},
|
||||||
enableIPv6: network.enable_ipv6 || false,
|
enableIPv6: network.enable_ipv6 || false,
|
||||||
internal: network.internal || false,
|
internal: network.internal || false,
|
||||||
labels: network.labels || {},
|
labels: {
|
||||||
|
'io.balena.app-id': String(appId),
|
||||||
|
...ComposeUtils.normalizeLabels(network.labels || {}),
|
||||||
|
},
|
||||||
options: network.driver_opts || {},
|
options: network.driver_opts || {},
|
||||||
};
|
};
|
||||||
|
|
||||||
net.config.labels = ComposeUtils.normalizeLabels(net.config.labels);
|
|
||||||
|
|
||||||
return net;
|
return net;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,7 +154,7 @@ export class Network {
|
|||||||
|
|
||||||
public async create(): Promise<void> {
|
public async create(): Promise<void> {
|
||||||
logger.logSystemEvent(logTypes.createNetwork, {
|
logger.logSystemEvent(logTypes.createNetwork, {
|
||||||
network: { name: this.name },
|
network: { name: this.name, appUuid: this.appUuid },
|
||||||
});
|
});
|
||||||
|
|
||||||
await docker.createNetwork(this.toDockerConfig());
|
await docker.createNetwork(this.toDockerConfig());
|
||||||
@ -125,7 +162,7 @@ export class Network {
|
|||||||
|
|
||||||
public toDockerConfig(): dockerode.NetworkCreateOptions {
|
public toDockerConfig(): dockerode.NetworkCreateOptions {
|
||||||
return {
|
return {
|
||||||
Name: Network.generateDockerName(this.appId, this.name),
|
Name: Network.generateDockerName(this.appUuid!, this.name),
|
||||||
Driver: this.config.driver,
|
Driver: this.config.driver,
|
||||||
CheckDuplicate: true,
|
CheckDuplicate: true,
|
||||||
Options: this.config.options,
|
Options: this.config.options,
|
||||||
@ -153,28 +190,41 @@ export class Network {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public remove(): Bluebird<void> {
|
public async remove() {
|
||||||
logger.logSystemEvent(logTypes.removeNetwork, {
|
logger.logSystemEvent(logTypes.removeNetwork, {
|
||||||
network: { name: this.name, appId: this.appId },
|
network: { name: this.name, appUuid: this.appUuid },
|
||||||
});
|
});
|
||||||
|
|
||||||
const networkName = Network.generateDockerName(this.appId, this.name);
|
// Find the network
|
||||||
|
const [networkName] = (await docker.listNetworks())
|
||||||
return Bluebird.resolve(docker.listNetworks())
|
.filter((network) => {
|
||||||
.then((networks) => networks.filter((n) => n.Name === networkName))
|
try {
|
||||||
.then(([network]) => {
|
const { appId, appUuid, name } = Network.deconstructDockerName(
|
||||||
if (!network) {
|
network.Name,
|
||||||
return Bluebird.resolve();
|
);
|
||||||
|
return (
|
||||||
|
name === this.name &&
|
||||||
|
(appId === this.appId || appUuid === this.appUuid)
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return Bluebird.resolve(
|
})
|
||||||
docker.getNetwork(networkName).remove(),
|
.map((network) => network.Name);
|
||||||
).tapCatch((error) => {
|
|
||||||
|
if (!networkName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await docker.getNetwork(networkName).remove();
|
||||||
|
} catch (error) {
|
||||||
logger.logSystemEvent(logTypes.removeNetworkError, {
|
logger.logSystemEvent(logTypes.removeNetworkError, {
|
||||||
network: { name: this.name, appId: this.appId },
|
network: { name: this.name, appUuid: this.appUuid },
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
});
|
throw error;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public isEqualConfig(network: Network): boolean {
|
public isEqualConfig(network: Network): boolean {
|
||||||
@ -210,8 +260,8 @@ export class Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static generateDockerName(appId: number, name: string) {
|
public static generateDockerName(appIdOrUuid: number | string, name: string) {
|
||||||
return `${appId}_${name}`;
|
return `${appIdOrUuid}_${name}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,7 +171,7 @@ export class Service {
|
|||||||
networks = config.networks || {};
|
networks = config.networks || {};
|
||||||
}
|
}
|
||||||
// Prefix the network entries with the app id
|
// Prefix the network entries with the app id
|
||||||
networks = _.mapKeys(networks, (_v, k) => `${service.appId}_${k}`);
|
networks = _.mapKeys(networks, (_v, k) => `${service.appUuid}_${k}`);
|
||||||
// Ensure that we add an alias of the service name
|
// Ensure that we add an alias of the service name
|
||||||
networks = _.mapValues(networks, (v) => {
|
networks = _.mapValues(networks, (v) => {
|
||||||
if (v.aliases == null) {
|
if (v.aliases == null) {
|
||||||
@ -257,7 +257,7 @@ export class Service {
|
|||||||
) {
|
) {
|
||||||
if (networks[config.networkMode!] == null && !serviceNetworkMode) {
|
if (networks[config.networkMode!] == null && !serviceNetworkMode) {
|
||||||
// The network mode has not been set explicitly
|
// The network mode has not been set explicitly
|
||||||
config.networkMode = `${service.appId}_${config.networkMode}`;
|
config.networkMode = `${service.appUuid}_${config.networkMode}`;
|
||||||
// If we don't have any networks, we need to
|
// If we don't have any networks, we need to
|
||||||
// create the default with some default options
|
// create the default with some default options
|
||||||
networks[config.networkMode] = {
|
networks[config.networkMode] = {
|
||||||
@ -1008,11 +1008,23 @@ export class Service {
|
|||||||
public hasNetwork(networkName: string) {
|
public hasNetwork(networkName: string) {
|
||||||
// TODO; we could probably export network naming methods to another
|
// TODO; we could probably export network naming methods to another
|
||||||
// module to avoid duplicate code
|
// module to avoid duplicate code
|
||||||
return `${this.appId}_${networkName}` in this.config.networks;
|
// We don't know if this service is current or target state so we need
|
||||||
|
// to check both appId and appUuid since the current service may still
|
||||||
|
// have appId
|
||||||
|
return (
|
||||||
|
`${this.appUuid}_${networkName}` in this.config.networks ||
|
||||||
|
`${this.appId}_${networkName}` in this.config.networks
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasNetworkMode(networkName: string) {
|
public hasNetworkMode(networkName: string) {
|
||||||
return `${this.appId}_${networkName}` === this.config.networkMode;
|
// We don't know if this service is current or target state so we need
|
||||||
|
// to check both appId and appUuid since the current service may still
|
||||||
|
// have appId
|
||||||
|
return (
|
||||||
|
`${this.appUuid}_${networkName}` === this.config.networkMode ||
|
||||||
|
`${this.appId}_${networkName}` === this.config.networkMode
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasVolume(volumeName: string) {
|
public hasVolume(volumeName: string) {
|
||||||
|
@ -12,7 +12,7 @@ import { withMockerode } from './lib/mockerode';
|
|||||||
|
|
||||||
function getDefaultNetwork(appId: number) {
|
function getDefaultNetwork(appId: number) {
|
||||||
return {
|
return {
|
||||||
default: Network.fromComposeObject('default', appId, {}),
|
default: Network.fromComposeObject('default', appId, 'deadbeef', {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
"Type": "journald",
|
"Type": "journald",
|
||||||
"Config": {}
|
"Config": {}
|
||||||
},
|
},
|
||||||
"NetworkMode": "1011165_default",
|
"NetworkMode": "aaaaaaaa_default",
|
||||||
"PortBindings": {},
|
"PortBindings": {},
|
||||||
"RestartPolicy": {
|
"RestartPolicy": {
|
||||||
"Name": "always",
|
"Name": "always",
|
||||||
@ -209,7 +209,7 @@
|
|||||||
"IPv6Gateway": "",
|
"IPv6Gateway": "",
|
||||||
"MacAddress": "",
|
"MacAddress": "",
|
||||||
"Networks": {
|
"Networks": {
|
||||||
"1011165_default": {
|
"aaaaaaaa_default": {
|
||||||
"IPAMConfig": {},
|
"IPAMConfig": {},
|
||||||
"Links": null,
|
"Links": null,
|
||||||
"Aliases": [
|
"Aliases": [
|
||||||
|
@ -209,7 +209,7 @@
|
|||||||
"IPv6Gateway": "",
|
"IPv6Gateway": "",
|
||||||
"MacAddress": "",
|
"MacAddress": "",
|
||||||
"Networks": {
|
"Networks": {
|
||||||
"1011165_default": {
|
"aaaaaaaa_default": {
|
||||||
"IPAMConfig": null,
|
"IPAMConfig": null,
|
||||||
"Links": null,
|
"Links": null,
|
||||||
"Aliases": [
|
"Aliases": [
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
"Type": "journald",
|
"Type": "journald",
|
||||||
"Config": {}
|
"Config": {}
|
||||||
},
|
},
|
||||||
"NetworkMode": "1011165_default",
|
"NetworkMode": "aaaaaaaa_default",
|
||||||
"PortBindings": {},
|
"PortBindings": {},
|
||||||
"RestartPolicy": {
|
"RestartPolicy": {
|
||||||
"Name": "always",
|
"Name": "always",
|
||||||
@ -209,7 +209,7 @@
|
|||||||
"IPv6Gateway": "",
|
"IPv6Gateway": "",
|
||||||
"MacAddress": "",
|
"MacAddress": "",
|
||||||
"Networks": {
|
"Networks": {
|
||||||
"1011165_default": {
|
"aaaaaaaa_default": {
|
||||||
"IPAMConfig": null,
|
"IPAMConfig": null,
|
||||||
"Links": null,
|
"Links": null,
|
||||||
"Aliases": [
|
"Aliases": [
|
||||||
|
@ -4,7 +4,6 @@ import rewire = require('rewire');
|
|||||||
|
|
||||||
import { unlinkAll } from '../../src/lib/fs-utils';
|
import { unlinkAll } from '../../src/lib/fs-utils';
|
||||||
import * as applicationManager from '../../src/compose/application-manager';
|
import * as applicationManager from '../../src/compose/application-manager';
|
||||||
import * as networkManager from '../../src/compose/network-manager';
|
|
||||||
import * as serviceManager from '../../src/compose/service-manager';
|
import * as serviceManager from '../../src/compose/service-manager';
|
||||||
import * as volumeManager from '../../src/compose/volume-manager';
|
import * as volumeManager from '../../src/compose/volume-manager';
|
||||||
import * as commitStore from '../../src/compose/commit';
|
import * as commitStore from '../../src/compose/commit';
|
||||||
@ -185,7 +184,6 @@ function buildRoutes(): Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TO-DO: Create a cleaner way to restore previous values.
|
// TO-DO: Create a cleaner way to restore previous values.
|
||||||
const originalNetGetAll = networkManager.getAllByAppId;
|
|
||||||
const originalVolGetAll = volumeManager.getAllByAppId;
|
const originalVolGetAll = volumeManager.getAllByAppId;
|
||||||
const originalSvcGetAppId = serviceManager.getAllByAppId;
|
const originalSvcGetAppId = serviceManager.getAllByAppId;
|
||||||
const originalSvcGetStatus = serviceManager.getState;
|
const originalSvcGetStatus = serviceManager.getState;
|
||||||
@ -194,8 +192,6 @@ const originalReadyForUpdates = apiBinder.__get__('readyForUpdates');
|
|||||||
function setupStubs() {
|
function setupStubs() {
|
||||||
apiBinder.__set__('readyForUpdates', true);
|
apiBinder.__set__('readyForUpdates', true);
|
||||||
// @ts-expect-error Assigning to a RO property
|
// @ts-expect-error Assigning to a RO property
|
||||||
networkManager.getAllByAppId = async () => STUBBED_VALUES.networks;
|
|
||||||
// @ts-expect-error Assigning to a RO property
|
|
||||||
volumeManager.getAllByAppId = async () => STUBBED_VALUES.volumes;
|
volumeManager.getAllByAppId = async () => STUBBED_VALUES.volumes;
|
||||||
// @ts-expect-error Assigning to a RO property
|
// @ts-expect-error Assigning to a RO property
|
||||||
serviceManager.getState = async () => STUBBED_VALUES.services;
|
serviceManager.getState = async () => STUBBED_VALUES.services;
|
||||||
@ -207,8 +203,6 @@ function setupStubs() {
|
|||||||
function restoreStubs() {
|
function restoreStubs() {
|
||||||
apiBinder.__set__('readyForUpdates', originalReadyForUpdates);
|
apiBinder.__set__('readyForUpdates', originalReadyForUpdates);
|
||||||
// @ts-expect-error Assigning to a RO property
|
// @ts-expect-error Assigning to a RO property
|
||||||
networkManager.getAllByAppId = originalNetGetAll;
|
|
||||||
// @ts-expect-error Assigning to a RO property
|
|
||||||
volumeManager.getAllByAppId = originalVolGetAll;
|
volumeManager.getAllByAppId = originalVolGetAll;
|
||||||
// @ts-expect-error Assigning to a RO property
|
// @ts-expect-error Assigning to a RO property
|
||||||
serviceManager.getState = originalSvcGetStatus;
|
serviceManager.getState = originalSvcGetStatus;
|
||||||
|
@ -26,10 +26,12 @@ function createApp({
|
|||||||
volumes = [] as Volume[],
|
volumes = [] as Volume[],
|
||||||
isTarget = false,
|
isTarget = false,
|
||||||
appId = 1,
|
appId = 1,
|
||||||
|
appUuid = 'appuuid',
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return new App(
|
return new App(
|
||||||
{
|
{
|
||||||
appId,
|
appId,
|
||||||
|
appUuid,
|
||||||
services,
|
services,
|
||||||
networks: networks.reduce(
|
networks: networks.reduce(
|
||||||
(res, net) => ({ ...res, [net.name]: net }),
|
(res, net) => ({ ...res, [net.name]: net }),
|
||||||
@ -44,6 +46,7 @@ function createApp({
|
|||||||
async function createService(
|
async function createService(
|
||||||
{
|
{
|
||||||
appId = 1,
|
appId = 1,
|
||||||
|
appUuid = 'appuuid',
|
||||||
serviceName = 'test',
|
serviceName = 'test',
|
||||||
commit = 'test-commit',
|
commit = 'test-commit',
|
||||||
...conf
|
...conf
|
||||||
@ -53,6 +56,7 @@ async function createService(
|
|||||||
const svc = await Service.fromComposeObject(
|
const svc = await Service.fromComposeObject(
|
||||||
{
|
{
|
||||||
appId,
|
appId,
|
||||||
|
appUuid,
|
||||||
serviceName,
|
serviceName,
|
||||||
commit,
|
commit,
|
||||||
running: true,
|
running: true,
|
||||||
@ -71,14 +75,18 @@ async function createService(
|
|||||||
function createImage(
|
function createImage(
|
||||||
{
|
{
|
||||||
appId = 1,
|
appId = 1,
|
||||||
|
appUuid = 'appuuid',
|
||||||
dependent = 0,
|
dependent = 0,
|
||||||
name = 'test-image',
|
name = 'test-image',
|
||||||
serviceName = 'test',
|
serviceName = 'test',
|
||||||
|
commit = 'test-commit',
|
||||||
...extra
|
...extra
|
||||||
} = {} as Partial<Image>,
|
} = {} as Partial<Image>,
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
appId,
|
appId,
|
||||||
|
appUuid,
|
||||||
|
commit,
|
||||||
dependent,
|
dependent,
|
||||||
name,
|
name,
|
||||||
serviceName,
|
serviceName,
|
||||||
@ -107,7 +115,7 @@ function expectNoStep(action: CompositionStepAction, steps: CompositionStep[]) {
|
|||||||
expectSteps(action, steps, 0, 0);
|
expectSteps(action, steps, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultNetwork = Network.fromComposeObject('default', 1, {});
|
const defaultNetwork = Network.fromComposeObject('default', 1, 'appuuid', {});
|
||||||
|
|
||||||
describe('compose/app', () => {
|
describe('compose/app', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
@ -323,14 +331,12 @@ 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({
|
||||||
|
appId: 1,
|
||||||
|
appUuid: 'deadbeef',
|
||||||
image: 'test-image',
|
image: 'test-image',
|
||||||
composition: { volumes: ['db-volume:/data'] },
|
composition: { volumes: ['db-volume:/data'] },
|
||||||
});
|
});
|
||||||
const volume = Volume.fromComposeObject(
|
const volume = Volume.fromComposeObject('db-volume', 1, 'deadbeef');
|
||||||
'db-volume',
|
|
||||||
service.appId,
|
|
||||||
'deadbeef',
|
|
||||||
);
|
|
||||||
const contextWithImages = {
|
const contextWithImages = {
|
||||||
...defaultContext,
|
...defaultContext,
|
||||||
...{
|
...{
|
||||||
@ -428,7 +434,7 @@ describe('compose/app', () => {
|
|||||||
it('should correctly infer a network create step', () => {
|
it('should correctly infer a network create step', () => {
|
||||||
const current = createApp({ networks: [] });
|
const current = createApp({ networks: [] });
|
||||||
const target = createApp({
|
const target = createApp({
|
||||||
networks: [Network.fromComposeObject('default', 1, {})],
|
networks: [Network.fromComposeObject('default', 1, 'deadbeef', {})],
|
||||||
isTarget: true,
|
isTarget: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -442,7 +448,9 @@ describe('compose/app', () => {
|
|||||||
|
|
||||||
it('should correctly infer a network remove step', () => {
|
it('should correctly infer a network remove step', () => {
|
||||||
const current = createApp({
|
const current = createApp({
|
||||||
networks: [Network.fromComposeObject('test-network', 1, {})],
|
networks: [
|
||||||
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
||||||
|
],
|
||||||
isTarget: true,
|
isTarget: true,
|
||||||
});
|
});
|
||||||
const target = createApp({ networks: [], isTarget: true });
|
const target = createApp({ networks: [], isTarget: true });
|
||||||
@ -459,8 +467,8 @@ describe('compose/app', () => {
|
|||||||
it('should correctly infer more than one network removal step', () => {
|
it('should correctly infer more than one network removal step', () => {
|
||||||
const current = createApp({
|
const current = createApp({
|
||||||
networks: [
|
networks: [
|
||||||
Network.fromComposeObject('test-network', 1, {}),
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
||||||
Network.fromComposeObject('test-network-2', 1, {}),
|
Network.fromComposeObject('test-network-2', 1, 'deadbeef', {}),
|
||||||
],
|
],
|
||||||
isTarget: true,
|
isTarget: true,
|
||||||
});
|
});
|
||||||
@ -480,11 +488,13 @@ describe('compose/app', () => {
|
|||||||
|
|
||||||
it('should correctly infer a network recreation step', () => {
|
it('should correctly infer a network recreation step', () => {
|
||||||
const current = createApp({
|
const current = createApp({
|
||||||
networks: [Network.fromComposeObject('test-network', 1, {})],
|
networks: [
|
||||||
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
const target = createApp({
|
const target = createApp({
|
||||||
networks: [
|
networks: [
|
||||||
Network.fromComposeObject('test-network', 1, {
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {
|
||||||
labels: { TEST: 'TEST' },
|
labels: { TEST: 'TEST' },
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@ -524,20 +534,28 @@ describe('compose/app', () => {
|
|||||||
expect(createNetworkStep)
|
expect(createNetworkStep)
|
||||||
.to.have.property('target')
|
.to.have.property('target')
|
||||||
.that.has.property('config')
|
.that.has.property('config')
|
||||||
.that.deep.includes({ labels: { TEST: 'TEST' } });
|
.that.deep.includes({
|
||||||
|
labels: { TEST: 'TEST', 'io.balena.app-id': '1' },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should kill dependencies of networks before removing', async () => {
|
it('should kill dependencies of networks before removing', async () => {
|
||||||
const current = createApp({
|
const current = createApp({
|
||||||
|
appUuid: 'deadbeef',
|
||||||
services: [
|
services: [
|
||||||
await createService({
|
await createService({
|
||||||
composition: { networks: { 'test-network': {} } },
|
appId: 1,
|
||||||
|
appUuid: 'deadbeef',
|
||||||
|
composition: { networks: ['test-network'] },
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
networks: [Network.fromComposeObject('test-network', 1, {})],
|
networks: [
|
||||||
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
const target = createApp({
|
const target = createApp({
|
||||||
services: [await createService()],
|
appUuid: 'deadbeef',
|
||||||
|
services: [await createService({ appUuid: 'deadbeef' })],
|
||||||
networks: [],
|
networks: [],
|
||||||
isTarget: true,
|
isTarget: true,
|
||||||
});
|
});
|
||||||
@ -554,10 +572,10 @@ describe('compose/app', () => {
|
|||||||
const current = createApp({
|
const current = createApp({
|
||||||
services: [
|
services: [
|
||||||
await createService({
|
await createService({
|
||||||
composition: { networks: { 'test-network': {} } },
|
composition: { networks: ['test-network'] },
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
networks: [Network.fromComposeObject('test-network', 1, {})],
|
networks: [Network.fromComposeObject('test-network', 1, 'appuuid', {})],
|
||||||
});
|
});
|
||||||
const target = createApp({
|
const target = createApp({
|
||||||
services: [
|
services: [
|
||||||
@ -566,7 +584,7 @@ describe('compose/app', () => {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
networks: [
|
networks: [
|
||||||
Network.fromComposeObject('test-network', 1, {
|
Network.fromComposeObject('test-network', 1, 'appuuid', {
|
||||||
labels: { test: 'test' },
|
labels: { test: 'test' },
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@ -599,7 +617,7 @@ describe('compose/app', () => {
|
|||||||
|
|
||||||
it('should not create the default network if it already exists', () => {
|
it('should not create the default network if it already exists', () => {
|
||||||
const current = createApp({
|
const current = createApp({
|
||||||
networks: [Network.fromComposeObject('default', 1, {})],
|
networks: [Network.fromComposeObject('default', 1, 'deadbeef', {})],
|
||||||
});
|
});
|
||||||
const target = createApp({ networks: [], isTarget: true });
|
const target = createApp({ networks: [], isTarget: true });
|
||||||
|
|
||||||
@ -611,17 +629,17 @@ describe('compose/app', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('service state behavior', () => {
|
describe('service state behavior', () => {
|
||||||
it('should create a kill step for service which is no longer referenced', async () => {
|
it('should create a kill step for a service which is no longer referenced', async () => {
|
||||||
const current = createApp({
|
const current = createApp({
|
||||||
services: [
|
services: [
|
||||||
await createService({ appId: 1, serviceName: 'main' }),
|
await createService({ appId: 1, serviceName: 'main' }),
|
||||||
await createService({ appId: 1, serviceName: 'aux' }),
|
await createService({ appId: 1, serviceName: 'aux' }),
|
||||||
],
|
],
|
||||||
networks: [Network.fromComposeObject('test-network', 1, {})],
|
networks: [defaultNetwork],
|
||||||
});
|
});
|
||||||
const target = createApp({
|
const target = createApp({
|
||||||
services: [await createService({ appId: 1, serviceName: 'main' })],
|
services: [await createService({ appId: 1, serviceName: 'main' })],
|
||||||
networks: [Network.fromComposeObject('test-network', 1, {})],
|
networks: [defaultNetwork],
|
||||||
isTarget: true,
|
isTarget: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -827,7 +845,7 @@ describe('compose/app', () => {
|
|||||||
const intermediate = createApp({
|
const intermediate = createApp({
|
||||||
services: [],
|
services: [],
|
||||||
// Default network was already created
|
// Default network was already created
|
||||||
networks: [Network.fromComposeObject('default', 1, {})],
|
networks: [defaultNetwork],
|
||||||
});
|
});
|
||||||
|
|
||||||
// now should see a 'start'
|
// now should see a 'start'
|
||||||
@ -1189,7 +1207,10 @@ describe('compose/app', () => {
|
|||||||
appId: 1,
|
appId: 1,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
networks: [defaultNetwork, Network.fromComposeObject('test', 1, {})],
|
networks: [
|
||||||
|
defaultNetwork,
|
||||||
|
Network.fromComposeObject('test', 1, 'appuuid', {}),
|
||||||
|
],
|
||||||
isTarget: true,
|
isTarget: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -15,12 +15,12 @@ import { InstancedAppState } from '../../../src/types/state';
|
|||||||
|
|
||||||
import * as dbHelper from '../../lib/db-helper';
|
import * as dbHelper from '../../lib/db-helper';
|
||||||
|
|
||||||
const DEFAULT_NETWORK = Network.fromComposeObject('default', 1, {});
|
const DEFAULT_NETWORK = Network.fromComposeObject('default', 1, 'appuuid', {});
|
||||||
|
|
||||||
async function createService(
|
async function createService(
|
||||||
{
|
{
|
||||||
appId = 1,
|
appId = 1,
|
||||||
appUuid = 'app-uuid',
|
appUuid = 'appuuid',
|
||||||
serviceName = 'main',
|
serviceName = 'main',
|
||||||
commit = 'main-commit',
|
commit = 'main-commit',
|
||||||
...conf
|
...conf
|
||||||
@ -54,7 +54,7 @@ async function createService(
|
|||||||
function createImage(
|
function createImage(
|
||||||
{
|
{
|
||||||
appId = 1,
|
appId = 1,
|
||||||
appUuid = 'app-uuid',
|
appUuid = 'appuuid',
|
||||||
name = 'test-image',
|
name = 'test-image',
|
||||||
serviceName = 'main',
|
serviceName = 'main',
|
||||||
commit = 'main-commit',
|
commit = 'main-commit',
|
||||||
@ -582,7 +582,7 @@ describe('compose/application-manager', () => {
|
|||||||
await createService({
|
await createService({
|
||||||
image: 'main-image',
|
image: 'main-image',
|
||||||
appId: 1,
|
appId: 1,
|
||||||
appUuid: 'app-uuid',
|
appUuid: 'appuuid',
|
||||||
commit: 'new-release',
|
commit: 'new-release',
|
||||||
serviceName: 'main',
|
serviceName: 'main',
|
||||||
composition: {
|
composition: {
|
||||||
@ -592,7 +592,7 @@ describe('compose/application-manager', () => {
|
|||||||
await createService({
|
await createService({
|
||||||
image: 'dep-image',
|
image: 'dep-image',
|
||||||
appId: 1,
|
appId: 1,
|
||||||
appUuid: 'app-uuid',
|
appUuid: 'appuuid',
|
||||||
commit: 'new-release',
|
commit: 'new-release',
|
||||||
serviceName: 'dep',
|
serviceName: 'dep',
|
||||||
}),
|
}),
|
||||||
@ -611,7 +611,7 @@ describe('compose/application-manager', () => {
|
|||||||
services: [
|
services: [
|
||||||
await createService({
|
await createService({
|
||||||
appId: 1,
|
appId: 1,
|
||||||
appUuid: 'app-uuid',
|
appUuid: 'appuuid',
|
||||||
commit: 'old-release',
|
commit: 'old-release',
|
||||||
serviceName: 'main',
|
serviceName: 'main',
|
||||||
composition: {
|
composition: {
|
||||||
@ -620,7 +620,7 @@ describe('compose/application-manager', () => {
|
|||||||
}),
|
}),
|
||||||
await createService({
|
await createService({
|
||||||
appId: 1,
|
appId: 1,
|
||||||
appUuid: 'app-uuid',
|
appUuid: 'appuuid',
|
||||||
commit: 'old-release',
|
commit: 'old-release',
|
||||||
serviceName: 'dep',
|
serviceName: 'dep',
|
||||||
}),
|
}),
|
||||||
@ -630,14 +630,14 @@ describe('compose/application-manager', () => {
|
|||||||
// Both images have been downloaded
|
// Both images have been downloaded
|
||||||
createImage({
|
createImage({
|
||||||
appId: 1,
|
appId: 1,
|
||||||
appUuid: 'app-uuid',
|
appUuid: 'appuuid',
|
||||||
name: 'main-image',
|
name: 'main-image',
|
||||||
serviceName: 'main',
|
serviceName: 'main',
|
||||||
commit: 'new-release',
|
commit: 'new-release',
|
||||||
}),
|
}),
|
||||||
createImage({
|
createImage({
|
||||||
appId: 1,
|
appId: 1,
|
||||||
appUuid: 'app-uuid',
|
appUuid: 'appuuid',
|
||||||
name: 'dep-image',
|
name: 'dep-image',
|
||||||
serviceName: 'dep',
|
serviceName: 'dep',
|
||||||
commit: 'new-release',
|
commit: 'new-release',
|
||||||
@ -1202,8 +1202,8 @@ describe('compose/application-manager', () => {
|
|||||||
],
|
],
|
||||||
networks: [
|
networks: [
|
||||||
// Default networks for two apps
|
// Default networks for two apps
|
||||||
Network.fromComposeObject('default', 1, {}),
|
Network.fromComposeObject('default', 1, 'app-one', {}),
|
||||||
Network.fromComposeObject('default', 2, {}),
|
Network.fromComposeObject('default', 2, 'app-two', {}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
@ -1217,8 +1217,8 @@ describe('compose/application-manager', () => {
|
|||||||
services: [],
|
services: [],
|
||||||
networks: [
|
networks: [
|
||||||
// Default networks for two apps
|
// Default networks for two apps
|
||||||
Network.fromComposeObject('default', 1, {}),
|
Network.fromComposeObject('default', 1, 'app-one', {}),
|
||||||
Network.fromComposeObject('default', 2, {}),
|
Network.fromComposeObject('default', 2, 'app-two', {}),
|
||||||
],
|
],
|
||||||
images: [
|
images: [
|
||||||
createImage({
|
createImage({
|
||||||
|
@ -10,10 +10,16 @@ import { log } from '../../../src/lib/supervisor-console';
|
|||||||
describe('compose/network', () => {
|
describe('compose/network', () => {
|
||||||
describe('creating a network from a compose object', () => {
|
describe('creating a network from a compose object', () => {
|
||||||
it('creates a default network configuration if no config is given', () => {
|
it('creates a default network configuration if no config is given', () => {
|
||||||
const network = Network.fromComposeObject('default', 12345, {});
|
const network = Network.fromComposeObject(
|
||||||
|
'default',
|
||||||
|
12345,
|
||||||
|
'deadbeef',
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
expect(network.name).to.equal('default');
|
expect(network.name).to.equal('default');
|
||||||
expect(network.appId).to.equal(12345);
|
expect(network.appId).to.equal(12345);
|
||||||
|
expect(network.appUuid).to.equal('deadbeef');
|
||||||
|
|
||||||
// Default configuration options
|
// Default configuration options
|
||||||
expect(network.config.driver).to.equal('bridge');
|
expect(network.config.driver).to.equal('bridge');
|
||||||
@ -23,12 +29,14 @@ describe('compose/network', () => {
|
|||||||
options: {},
|
options: {},
|
||||||
});
|
});
|
||||||
expect(network.config.enableIPv6).to.equal(false);
|
expect(network.config.enableIPv6).to.equal(false);
|
||||||
expect(network.config.labels).to.deep.equal({});
|
expect(network.config.labels).to.deep.equal({
|
||||||
|
'io.balena.app-id': '12345',
|
||||||
|
});
|
||||||
expect(network.config.options).to.deep.equal({});
|
expect(network.config.options).to.deep.equal({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('normalizes legacy labels', () => {
|
it('normalizes legacy labels', () => {
|
||||||
const network = Network.fromComposeObject('default', 12345, {
|
const network = Network.fromComposeObject('default', 12345, 'deadbeef', {
|
||||||
labels: {
|
labels: {
|
||||||
'io.resin.features.something': '1234',
|
'io.resin.features.something': '1234',
|
||||||
},
|
},
|
||||||
@ -36,11 +44,12 @@ describe('compose/network', () => {
|
|||||||
|
|
||||||
expect(network.config.labels).to.deep.equal({
|
expect(network.config.labels).to.deep.equal({
|
||||||
'io.balena.features.something': '1234',
|
'io.balena.features.something': '1234',
|
||||||
|
'io.balena.app-id': '12345',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts valid IPAM configurations', () => {
|
it('accepts valid IPAM configurations', () => {
|
||||||
const network0 = Network.fromComposeObject('default', 12345, {
|
const network0 = Network.fromComposeObject('default', 12345, 'deadbeef', {
|
||||||
ipam: { driver: 'dummy', config: [], options: {} },
|
ipam: { driver: 'dummy', config: [], options: {} },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -51,7 +60,7 @@ describe('compose/network', () => {
|
|||||||
options: {},
|
options: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const network1 = Network.fromComposeObject('default', 12345, {
|
const network1 = Network.fromComposeObject('default', 12345, 'deadbeef', {
|
||||||
ipam: {
|
ipam: {
|
||||||
driver: 'default',
|
driver: 'default',
|
||||||
config: [
|
config: [
|
||||||
@ -84,7 +93,7 @@ describe('compose/network', () => {
|
|||||||
it('warns about IPAM configuration without both gateway and subnet', () => {
|
it('warns about IPAM configuration without both gateway and subnet', () => {
|
||||||
const logSpy = sinon.spy(log, 'warn');
|
const logSpy = sinon.spy(log, 'warn');
|
||||||
|
|
||||||
Network.fromComposeObject('default', 12345, {
|
Network.fromComposeObject('default', 12345, 'deadbeef', {
|
||||||
ipam: {
|
ipam: {
|
||||||
driver: 'default',
|
driver: 'default',
|
||||||
config: [
|
config: [
|
||||||
@ -103,7 +112,7 @@ describe('compose/network', () => {
|
|||||||
|
|
||||||
logSpy.resetHistory();
|
logSpy.resetHistory();
|
||||||
|
|
||||||
Network.fromComposeObject('default', 12345, {
|
Network.fromComposeObject('default', 12345, 'deadbeef', {
|
||||||
ipam: {
|
ipam: {
|
||||||
driver: 'default',
|
driver: 'default',
|
||||||
config: [
|
config: [
|
||||||
@ -124,7 +133,7 @@ describe('compose/network', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('parses values from a compose object', () => {
|
it('parses values from a compose object', () => {
|
||||||
const network1 = Network.fromComposeObject('default', 12345, {
|
const network1 = Network.fromComposeObject('default', 12345, 'deadbeef', {
|
||||||
driver: 'bridge',
|
driver: 'bridge',
|
||||||
enable_ipv6: true,
|
enable_ipv6: true,
|
||||||
internal: false,
|
internal: false,
|
||||||
@ -171,6 +180,7 @@ describe('compose/network', () => {
|
|||||||
|
|
||||||
expect(dockerConfig.Labels).to.deep.equal({
|
expect(dockerConfig.Labels).to.deep.equal({
|
||||||
'io.balena.supervised': 'true',
|
'io.balena.supervised': 'true',
|
||||||
|
'io.balena.app-id': '12345',
|
||||||
'com.docker.some-label': 'yes',
|
'com.docker.some-label': 'yes',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -209,9 +219,17 @@ describe('compose/network', () => {
|
|||||||
Name: '1234',
|
Name: '1234',
|
||||||
} as NetworkInspectInfo),
|
} as NetworkInspectInfo),
|
||||||
).to.throw();
|
).to.throw();
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
Network.fromDockerNetwork({
|
||||||
|
Id: 'deadbeef',
|
||||||
|
Name: 'a173bdb734884b778f5cc3dffd18733e_default',
|
||||||
|
Labels: {}, // no app-id
|
||||||
|
} as NetworkInspectInfo),
|
||||||
|
).to.throw();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a network object from a docker network configuration', () => {
|
it('creates a network object from a legacy docker network configuration', () => {
|
||||||
const network = Network.fromDockerNetwork({
|
const network = Network.fromDockerNetwork({
|
||||||
Id: 'deadbeef',
|
Id: 'deadbeef',
|
||||||
Name: '1234_default',
|
Name: '1234_default',
|
||||||
@ -233,6 +251,7 @@ describe('compose/network', () => {
|
|||||||
'com.docker.some-option': 'abcd',
|
'com.docker.some-option': 'abcd',
|
||||||
} as NetworkInspectInfo['Options'],
|
} as NetworkInspectInfo['Options'],
|
||||||
Labels: {
|
Labels: {
|
||||||
|
'io.balena.supervised': 'true',
|
||||||
'io.balena.features.something': '123',
|
'io.balena.features.something': '123',
|
||||||
} as NetworkInspectInfo['Labels'],
|
} as NetworkInspectInfo['Labels'],
|
||||||
} as NetworkInspectInfo);
|
} as NetworkInspectInfo);
|
||||||
@ -257,6 +276,56 @@ describe('compose/network', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('creates a network object from a docker network configuration', () => {
|
||||||
|
const network = Network.fromDockerNetwork({
|
||||||
|
Id: 'deadbeef',
|
||||||
|
Name: 'a173bdb734884b778f5cc3dffd18733e_default',
|
||||||
|
Driver: 'bridge',
|
||||||
|
EnableIPv6: true,
|
||||||
|
IPAM: {
|
||||||
|
Driver: 'default',
|
||||||
|
Options: {},
|
||||||
|
Config: [
|
||||||
|
{
|
||||||
|
Subnet: '172.18.0.0/16',
|
||||||
|
Gateway: '172.18.0.1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as NetworkInspectInfo['IPAM'],
|
||||||
|
Internal: true,
|
||||||
|
Containers: {},
|
||||||
|
Options: {
|
||||||
|
'com.docker.some-option': 'abcd',
|
||||||
|
} as NetworkInspectInfo['Options'],
|
||||||
|
Labels: {
|
||||||
|
'io.balena.supervised': 'true',
|
||||||
|
'io.balena.features.something': '123',
|
||||||
|
'io.balena.app-id': '1234',
|
||||||
|
} as NetworkInspectInfo['Labels'],
|
||||||
|
} as NetworkInspectInfo);
|
||||||
|
|
||||||
|
expect(network.appId).to.equal(1234);
|
||||||
|
expect(network.appUuid).to.equal('a173bdb734884b778f5cc3dffd18733e');
|
||||||
|
expect(network.name).to.equal('default');
|
||||||
|
expect(network.config.enableIPv6).to.equal(true);
|
||||||
|
expect(network.config.ipam.driver).to.equal('default');
|
||||||
|
expect(network.config.ipam.options).to.deep.equal({});
|
||||||
|
expect(network.config.ipam.config).to.deep.equal([
|
||||||
|
{
|
||||||
|
subnet: '172.18.0.0/16',
|
||||||
|
gateway: '172.18.0.1',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(network.config.internal).to.equal(true);
|
||||||
|
expect(network.config.options).to.deep.equal({
|
||||||
|
'com.docker.some-option': 'abcd',
|
||||||
|
});
|
||||||
|
expect(network.config.labels).to.deep.equal({
|
||||||
|
'io.balena.features.something': '123',
|
||||||
|
'io.balena.app-id': '1234',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('normalizes legacy label names and excludes supervised label', () => {
|
it('normalizes legacy label names and excludes supervised label', () => {
|
||||||
const network = Network.fromDockerNetwork({
|
const network = Network.fromDockerNetwork({
|
||||||
Id: 'deadbeef',
|
Id: 'deadbeef',
|
||||||
@ -284,7 +353,7 @@ describe('compose/network', () => {
|
|||||||
it('creates a docker compose network object from the internal network config', () => {
|
it('creates a docker compose network object from the internal network config', () => {
|
||||||
const network = Network.fromDockerNetwork({
|
const network = Network.fromDockerNetwork({
|
||||||
Id: 'deadbeef',
|
Id: 'deadbeef',
|
||||||
Name: '1234_default',
|
Name: 'a173bdb734884b778f5cc3dffd18733e_default',
|
||||||
Driver: 'bridge',
|
Driver: 'bridge',
|
||||||
EnableIPv6: true,
|
EnableIPv6: true,
|
||||||
IPAM: {
|
IPAM: {
|
||||||
@ -304,9 +373,13 @@ describe('compose/network', () => {
|
|||||||
} as NetworkInspectInfo['Options'],
|
} as NetworkInspectInfo['Options'],
|
||||||
Labels: {
|
Labels: {
|
||||||
'io.balena.features.something': '123',
|
'io.balena.features.something': '123',
|
||||||
|
'io.balena.app-id': '12345',
|
||||||
} as NetworkInspectInfo['Labels'],
|
} as NetworkInspectInfo['Labels'],
|
||||||
} as NetworkInspectInfo);
|
} as NetworkInspectInfo);
|
||||||
|
|
||||||
|
expect(network.appId).to.equal(12345);
|
||||||
|
expect(network.appUuid).to.equal('a173bdb734884b778f5cc3dffd18733e');
|
||||||
|
|
||||||
// Convert to compose object
|
// Convert to compose object
|
||||||
const compose = network.toComposeObject();
|
const compose = network.toComposeObject();
|
||||||
expect(compose.driver).to.equal('bridge');
|
expect(compose.driver).to.equal('bridge');
|
||||||
@ -327,23 +400,26 @@ describe('compose/network', () => {
|
|||||||
});
|
});
|
||||||
expect(compose.labels).to.deep.equal({
|
expect(compose.labels).to.deep.equal({
|
||||||
'io.balena.features.something': '123',
|
'io.balena.features.something': '123',
|
||||||
|
'io.balena.app-id': '12345',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('generateDockerName', () => {
|
describe('generateDockerName', () => {
|
||||||
it('creates a proper network name from the user given name and the app id', () => {
|
it('creates a proper network name from the user given name and the app uuid', () => {
|
||||||
expect(Network.generateDockerName(12345, 'default')).to.equal(
|
expect(Network.generateDockerName('deadbeef', 'default')).to.equal(
|
||||||
'12345_default',
|
'deadbeef_default',
|
||||||
|
);
|
||||||
|
expect(Network.generateDockerName('deadbeef', 'bleh')).to.equal(
|
||||||
|
'deadbeef_bleh',
|
||||||
);
|
);
|
||||||
expect(Network.generateDockerName(12345, 'bleh')).to.equal('12345_bleh');
|
|
||||||
expect(Network.generateDockerName(1, 'default')).to.equal('1_default');
|
expect(Network.generateDockerName(1, 'default')).to.equal('1_default');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('comparing network configurations', () => {
|
describe('comparing network configurations', () => {
|
||||||
it('ignores IPAM configuration', () => {
|
it('ignores IPAM configuration', () => {
|
||||||
const network = Network.fromComposeObject('default', 12345, {
|
const network = Network.fromComposeObject('default', 12345, 'deadbeef', {
|
||||||
ipam: {
|
ipam: {
|
||||||
driver: 'default',
|
driver: 'default',
|
||||||
config: [
|
config: [
|
||||||
@ -357,13 +433,15 @@ describe('compose/network', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
network.isEqualConfig(Network.fromComposeObject('default', 12345, {})),
|
network.isEqualConfig(
|
||||||
|
Network.fromComposeObject('default', 12345, 'deadbeef', {}),
|
||||||
|
),
|
||||||
).to.be.true;
|
).to.be.true;
|
||||||
|
|
||||||
// Only ignores ipam.config, not other ipam elements
|
// Only ignores ipam.config, not other ipam elements
|
||||||
expect(
|
expect(
|
||||||
network.isEqualConfig(
|
network.isEqualConfig(
|
||||||
Network.fromComposeObject('default', 12345, {
|
Network.fromComposeObject('default', 12345, 'deadbeef', {
|
||||||
ipam: { driver: 'aaa' },
|
ipam: { driver: 'aaa' },
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@ -372,26 +450,61 @@ describe('compose/network', () => {
|
|||||||
|
|
||||||
it('compares configurations recursively', () => {
|
it('compares configurations recursively', () => {
|
||||||
expect(
|
expect(
|
||||||
Network.fromComposeObject('default', 12345, {}).isEqualConfig(
|
Network.fromComposeObject(
|
||||||
Network.fromComposeObject('default', 12345, {}),
|
'default',
|
||||||
|
12345,
|
||||||
|
'deadbeef',
|
||||||
|
{},
|
||||||
|
).isEqualConfig(
|
||||||
|
Network.fromComposeObject('default', 12345, 'deadbeef', {}),
|
||||||
),
|
),
|
||||||
).to.be.true;
|
).to.be.true;
|
||||||
expect(
|
expect(
|
||||||
Network.fromComposeObject('default', 12345, {
|
Network.fromComposeObject('default', 12345, 'deadbeef', {
|
||||||
driver: 'default',
|
driver: 'default',
|
||||||
}).isEqualConfig(Network.fromComposeObject('default', 12345, {})),
|
}).isEqualConfig(
|
||||||
|
Network.fromComposeObject('default', 12345, 'deadbeef', {}),
|
||||||
|
),
|
||||||
).to.be.false;
|
).to.be.false;
|
||||||
expect(
|
expect(
|
||||||
Network.fromComposeObject('default', 12345, {
|
Network.fromComposeObject('default', 12345, 'deadbeef', {
|
||||||
enable_ipv6: true,
|
enable_ipv6: true,
|
||||||
}).isEqualConfig(Network.fromComposeObject('default', 12345, {})),
|
}).isEqualConfig(
|
||||||
|
Network.fromComposeObject('default', 12345, 'deadbeef', {}),
|
||||||
|
),
|
||||||
).to.be.false;
|
).to.be.false;
|
||||||
expect(
|
expect(
|
||||||
Network.fromComposeObject('default', 12345, {
|
Network.fromComposeObject('default', 12345, 'deadbeef', {
|
||||||
enable_ipv6: false,
|
enable_ipv6: false,
|
||||||
internal: false,
|
internal: false,
|
||||||
}).isEqualConfig(
|
}).isEqualConfig(
|
||||||
Network.fromComposeObject('default', 12345, { internal: true }),
|
Network.fromComposeObject('default', 12345, 'deadbeef', {
|
||||||
|
internal: true,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).to.be.false;
|
||||||
|
|
||||||
|
// Comparison of a network without the app-uuid and a network
|
||||||
|
// with uuid has to return false
|
||||||
|
expect(
|
||||||
|
Network.fromComposeObject(
|
||||||
|
'default',
|
||||||
|
12345,
|
||||||
|
'deadbeef',
|
||||||
|
{},
|
||||||
|
).isEqualConfig(
|
||||||
|
Network.fromDockerNetwork({
|
||||||
|
Id: 'deadbeef',
|
||||||
|
Name: '12345_default',
|
||||||
|
IPAM: {
|
||||||
|
Driver: 'default',
|
||||||
|
Options: {},
|
||||||
|
Config: [],
|
||||||
|
} as NetworkInspectInfo['IPAM'],
|
||||||
|
Labels: {
|
||||||
|
'io.balena.supervised': 'true',
|
||||||
|
} as NetworkInspectInfo['Labels'],
|
||||||
|
} as NetworkInspectInfo),
|
||||||
),
|
),
|
||||||
).to.be.false;
|
).to.be.false;
|
||||||
});
|
});
|
||||||
@ -400,7 +513,11 @@ describe('compose/network', () => {
|
|||||||
describe('creating networks', () => {
|
describe('creating networks', () => {
|
||||||
it('creates a new network on the engine with the given data', async () => {
|
it('creates a new network on the engine with the given data', async () => {
|
||||||
await withMockerode(async (mockerode) => {
|
await withMockerode(async (mockerode) => {
|
||||||
const network = Network.fromComposeObject('default', 12345, {
|
const network = Network.fromComposeObject(
|
||||||
|
'default',
|
||||||
|
12345,
|
||||||
|
'deadbeef',
|
||||||
|
{
|
||||||
ipam: {
|
ipam: {
|
||||||
driver: 'default',
|
driver: 'default',
|
||||||
config: [
|
config: [
|
||||||
@ -412,14 +529,15 @@ describe('compose/network', () => {
|
|||||||
],
|
],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Create the network
|
// Create the network
|
||||||
await network.create();
|
await network.create();
|
||||||
|
|
||||||
// Check that the create function was called with proper arguments
|
// Check that the create function was called with proper arguments
|
||||||
expect(mockerode.createNetwork).to.have.been.calledOnceWith({
|
expect(mockerode.createNetwork).to.have.been.calledOnceWith({
|
||||||
Name: '12345_default',
|
Name: 'deadbeef_default',
|
||||||
Driver: 'bridge',
|
Driver: 'bridge',
|
||||||
CheckDuplicate: true,
|
CheckDuplicate: true,
|
||||||
IPAM: {
|
IPAM: {
|
||||||
@ -437,6 +555,7 @@ describe('compose/network', () => {
|
|||||||
Internal: false,
|
Internal: false,
|
||||||
Labels: {
|
Labels: {
|
||||||
'io.balena.supervised': 'true',
|
'io.balena.supervised': 'true',
|
||||||
|
'io.balena.app-id': '12345',
|
||||||
},
|
},
|
||||||
Options: {},
|
Options: {},
|
||||||
});
|
});
|
||||||
@ -445,7 +564,11 @@ describe('compose/network', () => {
|
|||||||
|
|
||||||
it('throws the error if there is a problem while creating the network', async () => {
|
it('throws the error if there is a problem while creating the network', async () => {
|
||||||
await withMockerode(async (mockerode) => {
|
await withMockerode(async (mockerode) => {
|
||||||
const network = Network.fromComposeObject('default', 12345, {
|
const network = Network.fromComposeObject(
|
||||||
|
'default',
|
||||||
|
12345,
|
||||||
|
'deadbeef',
|
||||||
|
{
|
||||||
ipam: {
|
ipam: {
|
||||||
driver: 'default',
|
driver: 'default',
|
||||||
config: [
|
config: [
|
||||||
@ -457,7 +580,8 @@ describe('compose/network', () => {
|
|||||||
],
|
],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Re-define the dockerode.createNetwork to throw
|
// Re-define the dockerode.createNetwork to throw
|
||||||
mockerode.createNetwork.rejects('Unknown engine error');
|
mockerode.createNetwork.rejects('Unknown engine error');
|
||||||
@ -471,10 +595,10 @@ describe('compose/network', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('removing a network', () => {
|
describe('removing a network', () => {
|
||||||
it('removes the network from the engine if it exists', async () => {
|
it('removes the legacy network from the engine if it exists', async () => {
|
||||||
// Create a mock network to add to the mock engine
|
// Create a mock network to add to the mock engine
|
||||||
const dockerNetwork = createNetwork({
|
const dockerNetwork = createNetwork({
|
||||||
Id: 'deadbeef',
|
Id: 'aaaaaaa',
|
||||||
Name: '12345_default',
|
Name: '12345_default',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -484,7 +608,48 @@ describe('compose/network', () => {
|
|||||||
expect(await mockerode.listNetworks()).to.have.lengthOf(1);
|
expect(await mockerode.listNetworks()).to.have.lengthOf(1);
|
||||||
|
|
||||||
// Create a dummy network object
|
// Create a dummy network object
|
||||||
const network = Network.fromComposeObject('default', 12345, {});
|
const network = Network.fromComposeObject(
|
||||||
|
'default',
|
||||||
|
12345,
|
||||||
|
'deadbeef',
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Perform the operation
|
||||||
|
await network.remove();
|
||||||
|
|
||||||
|
// The removal step should delete the object from the engine data
|
||||||
|
expect(mockerode.removeNetwork).to.have.been.calledOnceWith(
|
||||||
|
'aaaaaaa',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ networks: [dockerNetwork] },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes the network from the engine if it exists', async () => {
|
||||||
|
// Create a mock network to add to the mock engine
|
||||||
|
const dockerNetwork = createNetwork({
|
||||||
|
Id: 'deadbeef',
|
||||||
|
Name: 'a173bdb734884b778f5cc3dffd18733e_default',
|
||||||
|
Labels: {
|
||||||
|
'io.balena.supervised': 'true',
|
||||||
|
'io.balena.app-id': '12345',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await withMockerode(
|
||||||
|
async (mockerode) => {
|
||||||
|
// Check that the engine has the network
|
||||||
|
expect(await mockerode.listNetworks()).to.have.lengthOf(1);
|
||||||
|
|
||||||
|
// Create a dummy network object
|
||||||
|
const network = Network.fromComposeObject(
|
||||||
|
'default',
|
||||||
|
12345,
|
||||||
|
'a173bdb734884b778f5cc3dffd18733e',
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
// Perform the operation
|
// Perform the operation
|
||||||
await network.remove();
|
await network.remove();
|
||||||
@ -501,7 +666,7 @@ describe('compose/network', () => {
|
|||||||
it('ignores the request if the given network does not exist on the engine', async () => {
|
it('ignores the request if the given network does not exist on the engine', async () => {
|
||||||
// Create a mock network to add to the mock engine
|
// Create a mock network to add to the mock engine
|
||||||
const mockNetwork = createNetwork({
|
const mockNetwork = createNetwork({
|
||||||
Id: 'deadbeef',
|
Id: 'aaaaaaaa',
|
||||||
Name: 'some_network',
|
Name: 'some_network',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -511,7 +676,12 @@ describe('compose/network', () => {
|
|||||||
expect(await mockerode.listNetworks()).to.have.lengthOf(1);
|
expect(await mockerode.listNetworks()).to.have.lengthOf(1);
|
||||||
|
|
||||||
// Create a dummy network object
|
// Create a dummy network object
|
||||||
const network = Network.fromComposeObject('default', 12345, {});
|
const network = Network.fromComposeObject(
|
||||||
|
'default',
|
||||||
|
12345,
|
||||||
|
'deadbeef',
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
// This should not fail
|
// This should not fail
|
||||||
await expect(network.remove()).to.not.be.rejected;
|
await expect(network.remove()).to.not.be.rejected;
|
||||||
@ -526,18 +696,29 @@ describe('compose/network', () => {
|
|||||||
it('throws the error if there is a problem while removing the network', async () => {
|
it('throws the error if there is a problem while removing the network', async () => {
|
||||||
// Create a mock network to add to the mock engine
|
// Create a mock network to add to the mock engine
|
||||||
const mockNetwork = createNetwork({
|
const mockNetwork = createNetwork({
|
||||||
Id: 'deadbeef',
|
Id: 'aaaaaaaa',
|
||||||
Name: '12345_default',
|
Name: 'a173bdb734884b778f5cc3dffd18733e_default',
|
||||||
|
Labels: {
|
||||||
|
'io.balena.app-id': '12345',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await withMockerode(
|
await withMockerode(
|
||||||
async (mockerode) => {
|
async (mockerode) => {
|
||||||
// We can change the return value of the mockerode removeNetwork
|
// We can change the return value of the mockerode removeNetwork
|
||||||
// to have the remove operation fail
|
// to have the remove operation fail
|
||||||
mockerode.removeNetwork.throws('Failed to remove the network');
|
mockerode.removeNetwork.throws({
|
||||||
|
statusCode: 500,
|
||||||
|
message: 'Failed to remove the network',
|
||||||
|
});
|
||||||
|
|
||||||
// Create a dummy network object
|
// Create a dummy network object
|
||||||
const network = Network.fromComposeObject('default', 12345, {});
|
const network = Network.fromComposeObject(
|
||||||
|
'default',
|
||||||
|
12345,
|
||||||
|
'a173bdb734884b778f5cc3dffd18733e',
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
await expect(network.remove()).to.be.rejected;
|
await expect(network.remove()).to.be.rejected;
|
||||||
},
|
},
|
||||||
|
@ -429,6 +429,7 @@ describe('compose/service', () => {
|
|||||||
await Service.fromComposeObject(
|
await Service.fromComposeObject(
|
||||||
{
|
{
|
||||||
appId: 123456,
|
appId: 123456,
|
||||||
|
appUuid: 'deadbeef',
|
||||||
serviceId: 123456,
|
serviceId: 123456,
|
||||||
serviceName: 'test',
|
serviceName: 'test',
|
||||||
composition: {
|
composition: {
|
||||||
@ -448,7 +449,7 @@ describe('compose/service', () => {
|
|||||||
).toDockerContainer({ deviceName: 'foo' } as any).NetworkingConfig,
|
).toDockerContainer({ deviceName: 'foo' } as any).NetworkingConfig,
|
||||||
).to.deep.equal({
|
).to.deep.equal({
|
||||||
EndpointsConfig: {
|
EndpointsConfig: {
|
||||||
'123456_balena': {
|
deadbeef_balena: {
|
||||||
IPAMConfig: {
|
IPAMConfig: {
|
||||||
IPv4Address: '1.2.3.4',
|
IPv4Address: '1.2.3.4',
|
||||||
},
|
},
|
||||||
@ -470,7 +471,7 @@ describe('compose/service', () => {
|
|||||||
).toDockerContainer({ deviceName: 'foo' } as any).NetworkingConfig,
|
).toDockerContainer({ deviceName: 'foo' } as any).NetworkingConfig,
|
||||||
).to.deep.equal({
|
).to.deep.equal({
|
||||||
EndpointsConfig: {
|
EndpointsConfig: {
|
||||||
'123456_balena': {
|
deadbeef_balena: {
|
||||||
IPAMConfig: {
|
IPAMConfig: {
|
||||||
IPv4Address: '1.2.3.4',
|
IPv4Address: '1.2.3.4',
|
||||||
IPv6Address: '5.6.7.8',
|
IPv6Address: '5.6.7.8',
|
||||||
|
Reference in New Issue
Block a user