Compose: Support more network creation options

Change-type: minor
Signed-off-by: Cameron Diver <cameron@resin.io>
This commit is contained in:
Cameron Diver 2018-07-25 13:59:59 +01:00
parent a7551abe93
commit e0231f15e9
No known key found for this signature in database
GPG Key ID: 69264F9C923F55C1
6 changed files with 306 additions and 50 deletions

View File

@ -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
View 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 { }

View File

@ -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> {

View File

@ -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',
);
}
},
);
}
}

View 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;
}

View 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'
}
})