Skip to content

Commit

Permalink
Allow x-project-name property in Compose file to define project name
Browse files Browse the repository at this point in the history
Signed-off-by: Joffrey F <joffrey@docker.com>
  • Loading branch information
shin- committed Nov 16, 2017
1 parent d48002a commit 77a74b6
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 12 deletions.
30 changes: 26 additions & 4 deletions compose/cli/command.py
Expand Up @@ -85,9 +85,7 @@ def get_project(project_dir, config_path=None, project_name=None, verbose=False,
if not environment:
environment = Environment.from_env_file(project_dir)
config_details = config.find(project_dir, config_path, environment, override_dir)
project_name = get_project_name(
config_details.working_dir, project_name, environment
)
project_name = get_project_name(config_details, project_name, environment)
config_data = config.load(config_details)

api_version = environment.get(
Expand All @@ -103,13 +101,37 @@ def get_project(project_dir, config_path=None, project_name=None, verbose=False,
return Project.from_config(project_name, config_data, client)


def get_project_name(working_dir, project_name=None, environment=None):
def get_project_name(config_details, project_name=None, environment=None):
def normalize_name(name):
return re.sub(r'[^a-z0-9]', '', name.lower())

def get_name_from_config(config_details):
x_name = None
for file in config_details.config_files:
x_properties = file.get_x_properties()
if 'x-project-name' in x_properties:
if x_name and x_properties['x-project-name'] != x_name[0]:
raise errors.UserError(
'Conflicting x-project-name declarations: '
'"{}" ({}) does not match "{}" ({})'.format(
x_name[0], x_name[1], x_properties['x-project-name'],
file.filename
)
)
x_name = (x_properties['x-project-name'], file.filename)

return x_name[0] if x_name else None

working_dir = config_details.working_dir

if not environment:
environment = Environment.from_env_file(working_dir)

project_name = project_name or environment.get('COMPOSE_PROJECT_NAME')

if environment.get_boolean('COMPOSE_X_PROJECT_NAME') and not project_name:
project_name = get_name_from_config(config_details)

if project_name:
return normalize_name(project_name)

Expand Down
5 changes: 5 additions & 0 deletions compose/config/config.py
Expand Up @@ -225,6 +225,11 @@ def get_secrets(self):
def get_configs(self):
return {} if self.version < const.COMPOSEFILE_V3_3 else self.config.get('configs', {})

def get_x_properties(self):
if self.version == V1:
return {}
return dict((k, v) for k, v in self.config.items() if k.startswith('x-'))


class Config(namedtuple('_Config', 'version services volumes networks secrets configs')):
"""
Expand Down
61 changes: 53 additions & 8 deletions tests/unit/cli_test.py
Expand Up @@ -20,45 +20,54 @@
from compose.cli.docopt_command import NoSuchCommand
from compose.cli.errors import UserError
from compose.cli.main import TopLevelCommand
from compose.config.config import ConfigDetails
from compose.config.config import ConfigFile
from compose.const import IS_WINDOWS_PLATFORM
from compose.project import Project


class CLITestCase(unittest.TestCase):

@staticmethod
def get_config_details(working_dir, x_project_name=None):
config = {'version': '2.3'}
if x_project_name:
config['x-project-name'] = x_project_name
return ConfigDetails(working_dir, [ConfigFile('base.yml', config)])

def test_default_project_name(self):
test_dir = py._path.local.LocalPath('tests/fixtures/simple-composefile')
with test_dir.as_cwd():
project_name = get_project_name('.')
project_name = get_project_name(self.get_config_details('.'))
self.assertEqual('simplecomposefile', project_name)

def test_project_name_with_explicit_base_dir(self):
base_dir = 'tests/fixtures/simple-composefile'
project_name = get_project_name(base_dir)
project_name = get_project_name(self.get_config_details(base_dir))
self.assertEqual('simplecomposefile', project_name)

def test_project_name_with_explicit_uppercase_base_dir(self):
base_dir = 'tests/fixtures/UpperCaseDir'
project_name = get_project_name(base_dir)
project_name = get_project_name(self.get_config_details(base_dir))
self.assertEqual('uppercasedir', project_name)

def test_project_name_with_explicit_project_name(self):
name = 'explicit-project-name'
project_name = get_project_name(None, project_name=name)
project_name = get_project_name(self.get_config_details('.'), project_name=name)
self.assertEqual('explicitprojectname', project_name)

@mock.patch.dict(os.environ)
def test_project_name_from_environment_new_var(self):
name = 'namefromenv'
os.environ['COMPOSE_PROJECT_NAME'] = name
project_name = get_project_name(None)
project_name = get_project_name(self.get_config_details('.'))
self.assertEqual(project_name, name)

def test_project_name_with_empty_environment_var(self):
base_dir = 'tests/fixtures/simple-composefile'
with mock.patch.dict(os.environ):
os.environ['COMPOSE_PROJECT_NAME'] = ''
project_name = get_project_name(base_dir)
project_name = get_project_name(self.get_config_details(base_dir))
self.assertEqual('simplecomposefile', project_name)

@mock.patch.dict(os.environ)
Expand All @@ -68,15 +77,51 @@ def test_project_name_with_environment_file(self):
name = 'namefromenvfile'
with open(os.path.join(base_dir, '.env'), 'w') as f:
f.write('COMPOSE_PROJECT_NAME={}'.format(name))
project_name = get_project_name(base_dir)
project_name = get_project_name(self.get_config_details(base_dir))
assert project_name == name

# Environment has priority over .env file
os.environ['COMPOSE_PROJECT_NAME'] = 'namefromenv'
assert get_project_name(base_dir) == os.environ['COMPOSE_PROJECT_NAME']
assert get_project_name(
self.get_config_details(base_dir)) == os.environ['COMPOSE_PROJECT_NAME']
finally:
shutil.rmtree(base_dir)

@mock.patch.dict(os.environ)
def test_project_name_from_config_file(self):
base_dir = 'tests/fixtures/simple-composefile'
config_details = self.get_config_details(base_dir, 'namefromcomposefile')
# Ignored if env switch is unset
assert get_project_name(config_details) == 'simplecomposefile'

# Ignored if env switch is set to falsy value
os.environ['COMPOSE_X_PROJECT_NAME'] = 'false'
assert get_project_name(config_details) == 'simplecomposefile'

# Env switch and no higher precedence value
os.environ['COMPOSE_X_PROJECT_NAME'] = '1'
assert get_project_name(config_details) == 'namefromcomposefile'

# --project-name takes precedence
assert get_project_name(config_details, 'cliname') == 'cliname'

# COMPOSE_PROJECT_NAME env takes precedence
os.environ['COMPOSE_PROJECT_NAME'] = 'namefromenv'
assert get_project_name(config_details) == 'namefromenv'

@mock.patch.dict(os.environ)
def test_project_name_conflict_from_config_file(self):
base_dir = 'tests/fixtures/simple-composefile'
config_details = ConfigDetails(base_dir, [
ConfigFile('base.yml', {'version': '2.3', 'x-project-name': 'foo'}),
ConfigFile('override.yml', {'version': '2.3', 'x-project-name': 'bar'})
])

os.environ['COMPOSE_X_PROJECT_NAME'] = 'true'
with pytest.raises(UserError) as excinfo:
get_project_name(config_details)
assert '"foo" (base.yml) does not match "bar" (override.yml)' in str(excinfo)

def test_get_project(self):
base_dir = 'tests/fixtures/longer-filename-composefile'
project = get_project(base_dir)
Expand Down

0 comments on commit 77a74b6

Please sign in to comment.