Merge pull request #5896 from docker/5874-legacy-proj-name

Retrieve objects using legacy (< 1.21) project names
This commit is contained in:
Joffrey F 2018-04-24 16:41:42 -07:00 committed by GitHub
commit faa532c315
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 131 additions and 36 deletions

View File

@ -2,6 +2,7 @@ from __future__ import absolute_import
from __future__ import unicode_literals
import logging
import re
from collections import OrderedDict
from docker.errors import NotFound
@ -10,9 +11,11 @@ from docker.types import IPAMPool
from docker.utils import version_gte
from docker.utils import version_lt
from . import __version__
from .config import ConfigurationError
from .const import LABEL_NETWORK
from .const import LABEL_PROJECT
from .const import LABEL_VERSION
log = logging.getLogger(__name__)
@ -39,6 +42,7 @@ class Network(object):
self.enable_ipv6 = enable_ipv6
self.labels = labels
self.custom_name = custom_name
self.legacy = None
def ensure(self):
if self.external:
@ -64,8 +68,9 @@ class Network(object):
)
return
self._set_legacy_flag()
try:
data = self.inspect()
data = self.inspect(legacy=self.legacy)
check_remote_network_config(data, self)
except NotFound:
driver_name = 'the default driver'
@ -73,8 +78,7 @@ class Network(object):
driver_name = 'driver "{}"'.format(self.driver)
log.info(
'Creating network "{}" with {}'
.format(self.full_name, driver_name)
'Creating network "{}" with {}'.format(self.full_name, driver_name)
)
self.client.create_network(
@ -91,21 +95,38 @@ class Network(object):
def remove(self):
if self.external:
log.info("Network %s is external, skipping", self.full_name)
log.info("Network %s is external, skipping", self.true_name)
return
log.info("Removing network {}".format(self.full_name))
self.client.remove_network(self.full_name)
log.info("Removing network {}".format(self.true_name))
self.client.remove_network(self.true_name)
def inspect(self):
def inspect(self, legacy=False):
if legacy:
return self.client.inspect_network(self.legacy_full_name)
return self.client.inspect_network(self.full_name)
@property
def legacy_full_name(self):
if self.custom_name:
return self.name
return '{0}_{1}'.format(
re.sub(r'[_-]', '', self.project), self.name
)
@property
def full_name(self):
if self.custom_name:
return self.name
return '{0}_{1}'.format(self.project, self.name)
@property
def true_name(self):
self._set_legacy_flag()
if self.legacy:
return self.legacy_full_name
return self.full_name
@property
def _labels(self):
if version_lt(self.client._version, '1.23'):
@ -114,9 +135,19 @@ class Network(object):
labels.update({
LABEL_PROJECT: self.project,
LABEL_NETWORK: self.name,
LABEL_VERSION: __version__,
})
return labels
def _set_legacy_flag(self):
if self.legacy is not None:
return
try:
data = self.inspect(legacy=True)
self.legacy = data is not None
except NotFound:
self.legacy = False
def create_ipam_config_from_dict(ipam_dict):
if not ipam_dict:
@ -150,49 +181,49 @@ def check_remote_ipam_config(remote, local):
remote_ipam = remote.get('IPAM')
ipam_dict = create_ipam_config_from_dict(local.ipam)
if local.ipam.get('driver') and local.ipam.get('driver') != remote_ipam.get('Driver'):
raise NetworkConfigChangedError(local.full_name, 'IPAM driver')
raise NetworkConfigChangedError(local.true_name, 'IPAM driver')
if len(ipam_dict['Config']) != 0:
if len(ipam_dict['Config']) != len(remote_ipam['Config']):
raise NetworkConfigChangedError(local.full_name, 'IPAM configs')
raise NetworkConfigChangedError(local.true_name, 'IPAM configs')
remote_configs = sorted(remote_ipam['Config'], key='Subnet')
local_configs = sorted(ipam_dict['Config'], key='Subnet')
while local_configs:
lc = local_configs.pop()
rc = remote_configs.pop()
if lc.get('Subnet') != rc.get('Subnet'):
raise NetworkConfigChangedError(local.full_name, 'IPAM config subnet')
raise NetworkConfigChangedError(local.true_name, 'IPAM config subnet')
if lc.get('Gateway') is not None and lc.get('Gateway') != rc.get('Gateway'):
raise NetworkConfigChangedError(local.full_name, 'IPAM config gateway')
raise NetworkConfigChangedError(local.true_name, 'IPAM config gateway')
if lc.get('IPRange') != rc.get('IPRange'):
raise NetworkConfigChangedError(local.full_name, 'IPAM config ip_range')
raise NetworkConfigChangedError(local.true_name, 'IPAM config ip_range')
if sorted(lc.get('AuxiliaryAddresses')) != sorted(rc.get('AuxiliaryAddresses')):
raise NetworkConfigChangedError(local.full_name, 'IPAM config aux_addresses')
raise NetworkConfigChangedError(local.true_name, 'IPAM config aux_addresses')
remote_opts = remote_ipam.get('Options') or {}
local_opts = local.ipam.get('Options') or {}
for k in set.union(set(remote_opts.keys()), set(local_opts.keys())):
if remote_opts.get(k) != local_opts.get(k):
raise NetworkConfigChangedError(local.full_name, 'IPAM option "{}"'.format(k))
raise NetworkConfigChangedError(local.true_name, 'IPAM option "{}"'.format(k))
def check_remote_network_config(remote, local):
if local.driver and remote.get('Driver') != local.driver:
raise NetworkConfigChangedError(local.full_name, 'driver')
raise NetworkConfigChangedError(local.true_name, 'driver')
local_opts = local.driver_opts or {}
remote_opts = remote.get('Options') or {}
for k in set.union(set(remote_opts.keys()), set(local_opts.keys())):
if k in OPTS_EXCEPTIONS:
continue
if remote_opts.get(k) != local_opts.get(k):
raise NetworkConfigChangedError(local.full_name, 'option "{}"'.format(k))
raise NetworkConfigChangedError(local.true_name, 'option "{}"'.format(k))
if local.ipam is not None:
check_remote_ipam_config(remote, local)
if local.internal is not None and local.internal != remote.get('Internal', False):
raise NetworkConfigChangedError(local.full_name, 'internal')
raise NetworkConfigChangedError(local.true_name, 'internal')
if local.enable_ipv6 is not None and local.enable_ipv6 != remote.get('EnableIPv6', False):
raise NetworkConfigChangedError(local.full_name, 'enable_ipv6')
raise NetworkConfigChangedError(local.true_name, 'enable_ipv6')
local_labels = local.labels or {}
remote_labels = remote.get('Labels', {})
@ -202,7 +233,7 @@ def check_remote_network_config(remote, local):
if remote_labels.get(k) != local_labels.get(k):
log.warn(
'Network {}: label "{}" has changed. It may need to be'
' recreated.'.format(local.full_name, k)
' recreated.'.format(local.true_name, k)
)
@ -257,7 +288,7 @@ class ProjectNetworks(object):
try:
network.remove()
except NotFound:
log.warn("Network %s not found.", network.full_name)
log.warn("Network %s not found.", network.true_name)
def initialize(self):
if not self.use_networking:
@ -286,7 +317,7 @@ def get_networks(service_dict, network_definitions):
for name, netdef in get_network_defs_for_service(service_dict).items():
network = network_definitions.get(name)
if network:
networks[network.full_name] = netdef
networks[network.true_name] = netdef
else:
raise ConfigurationError(
'Service "{}" uses an undefined network "{}"'

View File

@ -51,6 +51,7 @@ from .progress_stream import StreamOutputError
from .utils import json_hash
from .utils import parse_bytes
from .utils import parse_seconds_float
from .version import ComposeVersion
log = logging.getLogger(__name__)
@ -192,11 +193,25 @@ class Service(object):
def containers(self, stopped=False, one_off=False, filters={}):
filters.update({'label': self.labels(one_off=one_off)})
return list(filter(None, [
result = list(filter(None, [
Container.from_ps(self.client, container)
for container in self.client.containers(
all=stopped,
filters=filters)]))
filters=filters)])
)
if result:
return result
filters.update({'label': self.labels(one_off=one_off, legacy=True)})
return list(
filter(
self.has_legacy_proj_name, filter(None, [
Container.from_ps(self.client, container)
for container in self.client.containers(
all=stopped,
filters=filters)])
)
)
def get_container(self, number=1):
"""Return a :class:`compose.container.Container` for this service. The
@ -380,6 +395,10 @@ class Service(object):
has_diverged = False
for c in containers:
if self.has_legacy_proj_name(c):
log.debug('%s has diverged: Legacy project name' % c.name)
has_diverged = True
continue
container_config_hash = c.labels.get(LABEL_CONFIG_HASH, None)
if container_config_hash != config_hash:
log.debug(
@ -1053,11 +1072,12 @@ class Service(object):
def can_be_built(self):
return 'build' in self.options
def labels(self, one_off=False):
def labels(self, one_off=False, legacy=False):
proj_name = self.project if not legacy else re.sub(r'[_-]', '', self.project)
return [
'{0}={1}'.format(LABEL_PROJECT, self.project),
'{0}={1}'.format(LABEL_PROJECT, proj_name),
'{0}={1}'.format(LABEL_SERVICE, self.name),
'{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False")
'{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False"),
]
@property
@ -1214,6 +1234,12 @@ class Service(object):
return result
def has_legacy_proj_name(self, ctnr):
return (
ComposeVersion(ctnr.labels.get(LABEL_VERSION)) < ComposeVersion('1.21.0') and
ctnr.project != self.project
)
def short_id_alias_exists(container, network):
aliases = container.get(

View File

@ -2,15 +2,19 @@ from __future__ import absolute_import
from __future__ import unicode_literals
import logging
import re
from docker.errors import NotFound
from docker.utils import version_lt
from . import __version__
from .config import ConfigurationError
from .config.types import VolumeSpec
from .const import LABEL_PROJECT
from .const import LABEL_VERSION
from .const import LABEL_VOLUME
log = logging.getLogger(__name__)
@ -25,6 +29,7 @@ class Volume(object):
self.external = external
self.labels = labels
self.custom_name = custom_name
self.legacy = None
def create(self):
return self.client.create_volume(
@ -33,17 +38,20 @@ class Volume(object):
def remove(self):
if self.external:
log.info("Volume %s is external, skipping", self.full_name)
log.info("Volume %s is external, skipping", self.true_name)
return
log.info("Removing volume %s", self.full_name)
return self.client.remove_volume(self.full_name)
log.info("Removing volume %s", self.true_name)
return self.client.remove_volume(self.true_name)
def inspect(self):
def inspect(self, legacy=None):
if legacy:
return self.client.inspect_volume(self.legacy_full_name)
return self.client.inspect_volume(self.full_name)
def exists(self):
self._set_legacy_flag()
try:
self.inspect()
self.inspect(legacy=self.legacy)
except NotFound:
return False
return True
@ -54,6 +62,21 @@ class Volume(object):
return self.name
return '{0}_{1}'.format(self.project, self.name)
@property
def legacy_full_name(self):
if self.custom_name:
return self.name
return '{0}_{1}'.format(
re.sub(r'[_-]', '', self.project), self.name
)
@property
def true_name(self):
self._set_legacy_flag()
if self.legacy:
return self.legacy_full_name
return self.full_name
@property
def _labels(self):
if version_lt(self.client._version, '1.23'):
@ -62,9 +85,19 @@ class Volume(object):
labels.update({
LABEL_PROJECT: self.project,
LABEL_VOLUME: self.name,
LABEL_VERSION: __version__,
})
return labels
def _set_legacy_flag(self):
if self.legacy is not None:
return
try:
data = self.inspect(legacy=True)
self.legacy = data is not None
except NotFound:
self.legacy = False
class ProjectVolumes(object):
@ -94,7 +127,7 @@ class ProjectVolumes(object):
try:
volume.remove()
except NotFound:
log.warn("Volume %s not found.", volume.full_name)
log.warn("Volume %s not found.", volume.true_name)
def initialize(self):
try:
@ -124,7 +157,7 @@ class ProjectVolumes(object):
)
volume.create()
else:
check_remote_volume_config(volume.inspect(), volume)
check_remote_volume_config(volume.inspect(legacy=volume.legacy), volume)
except NotFound:
raise ConfigurationError(
'Volume %s specifies nonexistent driver %s' % (volume.name, volume.driver)
@ -136,9 +169,9 @@ class ProjectVolumes(object):
if isinstance(volume_spec, VolumeSpec):
volume = self.volumes[volume_spec.external]
return volume_spec._replace(external=volume.full_name)
return volume_spec._replace(external=volume.true_name)
else:
volume_spec.source = self.volumes[volume_spec.source].full_name
volume_spec.source = self.volumes[volume_spec.source].true_name
return volume_spec
@ -152,7 +185,7 @@ class VolumeConfigChangedError(ConfigurationError):
'first:\n$ docker volume rm {full_name}'.format(
vol_name=local.name, property_name=property_name,
local_value=local_value, remote_value=remote_value,
full_name=local.full_name
full_name=local.true_name
)
)

View File

@ -84,6 +84,7 @@ class NetworkTest(unittest.TestCase):
{'Driver': 'overlay', 'Options': remote_options}, net
)
@mock.patch('compose.network.Network.true_name', lambda n: n.full_name)
def test_check_remote_network_config_driver_mismatch(self):
net = Network(None, 'compose_test', 'net1', 'overlay')
with pytest.raises(NetworkConfigChangedError) as e:
@ -93,6 +94,7 @@ class NetworkTest(unittest.TestCase):
assert 'driver has changed' in str(e.value)
@mock.patch('compose.network.Network.true_name', lambda n: n.full_name)
def test_check_remote_network_config_options_mismatch(self):
net = Network(None, 'compose_test', 'net1', 'overlay')
with pytest.raises(NetworkConfigChangedError) as e:
@ -146,6 +148,7 @@ class NetworkTest(unittest.TestCase):
net
)
@mock.patch('compose.network.Network.true_name', lambda n: n.full_name)
def test_check_remote_network_labels_mismatch(self):
net = Network(None, 'compose_test', 'net1', 'overlay', labels={
'com.project.touhou.character': 'sakuya.izayoi'

View File

@ -60,6 +60,7 @@ class ProjectTest(unittest.TestCase):
assert project.get_service('db').options['image'] == 'busybox:latest'
assert not project.networks.use_networking
@mock.patch('compose.network.Network.true_name', lambda n: n.full_name)
def test_from_config_v2(self):
config = Config(
version=V2_0,
@ -217,6 +218,7 @@ class ProjectTest(unittest.TestCase):
)
assert project.get_service('test')._get_volumes_from() == [container_name + ":rw"]
@mock.patch('compose.network.Network.true_name', lambda n: n.full_name)
def test_use_volumes_from_service_container(self):
container_ids = ['aabbccddee', '12345']