diff --git a/deps/eng b/deps/eng index 34cbcb43..abf5ffdb 160000 --- a/deps/eng +++ b/deps/eng @@ -1 +1 @@ -Subproject commit 34cbcb436d57ba17538b34592d121c856e03c736 +Subproject commit abf5ffdb7a0637c032ee81efe76b8f3859c17699 diff --git a/docs/api/divergence.md b/docs/api/divergence.md index 07e08290..86d9c257 100644 --- a/docs/api/divergence.md +++ b/docs/api/divergence.md @@ -154,6 +154,8 @@ names are currently defined: Used by the CNS service. * `triton.network.public` (string): Set on a container, used to specify the external network name the instance will use. +* `triton.network.public_ipv4` (string): Set on a container, used to specify the + external network ip address the instance will use. The `com.joyent.*` namespace is reserved for Triton specific use cases, these label names are currently defined: diff --git a/docs/api/features/networks.md b/docs/api/features/networks.md index 18abcbd0..4ce0b799 100644 --- a/docs/api/features/networks.md +++ b/docs/api/features/networks.md @@ -53,6 +53,11 @@ Note that this this only overrides the default public network selection. This means that when fabric networks are enabled you will still need to specify one of `-p` or `-P` to get the public NIC. +The external network ipv4 address used by a container can be changed by setting +the `triton.network.public_ipv4` label to the desired ipv4 address that is +available in the `triton.network.public` network. The account must +be set as an owner on the `triton.network.public` network. + ## Related * [`sdc-fabric vlan`](https://apidocs.tritondatacenter.com/cloudapi/#CreateFabricVLAN) and `POST /my/fabrics/default/vlans` in CloudAPI diff --git a/lib/backends/sdc/networks.js b/lib/backends/sdc/networks.js index 74ec05a2..cd0debbf 100644 --- a/lib/backends/sdc/networks.js +++ b/lib/backends/sdc/networks.js @@ -5,7 +5,8 @@ */ /* - * Copyright (c) 2017, Joyent, Inc. + * Copyright 2021 Joyent, Inc. + * Copyright 2025 Bruce Smith */ /* @@ -29,6 +30,8 @@ var ADMIN_NIC_TAG = 'admin'; // Label name used to set the external (public) network for a container. var TRITON_PUBLIC_NETWORK_LABEL = 'triton.network.public'; +var TRITON_PUBLIC_NETWORK_IPV4_LABEL = 'triton.network.public_ipv4'; + var _napiClientCache; // set in `getNapiClient` @@ -623,6 +626,8 @@ function externalNetworkByName(opts, container, payload, callback) { assert.func(callback, 'callback'); var externalNetworkName; + var externalNetworkIP; + var labels = container.Labels || {}; var log = opts.log; @@ -632,6 +637,12 @@ function externalNetworkByName(opts, container, payload, callback) { externalNetworkName = labels[TRITON_PUBLIC_NETWORK_LABEL]; } + if (Object.prototype.hasOwnProperty.call(labels, + TRITON_PUBLIC_NETWORK_IPV4_LABEL)) + { + externalNetworkIP = labels[TRITON_PUBLIC_NETWORK_IPV4_LABEL]; + } + if (!payload.hasOwnProperty('networks')) { payload.networks = []; } else { @@ -698,7 +709,26 @@ function externalNetworkByName(opts, container, payload, callback) { return; } - payload.networks.push({uuid: networks[0].uuid, primary: true}); + var network = {ipv4_uuid: networks[0].uuid, primary: true}; + // We land here if triton.network.public_ipv4 was provided. + if (externalNetworkIP) { + // We can only assign IPs to networks we are the owner on. + // Some networks can have no owner allowing us to provision + // but not assign addresses on them. + if ( + networks[0].owner_uuids == null + || networks[0].owner_uuids.indexOf(opts.account.uuid) === -1 + ) { + callback(new errors.ValidationError(util.format( + '%s label requires network ownership', + TRITON_PUBLIC_NETWORK_IPV4_LABEL))); + return; + } + + network.ipv4_ips = [ externalNetworkIP ]; + } + + payload.networks.push(network); callback(); return; @@ -752,13 +782,13 @@ function addNetworksToContainerPayload(opts, container, payload, callback) { vasync.pipeline({ funcs: [ function addFabricNetworks(_, next) { - if (!opts.config.overlay.enabled) { - next(); - return; - } networkMode = container.HostConfig.NetworkMode; - if (!networkMode || networkMode === 'bridge' - || networkMode === 'default') { + var overlayEnabled = opts.config.overlay.enabled + var defaultNetworkMode = !networkMode || networkMode === 'bridge' + || networkMode === 'default' + + // Handle Fabrics + if (overlayEnabled && defaultNetworkMode) { getDefaultFabricNetwork(opts, function onGetDefaultFabricNet(getDefaultFabricNetErr, defaultFabricNet) { @@ -766,17 +796,34 @@ function addNetworksToContainerPayload(opts, container, payload, callback) { [ {uuid: defaultFabricNet, primary: true} ]; next(getDefaultFabricNetErr); }); - } else { - findNetworkOrPoolByNameOrId(networkMode, opts, - function (findErr, network) + // Handle Non Fabrics + } else if (!defaultNetworkMode) { + findNetworkOrPoolByNameOrId(networkMode, opts, + function (findErr, network) { if (findErr) { next(findErr); return; } - payload.networks = [ {uuid: network.uuid, primary: true} ]; + payload.networks = + [ {ipv4_uuid: network.uuid, primary: true} ]; + netCfg = container.NetworkingConfig + if (netCfg != null + && netCfg.EndpointsConfig != null + && netCfg.EndpointsConfig[networkMode] + != undefined) { + var ipv4Addr = + container.NetworkingConfig. + EndpointsConfig[networkMode].IPAMConfig. + IPv4Address; + if (ipv4Addr) { + payload.networks[0].ipv4_ips = [ ipv4Addr ]; + } + } next(); }); + } else { + next(); } }, @@ -800,6 +847,86 @@ function addNetworksToContainerPayload(opts, container, payload, callback) { externalNetworkByName(opts, container, payload, next); }, + // Enforce 2 distinct networks + function enforceDistinctNetworks(_, next) { + if (payload.networks.length === 2) { + var prevUUID; + for (var i = 0; i < payload.networks.length; i++) { + var nw = payload.networks[i]; + // Handle both network and network obj + var netUUID = nw.ipv4_uuid || nw.uuid; + if (prevUUID === netUUID) { + next(new errors.ValidationError(util.format( + 'both networks are of uuid: %s, networks must be ' + + 'distinct', + netUUID))); + return; + } + prevUUID = netUUID; + } + } + next(); + }, + + // Enforce Publishing Ports for Non Fabric with > 1 network + function enforcePublishingPorts(_, next) { + // Handle publishing ports for non fabrics + if (payload.networks.length === 2 + && !containers.publishingPorts(container)) { + next(new errors.ValidationError(util.format( + 'non fabrics with 2 networks requires a container with ' + + 'published ports' + ))); + return; + } + next(); + }, + + /* + * We need to verify that if a user passed in networks with IPs that none + * of the IPs are considered "managed". NAPI will handle other + * validations for us. + */ + function verifyNetworkIPs(_, next) { + var napi = getNapiClient(opts.config.napi); + var networksWithIps = []; + payload.networks.forEach(function forEachNetwork(net) { + // Today we only support passing in ipv4 addrs, + // but this should be extended to support ipv6 addrs + if (net.ipv4_ips && net.ipv4_ips.length > 0) { + networksWithIps.push(net); + } + }); + + vasync.forEachPipeline({ + 'func': function validateIp(network, done) { + napi.getIP(network.ipv4_uuid, network.ipv4_ips[0], + function napiGetIp(err, ip) + { + if (err) { + done(err); + return; + } + if (ip.belongs_to_type === 'other' + || ip.owner_uuid === opts.config.adminUuid) { + done(new errors.InternalError( + 'Cannot use Managed IP')); + return; + } + done(null, ip); + }); + }, + 'inputs': networksWithIps + }, function (err) { + if (err) { + next(err); + return; + } + next(); + return; + }); + }, + function runModifyProvisionNetworksPlugins(_, next) { opts.app.plugins.modifyProvisionNetworks({ account: opts.account, diff --git a/package.json b/package.json index ec14ace8..0fb95564 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sdc-docker", - "version": "0.7.11", + "version": "0.8.0", "author": "MNX Cloud (mnx.io)", "private": true, "dependencies": { @@ -27,7 +27,7 @@ "strsplit": "1.0.0", "tape": "^4.4.0", "trace-event": "1.2.0", - "triton-tags": "1.4.0", + "triton-tags": "1.5.0", "ufds": "1.7.1", "vasync": "2.1.0", "verror": "1.9.0", @@ -44,4 +44,4 @@ "xtend": "^4.0.0" }, "license": "MPL-2.0" -} +} \ No newline at end of file diff --git a/test/integration/api-create.test.js b/test/integration/api-create.test.js index 5da952d7..2743f115 100644 --- a/test/integration/api-create.test.js +++ b/test/integration/api-create.test.js @@ -6,6 +6,7 @@ /* * Copyright 2017, Joyent, Inc. + * Copyright 2025 Bruce Smith */ /* @@ -44,8 +45,6 @@ var FABRICS = false; // --- Tests - - test('setup', function (tt) { tt.test('docker env', function (t) { @@ -875,7 +874,6 @@ test('create with NetworkMode (docker run --net=)', function (tt) { }); }); - /* * Tests for `docker run --label triton.network.public=foo` * @@ -993,3 +991,433 @@ test('run external network (docker run --label triton.network.public=)', }); }); }); + +/* + * Tests for `docker run --label triton.network.public_ipv4` + * + * TRITON-2497 Add static addresses to public networks + */ + +test('run external network (docker run --label triton.network.public_ipv4=)', + function (tt) { + var externalNetworkNoOwner; + var externalNetworkOwner; + var anotherExternalNetworkOwner; + var externalNetworkWrongOwner; + // This network will have no owner. + tt.test('add external network with no owner', function (t) { + // create a new one. + var nwUuid = libuuid.create(); + var nwParams = { + name: 'sdcdockertest_apicreate_external_no_owner', + nic_tag: 'external', + subnet: '10.0.11.0/24', + provision_start_ip: '10.0.11.2', + provision_end_ip: '10.0.11.254', + uuid: nwUuid, + vlan_id: 5, + gateway: '10.0.11.1', + resolvers: ['8.8.8.8', '8.8.4.4'] + }; + h.getOrCreateExternalNetwork(NAPI, nwParams, function (err, network) { + t.ifErr(err, 'getOrCreateExternalNetwork'); + externalNetworkNoOwner = network; + t.end(); + }); + }); + + // This network will have the correct owner. + tt.test('add external network with correct alice owner', function (t) { + // create a new one. + var nwUuid = libuuid.create(); + var nwParams = { + name: 'sdcdockertest_apicreate_external_alice0', + nic_tag: 'external', + subnet: '10.0.21.0/24', + provision_start_ip: '10.0.21.2', + provision_end_ip: '10.0.21.254', + uuid: nwUuid, + vlan_id: 5, + gateway: '10.0.21.1', + resolvers: ['8.8.8.8', '8.8.4.4'], + owner_uuids: [ALICE.account.uuid] + }; + h.getOrCreateExternalNetwork(NAPI, nwParams, function (err, network) { + t.ifErr(err, 'getOrCreateExternalNetwork'); + externalNetworkOwner = network; + t.end(); + }); + }); + + // This network will have the correct owner + tt.test('add another external network with correct alice owner', + function (t) { + // create a new one. + var nwUuid = libuuid.create(); + var nwParams = { + name: 'sdcdockertest_apicreate_external_alice1', + nic_tag: 'external', + subnet: '10.0.31.0/24', + provision_start_ip: '10.0.31.2', + provision_end_ip: '10.0.31.254', + uuid: nwUuid, + vlan_id: 5, + gateway: '10.0.31.1', + resolvers: ['8.8.8.8', '8.8.4.4'], + owner_uuids: [ALICE.account.uuid] + }; + h.getOrCreateExternalNetwork(NAPI, nwParams, + function (err, network) { + t.ifErr(err, 'getOrCreateExternalNetwork'); + anotherExternalNetworkOwner = network; + t.end(); + }); + }); + // This network will have the incorrect owner + tt.test('add another external network with incorrect bob owner', + function (t) { + // create a new one. + var nwUuid = libuuid.create(); + var nwParams = { + name: 'sdcdockertest_apicreate_external_bob0', + nic_tag: 'external', + subnet: '10.0.41.0/24', + provision_start_ip: '10.0.41.2', + provision_end_ip: '10.0.41.254', + uuid: nwUuid, + vlan_id: 5, + gateway: '10.0.41.1', + resolvers: ['8.8.8.8', '8.8.4.4'], + owner_uuids: [BOB.account.uuid] + }; + h.getOrCreateExternalNetwork(NAPI, nwParams, + function (err, network) { + t.ifErr(err, 'getOrCreateExternalNetwork'); + externalNetworkWrongOwner = network; + t.end(); + }); + }); + + // Fail to provision when there is no owner + tt.test('fail to run with assigned ipv4 address no owner', function (t) { + var expectedErr = '(Validation) triton.network.public_ipv4 label ' + + 'requires network ownership'; + h.createDockerContainer({ + vmapiClient: VMAPI, + dockerClient: DOCKER_ALICE, + test: t, + expectedErr: expectedErr, + extra: { + 'HostConfig.PublishAllPorts': true, + Labels: { + 'triton.network.public': externalNetworkNoOwner.name, + 'triton.network.public_ipv4': '10.0.11.200' + } + }, + start: true + }, oncreate); + + function oncreate(err, result) { + // Note: Error is already checked in createDockerContainer + assert.object(err, 'err'); + t.end(); + } + }); + + // provision when there is correct owner + tt.test('run with assigned ipv4 address with correct owner', function (t) { + var assignedAddr = '10.0.21.200'; + h.createDockerContainer({ + vmapiClient: VMAPI, + dockerClient: DOCKER_ALICE, + test: t, + extra: { + 'HostConfig.PublishAllPorts': true, + Labels: { + 'triton.network.public': externalNetworkOwner.name, + 'triton.network.public_ipv4': assignedAddr + } + }, + start: true + }, oncreate); + + function oncreate(err, result) { + assert.strictEqual(err, null); + var nics = result.vm.nics; + var extNic = nics[0]; + if (FABRICS) { + extNic = (nics[0].primary === true ? nics[0] : nics[1]); + t.equal(nics.length, 2, 'two nics'); + } else { + t.equal(nics.length, 1, 'one nic'); + } + t.equal(extNic.ip, assignedAddr, 'correct external ip') + DOCKER_ALICE.del('/containers/' + result.id + '?force=1', ondelete); + } + + function ondelete(err) { + t.ifErr(err, 'delete external triton.network.public_ipv4 ' + + 'testing container'); + t.end(); + } + }); + + // provision when there is correct owner publish false single network + tt.test('run with assigned ipv4 address with correct owner', function (t) { + var assignedAddr = '10.0.21.200'; + // With Non Fabrics this will deploy a single network dictated by + // the triton.network.public without exposed ports + var expectedErr = null; + // With Fabrics this is an error due to DOCKER-1045. + if (FABRICS) { + expectedErr = '(Validation) triton.network.public label requires a ' + + 'container with published ports' + } + h.createDockerContainer({ + vmapiClient: VMAPI, + dockerClient: DOCKER_ALICE, + test: t, + expectedErr: expectedErr, + extra: { + 'HostConfig.PublishAllPorts': false, + Labels: { + 'triton.network.public': externalNetworkOwner.name, + 'triton.network.public_ipv4': assignedAddr + } + }, + start: true + }, oncreate); + + function oncreate(err, result) { + if (FABRICS) { + assert.object(err, 'err'); + t.end(); + return; + } + assert.strictEqual(err, null); + var nics = result.vm.nics; + var extNic = nics[0]; + t.equal(extNic.ip, assignedAddr, 'correct external ip') + DOCKER_ALICE.del('/containers/' + result.id + '?force=1', ondelete); + } + + function ondelete(err) { + t.ifErr(err, 'delete external triton.network.public_ipv4 ' + + 'testing container'); + t.end(); + } + }); + + // fail to provision when there is incorrect owner + tt.test('fail to run with assigned ipv4 address with incorrect owner', + function (t) { + var assignedAddr = '10.0.41.200'; + var expectedErr = '(Error) network ' + + 'sdcdockertest_apicreate_external_bob0 not found'; + h.createDockerContainer({ + vmapiClient: VMAPI, + dockerClient: DOCKER_ALICE, + test: t, + expectedErr: expectedErr, + extra: { + 'HostConfig.PublishAllPorts': true, + Labels: { + 'triton.network.public': externalNetworkWrongOwner.name, + 'triton.network.public_ipv4': assignedAddr + } + }, + start: true + }, oncreate); + + function oncreate(err, result) { + // Note: Error is already checked in createDockerContainer + assert.object(err, 'err'); + t.end(); + } + }); + + // fail to provision when there is multiple ip on same network + // with correct owner + tt.test('fail run with assigned ipv4 address with multiple ip ' + + 'on same network with correct owner', function (t) { + var assignedAddrLabel = '10.0.21.200'; + var assignedAddrClient = '10.0.21.201'; + var expectedErr = format('(Validation) both networks are of uuid: %s, ' + + 'networks must be distinct', externalNetworkOwner.uuid); + var EndpointsConfig = {}; + EndpointsConfig[externalNetworkOwner.name] = { + IPAMConfig: { + IPv4Address: assignedAddrClient + } + } + h.createDockerContainer({ + vmapiClient: VMAPI, + dockerClient: DOCKER_ALICE, + test: t, + expectedErr: expectedErr, + extra: { + 'HostConfig.PublishAllPorts': true, + 'HostConfig.NetworkMode': externalNetworkOwner.name, + Labels: { + 'triton.network.public': externalNetworkOwner.name, + 'triton.network.public_ipv4': assignedAddrLabel + }, + 'NetworkingConfig.EndpointsConfig': EndpointsConfig + }, + start: true + }, oncreate); + + function oncreate(err, result) { + assert.object(err, 'err'); + t.end(); + } + }); + + // fail to provision when there is multiple ip on different networks + // with correct owner without published ports + tt.test('fail to run with assigned ipv4 address with multiple ip ' + + 'on different networks with correct owners without published ' + + 'ports', function (t) { + var assignedAddrLabel = '10.0.21.200'; + var assignedAddrClient = '10.0.31.200'; + var expectedErr = format('(Validation) non fabrics with 2 ' + + 'networks requires a container with published ports'); + if (FABRICS) { + var expectedErr = format('(Validation) triton.network.' + + 'public label requires a container with published ports'); + } + var EndpointsConfig = {}; + EndpointsConfig[anotherExternalNetworkOwner.name] = { + IPAMConfig: { + IPv4Address: assignedAddrClient + } + } + h.createDockerContainer({ + vmapiClient: VMAPI, + dockerClient: DOCKER_ALICE, + test: t, + expectedErr: expectedErr, + extra: { + 'HostConfig.PublishAllPorts': false, + 'HostConfig.NetworkMode': anotherExternalNetworkOwner.name, + Labels: { + 'triton.network.public': externalNetworkOwner.name, + 'triton.network.public_ipv4': assignedAddrLabel + }, + 'NetworkingConfig.EndpointsConfig': EndpointsConfig + }, + start: true + }, oncreate); + + function oncreate(err, result) { + assert.object(err, 'err'); + t.end(); + } + }); + + // provision when there is multiple ip on different networks + // with correct owner + tt.test('run with assigned ipv4 address with multiple ip ' + + 'on different networks with correct owners', function (t) { + var assignedAddrLabel = '10.0.21.200'; + var assignedAddrClient = '10.0.31.200'; + var EndpointsConfig = {}; + EndpointsConfig[anotherExternalNetworkOwner.name] = { + IPAMConfig: { + IPv4Address: assignedAddrClient + } + } + h.createDockerContainer({ + vmapiClient: VMAPI, + dockerClient: DOCKER_ALICE, + test: t, + extra: { + 'HostConfig.PublishAllPorts': true, + 'HostConfig.NetworkMode': anotherExternalNetworkOwner.name, + Labels: { + 'triton.network.public': externalNetworkOwner.name, + 'triton.network.public_ipv4': assignedAddrLabel + }, + 'NetworkingConfig.EndpointsConfig': EndpointsConfig + }, + start: true + }, oncreate); + + function oncreate(err, result) { + assert.strictEqual(err, null); + var nics = result.vm.nics; + t.equal(nics.length, 2, 'two nics'); + nics.forEach(function (nic) { + if (nic.primary) { + t.equal(nic.ip, assignedAddrLabel) + } else { + t.equal(nic.ip, assignedAddrClient) + } + }); + DOCKER_ALICE.del('/containers/' + result.id + '?force=1', ondelete); + } + + function ondelete(err) { + t.ifErr(err, 'delete external triton.network.public_ipv4 testing' + + 'container'); + t.end(); + } + }); + + // fail to provision when there is multiple ip on different networks + // with incorrect owners + tt.test('fail to run with assigned ipv4 address with multiple ip ' + + 'on different networks with incorrect owner and correct owner', + function (t) { + var expectedErr = '(Error) network ' + + 'sdcdockertest_apicreate_external_bob0 not found'; + var assignedAddrLabel = '10.0.21.200'; + var assignedAddrClient = '10.0.41.200'; + var EndpointsConfig = {}; + EndpointsConfig[anotherExternalNetworkOwner.name] = { + IPAMConfig: { + IPv4Address: assignedAddrClient + } + } + h.createDockerContainer({ + vmapiClient: VMAPI, + dockerClient: DOCKER_ALICE, + test: t, + expectedErr: expectedErr, + extra: { + 'HostConfig.PublishAllPorts': true, + 'HostConfig.NetworkMode': externalNetworkWrongOwner.name, + Labels: { + 'triton.network.public': externalNetworkOwner.name, + 'triton.network.public_ipv4': assignedAddrLabel + }, + 'NetworkingConfig.EndpointsConfig': EndpointsConfig + }, + start: true + }, oncreate); + + function oncreate(err, result) { + // Note: Error is already checked in createDockerContainer + assert.object(err, 'err'); + t.end(); + } + }); + + // These 4 networks are not cleaned up in the container creation + // so clean them all up here. + tt.test('TRITON-2497 external networks cleanup', function (t) { + var cleanupNetworks = [ + externalNetworkNoOwner, + externalNetworkOwner, + externalNetworkWrongOwner, + anotherExternalNetworkOwner + ]; + + cleanupNetworks.forEach(function (network) { + NAPI.deleteNetwork(network.uuid, function (err) { + t.ifErr(err, 'external network deletion'); + }); + }); + t.end(); + }); +}); diff --git a/test/integration/helpers.js b/test/integration/helpers.js index e19e6db1..51de7427 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -7,6 +7,8 @@ /* * Copyright (c) 2018, Joyent, Inc. * Copyright 2022 MNX Cloud, Inc. + * Copyright 2025 Edgecast Cloud LLC. + * Copyright 2025 Bruce Smith */ /* @@ -1772,6 +1774,9 @@ function createDockerContainer(opts, callback) { 'Name': '', 'MaximumRetryCount': 0 } + }, + 'NetworkingConfig': { + 'EndpointsConfig': {} } };