mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-20 17:52:51 +00:00
Compose: Support more network creation options
Change-type: minor Signed-off-by: Cameron Diver <cameron@resin.io>
This commit is contained in:
parent
a7551abe93
commit
e0231f15e9
@ -399,7 +399,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
opts,
|
||||
name,
|
||||
appId,
|
||||
target[name]
|
||||
target[name].config
|
||||
)
|
||||
return !currentNet.isEqualConfig(targetNet)
|
||||
else
|
||||
|
24
src/compose/errors.ts
Normal file
24
src/compose/errors.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// This module is for compose specific errors, but compose modules
|
||||
// will still use errors from the global ./lib/errors.ts
|
||||
|
||||
import TypedError = require('typed-error');
|
||||
|
||||
export class InvalidNetworkNameError extends TypedError {
|
||||
public constructor(public name: string) {
|
||||
super(`Invalid network name: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class ResourceRecreationAttemptError extends TypedError {
|
||||
public constructor(
|
||||
public resource: string,
|
||||
public name: string,
|
||||
) {
|
||||
super(
|
||||
`Trying to create ${resource} with name: ${name}, but a ${resource} `+
|
||||
'with that name and a different configuration already exists',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidNetworkConfigurationError extends TypedError { }
|
@ -4,12 +4,12 @@ import { fs } from 'mz';
|
||||
import * as constants from '../lib/constants';
|
||||
import Docker = require('../lib/docker-utils');
|
||||
import { ENOENT, NotFoundError } from '../lib/errors';
|
||||
import { Logger } from '../logger';
|
||||
import { Network, NetworkOptions } from './network';
|
||||
|
||||
export class NetworkManager {
|
||||
private docker: Docker;
|
||||
// FIXME: Type this
|
||||
private logger: any;
|
||||
private logger: Logger;
|
||||
|
||||
constructor(opts: NetworkOptions) {
|
||||
this.docker = opts.docker;
|
||||
@ -25,7 +25,7 @@ export class NetworkManager {
|
||||
.map((network: { Name: string }) => {
|
||||
return this.docker.getNetwork(network.Name).inspect()
|
||||
.then((net) => {
|
||||
return Network.fromDockerodeNetwork({
|
||||
return Network.fromDockerNetwork({
|
||||
docker: this.docker,
|
||||
logger: this.logger,
|
||||
}, net);
|
||||
@ -39,7 +39,7 @@ export class NetworkManager {
|
||||
}
|
||||
|
||||
public get(network: { name: string, appId: number }): Bluebird<Network> {
|
||||
return Network.fromAppIdAndName({
|
||||
return Network.fromNameAndAppId({
|
||||
logger: this.logger,
|
||||
docker: this.docker,
|
||||
}, network.name, network.appId);
|
||||
@ -54,9 +54,8 @@ export class NetworkManager {
|
||||
return network.Options['com.docker.network.bridge.name'] ===
|
||||
constants.supervisorNetworkInterface;
|
||||
})
|
||||
// FIXME: Types seem to be wrong here?
|
||||
.catchReturn(NotFoundError as any, false)
|
||||
.catchReturn(ENOENT as any, false);
|
||||
.catchReturn(NotFoundError, false)
|
||||
.catchReturn(ENOENT, false);
|
||||
}
|
||||
|
||||
public ensureSupervisorNetwork(): Bluebird<void> {
|
||||
|
@ -1,28 +1,31 @@
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import Docker = require('../lib/docker-utils');
|
||||
import { NotFoundError } from '../lib/errors';
|
||||
import {
|
||||
InvalidAppIdError,
|
||||
NotFoundError,
|
||||
} from '../lib/errors';
|
||||
import logTypes = require('../lib/log-types');
|
||||
import { checkInt } from '../lib/validation';
|
||||
import { Logger } from '../logger';
|
||||
|
||||
import {
|
||||
DockerIPAMConfig,
|
||||
DockerNetworkConfig,
|
||||
NetworkConfig,
|
||||
NetworkInspect,
|
||||
} from './types/network';
|
||||
|
||||
import {
|
||||
InvalidNetworkConfigurationError,
|
||||
InvalidNetworkNameError,
|
||||
ResourceRecreationAttemptError,
|
||||
} from './errors';
|
||||
|
||||
export interface NetworkOptions {
|
||||
docker: Docker;
|
||||
// TODO: Once the new logger is implemented and merged, type it
|
||||
// and use that type here
|
||||
logger: any;
|
||||
}
|
||||
|
||||
export interface NetworkConfig {
|
||||
}
|
||||
|
||||
// It appears the dockerode typings are incomplete,
|
||||
// extend here for now.
|
||||
interface NetworkInspect {
|
||||
Name: string;
|
||||
}
|
||||
|
||||
export interface ComposeNetwork {
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export class Network {
|
||||
@ -32,8 +35,7 @@ export class Network {
|
||||
public config: NetworkConfig;
|
||||
|
||||
private docker: Docker;
|
||||
// FIXME: Type this
|
||||
private logger: any;
|
||||
private logger: Logger;
|
||||
private networkOpts: NetworkOptions;
|
||||
|
||||
private constructor(opts: NetworkOptions) {
|
||||
@ -42,48 +44,87 @@ export class Network {
|
||||
this.networkOpts = opts;
|
||||
}
|
||||
|
||||
public static fromDockerodeNetwork(
|
||||
public static fromDockerNetwork(
|
||||
opts: NetworkOptions,
|
||||
network: NetworkInspect,
|
||||
): Network {
|
||||
|
||||
const ret = new Network(opts);
|
||||
|
||||
const match = network.Name.match(/^([0-9]+)_(.+)$/);
|
||||
if (match == null) {
|
||||
// FIXME: Type this properly
|
||||
throw new Error('Invalid network name: ' + network.Name);
|
||||
throw new InvalidNetworkNameError(network.Name);
|
||||
}
|
||||
const appId = checkInt(match[1]) || null;
|
||||
if (!appId) {
|
||||
throw new Error('Invalid appId: ' + appId);
|
||||
throw new InvalidAppIdError(match[1]);
|
||||
}
|
||||
|
||||
ret.appId = appId;
|
||||
ret.name = match[2];
|
||||
ret.config = { };
|
||||
ret.config = {
|
||||
driver: network.Driver,
|
||||
ipam: {
|
||||
driver: network.IPAM.Driver,
|
||||
config: _.map(network.IPAM.Config, (conf) => {
|
||||
const newConf: NetworkConfig['ipam']['config'][0] = {
|
||||
subnet: conf.Subnet,
|
||||
gateway: conf.Gateway,
|
||||
};
|
||||
|
||||
if (conf.IPRange != null) {
|
||||
newConf.ipRange = conf.IPRange;
|
||||
}
|
||||
if (conf.AuxAddress != null) {
|
||||
newConf.auxAddress = conf.AuxAddress;
|
||||
}
|
||||
return newConf;
|
||||
}),
|
||||
options: network.IPAM.Options == null ? { } : network.IPAM.Options,
|
||||
},
|
||||
enableIPv6: network.EnableIPv6,
|
||||
internal: network.Internal,
|
||||
labels: _.omit(network.Labels, [ 'io.resin.supervised' ]),
|
||||
options: network.Options,
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static async fromAppIdAndName(
|
||||
public static async fromNameAndAppId(
|
||||
opts: NetworkOptions,
|
||||
name: string,
|
||||
appId: number,
|
||||
): Bluebird<Network> {
|
||||
const network = await opts.docker.getNetwork(`${appId}_${name}`).inspect();
|
||||
return Network.fromDockerodeNetwork(opts, network);
|
||||
return Network.fromDockerNetwork(opts, network);
|
||||
}
|
||||
|
||||
public static fromComposeObject(
|
||||
opts: NetworkOptions,
|
||||
name: string,
|
||||
appId: number,
|
||||
network: ComposeNetwork,
|
||||
network: NetworkConfig,
|
||||
): Network {
|
||||
const net = new Network(opts);
|
||||
net.name = name;
|
||||
net.appId = appId;
|
||||
net.config = network;
|
||||
|
||||
Network.validateComposeConfig(network);
|
||||
|
||||
// Assign the default values for a network inspect,
|
||||
// so when we come to compare, it will match
|
||||
net.config = _.defaultsDeep(network, {
|
||||
driver: 'bridge',
|
||||
ipam: {
|
||||
driver: 'default',
|
||||
config: [],
|
||||
options: { },
|
||||
},
|
||||
enableIPv6: false,
|
||||
internal: false,
|
||||
labels: { },
|
||||
options: { },
|
||||
});
|
||||
|
||||
return net;
|
||||
}
|
||||
@ -91,26 +132,17 @@ export class Network {
|
||||
public create(): Bluebird<void> {
|
||||
this.logger.logSystemEvent(logTypes.createNetwork, { network: { name: this.name } });
|
||||
|
||||
return Network.fromAppIdAndName(this.networkOpts, this.name, this.appId)
|
||||
return Network.fromNameAndAppId(this.networkOpts, this.name, this.appId)
|
||||
.then((current) => {
|
||||
if (!this.isEqualConfig(current)) {
|
||||
// FIXME: type this error
|
||||
throw new Error(
|
||||
`Trying to create network '${this.name}', but a network` +
|
||||
' with the same anme and different configuration exists',
|
||||
);
|
||||
throw new ResourceRecreationAttemptError('network', this.name);
|
||||
}
|
||||
|
||||
// We have a network with the same config and name already created -
|
||||
// we can skip this.
|
||||
})
|
||||
.catch(NotFoundError, () => {
|
||||
return this.docker.createNetwork({
|
||||
Name: this.getDockerName(),
|
||||
Labels: {
|
||||
'io.resin.supervised': 'true',
|
||||
},
|
||||
});
|
||||
return this.docker.createNetwork(this.toDockerConfig());
|
||||
})
|
||||
.tapCatch((err) => {
|
||||
this.logger.logSystemEvent(logTypes.createNetworkError, {
|
||||
@ -120,6 +152,36 @@ export class Network {
|
||||
});
|
||||
}
|
||||
|
||||
public toDockerConfig(): DockerNetworkConfig {
|
||||
return {
|
||||
Name: this.getDockerName(),
|
||||
Driver: this.config.driver,
|
||||
CheckDuplicate: true,
|
||||
IPAM: {
|
||||
Driver: this.config.ipam.driver,
|
||||
Config: _.map(this.config.ipam.config, (conf) => {
|
||||
const ipamConf: DockerIPAMConfig = {
|
||||
Subnet: conf.subnet,
|
||||
Gateway: conf.gateway,
|
||||
};
|
||||
if (conf.auxAddress != null) {
|
||||
ipamConf.AuxAddress = conf.auxAddress;
|
||||
}
|
||||
if (conf.ipRange != null) {
|
||||
ipamConf.IPRange = conf.ipRange;
|
||||
}
|
||||
return ipamConf;
|
||||
}),
|
||||
Options: this.config.ipam.options,
|
||||
},
|
||||
EnableIPv6: this.config.enableIPv6,
|
||||
Internal: this.config.internal,
|
||||
Labels: _.merge({}, {
|
||||
'io.resin.supervised': 'true',
|
||||
}, this.config.labels),
|
||||
};
|
||||
}
|
||||
|
||||
public remove(): Bluebird<void> {
|
||||
this.logger.logSystemEvent(
|
||||
logTypes.removeNetwork,
|
||||
@ -136,12 +198,36 @@ export class Network {
|
||||
|
||||
}
|
||||
|
||||
public isEqualConfig(_network: Network) {
|
||||
return true;
|
||||
public isEqualConfig(network: Network): boolean {
|
||||
|
||||
// don't compare the ipam.config if it's not present
|
||||
// in the target state (as it will be present in the
|
||||
// current state, due to docker populating it with
|
||||
// default or generated values)
|
||||
let configToCompare = this.config;
|
||||
if (network.config.ipam.config.length === 0) {
|
||||
configToCompare = _.cloneDeep(this.config);
|
||||
configToCompare.ipam.config = [];
|
||||
}
|
||||
|
||||
return _.isEqual(configToCompare, network.config);
|
||||
}
|
||||
|
||||
public getDockerName(): string {
|
||||
return `${this.appId}_${this.name}`;
|
||||
}
|
||||
|
||||
private static validateComposeConfig(config: NetworkConfig): void {
|
||||
// Check if every ipam config entry has both a subnet and a gateway
|
||||
_.each(
|
||||
_.get(config, 'config.ipam.config', []),
|
||||
({ subnet, gateway }) => {
|
||||
if (subnet == null || gateway == null) {
|
||||
throw new InvalidNetworkConfigurationError(
|
||||
'Network IPAM config entries must have both a subnet and gateway',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
72
src/compose/types/network.ts
Normal file
72
src/compose/types/network.ts
Normal file
@ -0,0 +1,72 @@
|
||||
// It appears the dockerode typings are incomplete,
|
||||
// extend here for now.
|
||||
// TODO: Upstream these to definitelytyped
|
||||
export interface NetworkInspect {
|
||||
Name: string;
|
||||
Id: string;
|
||||
Created: string;
|
||||
Scope: string;
|
||||
Driver: string;
|
||||
EnableIPv6: boolean;
|
||||
IPAM: {
|
||||
Driver: string;
|
||||
Options: null | { [optName: string]: string };
|
||||
Config: Array<{
|
||||
Subnet: string;
|
||||
Gateway: string;
|
||||
IPRange?: string;
|
||||
AuxAddress?: string;
|
||||
}>;
|
||||
};
|
||||
Internal: boolean;
|
||||
Attachable: boolean;
|
||||
Ingress: boolean;
|
||||
Containers: {
|
||||
[containerId: string]: {
|
||||
Name: string;
|
||||
EndpointID: string;
|
||||
MacAddress: string;
|
||||
IPv4Address: string;
|
||||
IPv6Address: string;
|
||||
};
|
||||
};
|
||||
Options: { [optName: string]: string };
|
||||
Labels: { [labelName: string]: string };
|
||||
}
|
||||
|
||||
export interface NetworkConfig {
|
||||
driver: string;
|
||||
ipam: {
|
||||
driver: string;
|
||||
config: Array<{ subnet: string, gateway: string, ipRange?: string, auxAddress?: string }>;
|
||||
options: { [optName: string]: string };
|
||||
};
|
||||
enableIPv6: boolean;
|
||||
internal: boolean;
|
||||
labels: { [labelName: string]: string };
|
||||
options: { [optName: string]: string };
|
||||
}
|
||||
|
||||
export interface DockerIPAMConfig {
|
||||
Subnet: string;
|
||||
IPRange?: string;
|
||||
Gateway: string;
|
||||
AuxAddress?: string;
|
||||
}
|
||||
|
||||
export interface DockerNetworkConfig {
|
||||
Name: string;
|
||||
Driver?: string;
|
||||
CheckDuplicate: boolean;
|
||||
IPAM?: {
|
||||
Driver?: string;
|
||||
Config?: DockerIPAMConfig[];
|
||||
Options?: Dictionary<string>;
|
||||
};
|
||||
Internal?: boolean;
|
||||
Attachable?: boolean;
|
||||
Ingress?: boolean;
|
||||
Options?: Dictionary<string>;
|
||||
Labels?: Dictionary<string>;
|
||||
EnableIPv6?: boolean;
|
||||
}
|
75
test/18-compose-network.coffee
Normal file
75
test/18-compose-network.coffee
Normal file
@ -0,0 +1,75 @@
|
||||
m = require 'mochainon'
|
||||
{ expect } = m.chai
|
||||
|
||||
{ Network } = require '../src/compose/network'
|
||||
|
||||
describe 'compose/network.coffee', ->
|
||||
|
||||
describe 'compose config -> internal config', ->
|
||||
|
||||
it 'should convert a compose configuration to an internal representation', ->
|
||||
|
||||
network = Network.fromComposeObject({ logger: null, docker: null }, 'test', 123, {
|
||||
'driver':'bridge',
|
||||
'ipam':{
|
||||
'driver':'default',
|
||||
'config':[
|
||||
{
|
||||
'subnet':'172.25.0.0/25',
|
||||
'gateway':'172.25.0.1'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
expect(network.config).to.deep.equal({
|
||||
driver: 'bridge'
|
||||
ipam: {
|
||||
driver: 'default'
|
||||
config: [
|
||||
subnet: '172.25.0.0/25'
|
||||
gateway: '172.25.0.1'
|
||||
]
|
||||
options: { }
|
||||
}
|
||||
enableIPv6: false,
|
||||
internal: false,
|
||||
labels: { }
|
||||
options: { }
|
||||
})
|
||||
|
||||
describe 'internal config -> docker config', ->
|
||||
|
||||
it 'should convert an internal representation to a docker representation', ->
|
||||
|
||||
network = Network.fromComposeObject({ logger: null, docker: null }, 'test', 123, {
|
||||
'driver':'bridge',
|
||||
'ipam':{
|
||||
'driver':'default',
|
||||
'config':[
|
||||
{
|
||||
'subnet':'172.25.0.0/25',
|
||||
'gateway':'172.25.0.1'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
expect(network.toDockerConfig()).to.deep.equal({
|
||||
Name: '123_test',
|
||||
Driver: 'bridge',
|
||||
CheckDuplicate: true,
|
||||
IPAM: {
|
||||
Driver: 'default',
|
||||
Config: [{
|
||||
Subnet: '172.25.0.0/25'
|
||||
Gateway: '172.25.0.1'
|
||||
}]
|
||||
Options: { }
|
||||
}
|
||||
EnableIPv6: false,
|
||||
Internal: false,
|
||||
Labels: {
|
||||
'io.resin.supervised': 'true'
|
||||
}
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user