
This patch set standardizes the Pegleg directory structure because of the following reasons: 1) src/bin/pegleg is not necessary and only makes building (e.g. documentation building) and running of tox targets unnecessarily difficult. 2) src/bin/pegleg is a Java-like standard that bears no relevance to Python. Change-Id: I37d39d3d6186b92f8fbfe234221c9e44da48cf10
429 lines
15 KiB
Python
429 lines
15 KiB
Python
# Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import copy
|
|
import mock
|
|
import yaml
|
|
|
|
import click
|
|
import pytest
|
|
|
|
from pegleg import config
|
|
from pegleg.engine import repository
|
|
from pegleg.engine import util
|
|
|
|
TEST_REPOSITORIES = {
|
|
'repositories': {
|
|
'global': {
|
|
'revision': '843d1a50106e1f17f3f722e2ef1634ae442fe68f',
|
|
'url': 'ssh://REPO_USERNAME@gerrit:29418/aic-clcp-manifests.git'
|
|
},
|
|
'secrets': {
|
|
'revision':
|
|
'master',
|
|
'url': ('ssh://REPO_USERNAME@gerrit:29418/aic-clcp-security-'
|
|
'manifests.git')
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clean_temp_folders():
|
|
try:
|
|
yield
|
|
finally:
|
|
repository._clean_temp_folders()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def stub_out_copy_functionality():
|
|
try:
|
|
mock.patch.object(
|
|
repository,
|
|
'_copy_to_temp_folder',
|
|
autospec=True,
|
|
side_effect=lambda x, *a, **k: x).start()
|
|
yield
|
|
finally:
|
|
mock.patch.stopall()
|
|
|
|
|
|
def _repo_name(repo_url):
|
|
repo_name = repo_url.split('/')[-1]
|
|
if repo_name.endswith('.git'):
|
|
return repo_name[:-4]
|
|
return repo_name
|
|
|
|
|
|
def _test_process_repositories_inner(site_name="test_site",
|
|
expected_extra_repos=None):
|
|
repository.process_repositories(site_name)
|
|
actual_repo_list = config.get_extra_repo_list()
|
|
expected_repos = expected_extra_repos.get('repositories', {})
|
|
|
|
assert len(expected_repos) == len(actual_repo_list)
|
|
for repo in expected_repos.values():
|
|
repo_name = _repo_name(repo['url'])
|
|
assert any(repo_name in r for r in actual_repo_list)
|
|
|
|
|
|
def _test_process_repositories(site_repo=None,
|
|
repo_username=None,
|
|
repo_overrides=None):
|
|
"""Validate :func:`repository.process_repositories`.
|
|
|
|
:param site_repo: Primary site repository.
|
|
:param repo_username: Auth username that replaces REPO_USERNAME.
|
|
:param dict repo_overrides: Overrides with format: -e global=/opt/global,
|
|
keyed with name of override, e.g. global.
|
|
|
|
All params above are mutually exclusive. Can only test one at a time.
|
|
|
|
"""
|
|
|
|
@mock.patch.object(
|
|
util.definition,
|
|
'load_as_params',
|
|
autospec=True,
|
|
return_value=TEST_REPOSITORIES)
|
|
@mock.patch.object(
|
|
util.git, 'is_repository', autospec=True, return_value=True)
|
|
@mock.patch.object(
|
|
repository,
|
|
'_handle_repository',
|
|
autospec=True,
|
|
side_effect=lambda repo_url, *a, **k: _repo_name(repo_url))
|
|
def do_test(m_clone_repo, *_):
|
|
_test_process_repositories_inner(
|
|
expected_extra_repos=TEST_REPOSITORIES)
|
|
|
|
if site_repo:
|
|
# Validate that the primary site repository is cloned, in addition
|
|
# to the extra repositories.
|
|
repo_revision = None
|
|
repo_url = site_repo.rsplit('@', 1)
|
|
if len(repo_url) == 1: # Case: local repo path.
|
|
repo_url = repo_url[0]
|
|
elif len(repo_url) == 2: # Case: remote repo URL.
|
|
repo_url, repo_revision = repo_url
|
|
mock_calls = [
|
|
mock.call(repo_url, ref=repo_revision, auth_key=None)
|
|
]
|
|
mock_calls.extend([
|
|
mock.call(r['url'], ref=r['revision'], auth_key=None)
|
|
for r in TEST_REPOSITORIES['repositories'].values()
|
|
])
|
|
m_clone_repo.assert_has_calls(mock_calls)
|
|
elif repo_username:
|
|
# Validate that the REPO_USERNAME placeholder is replaced by
|
|
# repo_username.
|
|
m_clone_repo.assert_has_calls([
|
|
mock.call(
|
|
r['url'].replace('REPO_USERNAME', repo_username),
|
|
ref=r['revision'],
|
|
auth_key=None)
|
|
for r in TEST_REPOSITORIES['repositories'].values()
|
|
])
|
|
elif repo_overrides:
|
|
# This is computed from: len(cloned extra repos) +
|
|
# len(cloned primary repo), which is len(cloned extra repos) + 1
|
|
expected_call_count = len(TEST_REPOSITORIES['repositories']) + 1
|
|
assert (expected_call_count == m_clone_repo.call_count)
|
|
|
|
for x, r in TEST_REPOSITORIES['repositories'].items():
|
|
if x in repo_overrides:
|
|
ref = None
|
|
repo_url = repo_overrides[x].rsplit('@', 1)
|
|
if len(repo_url) == 1: # Case: local repo path.
|
|
repo_url = repo_url[0]
|
|
elif len(repo_url) == 2: # Case: remote repo URL.
|
|
repo_url, ref = repo_url
|
|
repo_url = repo_url.split('=')[-1]
|
|
else:
|
|
repo_url = r['url']
|
|
ref = r['revision']
|
|
m_clone_repo.assert_any_call(repo_url, ref=ref, auth_key=None)
|
|
else:
|
|
m_clone_repo.assert_has_calls([
|
|
mock.call(r['url'], ref=r['revision'], auth_key=None)
|
|
for r in TEST_REPOSITORIES['repositories'].values()
|
|
])
|
|
|
|
if site_repo:
|
|
# Set a test site repo, call the test and clean up.
|
|
with mock.patch.object(
|
|
config, 'get_site_repo', autospec=True,
|
|
return_value=site_repo):
|
|
do_test()
|
|
elif repo_username:
|
|
# Set a test repo username, call the test and clean up.
|
|
with mock.patch.object(
|
|
config,
|
|
'get_repo_username',
|
|
autospec=True,
|
|
return_value=repo_username):
|
|
do_test()
|
|
elif repo_overrides:
|
|
with mock.patch.object(
|
|
config,
|
|
'get_extra_repo_store',
|
|
autospec=True,
|
|
return_value=list(repo_overrides.values())):
|
|
do_test()
|
|
else:
|
|
do_test()
|
|
|
|
|
|
def test_process_repositories():
|
|
_test_process_repositories()
|
|
|
|
|
|
def test_process_repositories_with_site_repo_url():
|
|
"""Test process_repository when site repo is a remote URL."""
|
|
site_repo = (
|
|
'ssh://REPO_USERNAME@gerrit:29418/aic-clcp-site-manifests.git@333')
|
|
_test_process_repositories(site_repo=site_repo)
|
|
|
|
|
|
def test_process_repositories_handles_local_site_repo_path():
|
|
site_repo = '/opt/aic-clcp-site-manifests'
|
|
_test_process_repositories(site_repo=site_repo)
|
|
|
|
|
|
def test_process_repositories_handles_local_site_repo_path_with_revision():
|
|
site_repo = '/opt/aic-clcp-site-manifests@333'
|
|
_test_process_repositories(site_repo=site_repo)
|
|
|
|
|
|
@mock.patch.object(
|
|
util.definition,
|
|
'load_as_params',
|
|
autospec=True,
|
|
return_value=TEST_REPOSITORIES)
|
|
@mock.patch('os.path.exists', autospec=True, return_value=True)
|
|
@mock.patch.object(
|
|
util.git, 'is_repository', autospec=True, return_value=False)
|
|
def test_process_repositories_with_local_site_path_exists_not_repo(*_):
|
|
"""Validate that when the site repo already exists but isn't a repository
|
|
that an error is raised.
|
|
"""
|
|
with pytest.raises(click.ClickException) as exc:
|
|
_test_process_repositories_inner(
|
|
expected_extra_repos=TEST_REPOSITORIES)
|
|
assert "is not a valid Git repo" in str(exc.value)
|
|
|
|
|
|
def test_process_repositories_with_repo_username():
|
|
_test_process_repositories(repo_username='test_username')
|
|
|
|
|
|
def test_process_repositories_with_repo_overrides_remote_urls():
|
|
# Same URL, different revision (than TEST_REPOSITORIES).
|
|
overrides = {
|
|
'global':
|
|
'global=ssh://REPO_USERNAME@gerrit:29418/aic-clcp-manifests.git@12345'
|
|
}
|
|
_test_process_repositories(repo_overrides=overrides)
|
|
|
|
# Different URL, different revision (than TEST_REPOSITORIES).
|
|
overrides = {
|
|
'global': 'global=https://gerrit/aic-clcp-manifests.git@12345'
|
|
}
|
|
_test_process_repositories(repo_overrides=overrides)
|
|
|
|
|
|
def test_process_repositories_with_repo_overrides_local_paths():
|
|
# Local path without revision.
|
|
overrides = {'global': 'global=/opt/aic-clcp-manifests'}
|
|
_test_process_repositories(repo_overrides=overrides)
|
|
|
|
# Local path with revision.
|
|
overrides = {'global': 'global=/opt/aic-clcp-manifests@12345'}
|
|
_test_process_repositories(repo_overrides=overrides)
|
|
|
|
|
|
def test_process_repositories_with_multiple_repo_overrides_remote_urls():
|
|
overrides = {
|
|
'global':
|
|
'global=ssh://errit:29418/aic-clcp-manifests.git@12345',
|
|
'secrets':
|
|
'secrets=ssh://gerrit:29418/aic-clcp-security-manifests.git@54321'
|
|
}
|
|
_test_process_repositories(repo_overrides=overrides)
|
|
|
|
|
|
def test_process_repositories_with_multiple_repo_overrides_local_paths():
|
|
overrides = {
|
|
'global': 'global=/opt/aic-clcp-manifests@12345',
|
|
'secrets': 'secrets=/opt/aic-clcp-security-manifests.git@54321'
|
|
}
|
|
_test_process_repositories(repo_overrides=overrides)
|
|
|
|
|
|
@mock.patch.object(
|
|
util.definition,
|
|
'load_as_params',
|
|
autospec=True,
|
|
return_value=TEST_REPOSITORIES)
|
|
@mock.patch.object(util.git, 'is_repository', autospec=True, return_value=True)
|
|
@mock.patch.object(
|
|
repository,
|
|
'_handle_repository',
|
|
autospec=True,
|
|
side_effect=lambda repo_url, *a, **k: _repo_name(repo_url))
|
|
@mock.patch.object(repository, 'LOG', autospec=True)
|
|
def test_process_repositiories_extraneous_user_repo_value(m_log, *_):
|
|
repo_overrides = ['global=ssh://gerrit:29418/aic-clcp-manifests.git']
|
|
|
|
# Provide a repo user value.
|
|
with mock.patch.object(
|
|
config,
|
|
'get_repo_username',
|
|
autospec=True,
|
|
return_value='test_username'):
|
|
# Get rid of REPO_USERNAME through an override.
|
|
with mock.patch.object(
|
|
config,
|
|
'get_extra_repo_store',
|
|
autospec=True,
|
|
return_value=repo_overrides):
|
|
_test_process_repositories_inner(
|
|
expected_extra_repos=TEST_REPOSITORIES)
|
|
|
|
msg = ("A repository username was specified but no REPO_USERNAME "
|
|
"string found in repository url %s",
|
|
repo_overrides[0].split('=')[-1])
|
|
m_log.warning.assert_any_call(*msg)
|
|
|
|
|
|
@mock.patch.object(
|
|
util.definition, 'load_as_params', autospec=True,
|
|
return_value={}) # No repositories in site definition.
|
|
@mock.patch.object(util.git, 'is_repository', autospec=True, return_value=True)
|
|
@mock.patch.object(
|
|
repository,
|
|
'_handle_repository',
|
|
autospec=True,
|
|
side_effect=lambda repo_url, *a, **k: _repo_name(repo_url))
|
|
def test_process_repositiories_no_site_def_repos(*_):
|
|
_test_process_repositories_inner(expected_extra_repos={})
|
|
|
|
|
|
@mock.patch.object(
|
|
util.definition, 'load_as_params', autospec=True,
|
|
return_value={}) # No repositories in site definition.
|
|
@mock.patch.object(util.git, 'is_repository', autospec=True, return_value=True)
|
|
@mock.patch.object(
|
|
repository,
|
|
'_handle_repository',
|
|
autospec=True,
|
|
side_effect=lambda repo_url, *a, **k: _repo_name(repo_url))
|
|
@mock.patch.object(repository, 'LOG', autospec=True)
|
|
def test_process_repositiories_no_site_def_repos_with_extraneous_overrides(
|
|
m_log, *_):
|
|
"""Validate that overrides that don't match site-definition entries are
|
|
ignored.
|
|
"""
|
|
site_name = mock.sentinel.site
|
|
repo_overrides = ['global=ssh://gerrit:29418/aic-clcp-manifests.git']
|
|
expected_overrides = {
|
|
'repositories': {
|
|
'global': {
|
|
'revision': '843d1a50106e1f17f3f722e2ef1634ae442fe68f',
|
|
'url': repo_overrides[0]
|
|
}
|
|
}
|
|
}
|
|
|
|
# Provide repo overrides.
|
|
with mock.patch.object(
|
|
config,
|
|
'get_extra_repo_store',
|
|
autospec=True,
|
|
return_value=repo_overrides):
|
|
_test_process_repositories_inner(
|
|
site_name=site_name, expected_extra_repos=expected_overrides)
|
|
|
|
debug_msg = ("Repo override: %s not found under `repositories` for "
|
|
"site-definition.yaml. Site def repositories: %s",
|
|
repo_overrides[0], "")
|
|
info_msg = ("No repositories found in site-definition.yaml for site: %s. "
|
|
"Defaulting to specified repository overrides.", site_name)
|
|
m_log.debug.assert_any_call(*debug_msg)
|
|
m_log.info.assert_any_call(*info_msg)
|
|
|
|
|
|
@mock.patch.object(
|
|
util.definition, 'load_as_params', autospec=True,
|
|
return_value={}) # No repositories in site definition.
|
|
@mock.patch.object(util.git, 'is_repository', autospec=True, return_value=True)
|
|
@mock.patch.object(repository, 'LOG', autospec=True)
|
|
def test_process_repositories_without_repositories_key_in_site_definition(
|
|
m_log, *_):
|
|
# Stub this out since default config site repo is '.' and local repo might
|
|
# be dirty.
|
|
with mock.patch.object(repository, '_handle_repository', autospec=True):
|
|
_test_process_repositories_inner(
|
|
site_name=mock.sentinel.site, expected_extra_repos={})
|
|
msg = ("The repository for site_name: %s does not contain a "
|
|
"site-definition.yaml with a 'repositories' key" % str(
|
|
mock.sentinel.site))
|
|
assert any(msg in x[1][0] for x in m_log.info.mock_calls)
|
|
|
|
|
|
@mock.patch.object(
|
|
util.definition,
|
|
'load_as_params',
|
|
autospec=True,
|
|
return_value=TEST_REPOSITORIES)
|
|
@mock.patch.object(util.git, 'is_repository', autospec=True, return_value=True)
|
|
@mock.patch.object(config, 'get_extra_repo_store', autospec=True)
|
|
def test_process_extra_repositories_malformed_format_raises_exception(
|
|
m_get_extra_repo_store, *_):
|
|
# Will fail since it doesn't contain "=".
|
|
broken_repo_url = 'broken_url'
|
|
m_get_extra_repo_store.return_value = [broken_repo_url]
|
|
error = ("The repository %s must be in the form of "
|
|
"name=repoUrl[@revision]" % broken_repo_url)
|
|
|
|
# Stub this out since default config site repo is '.' and local repo might
|
|
# be dirty.
|
|
with mock.patch.object(repository, '_handle_repository', autospec=True):
|
|
with pytest.raises(click.ClickException) as exc:
|
|
repository.process_repositories(mock.sentinel.site)
|
|
assert error == str(exc.value)
|
|
|
|
|
|
def test_process_site_repository():
|
|
def _do_test(site_repo):
|
|
expected = site_repo.rsplit('@', 1)[0]
|
|
|
|
with mock.patch.object(
|
|
config, 'get_site_repo', autospec=True,
|
|
return_value=site_repo) as mock_get_site_repo:
|
|
|
|
with mock.patch.object(
|
|
repository, '_handle_repository', autospec=True):
|
|
result = repository.process_site_repository()
|
|
assert expected == result
|
|
|
|
# Ensure that the reference is always pruned.
|
|
_do_test('http://github.com/openstack/treasuremap@master')
|
|
_do_test('http://github.com/openstack/treasuremap')
|
|
_do_test('https://github.com/openstack/treasuremap@master')
|
|
_do_test('https://github.com/openstack/treasuremap')
|
|
_do_test('ssh://foo@github.com/openstack/treasuremap:12345@master')
|
|
_do_test('ssh://foo@github.com/openstack/treasuremap:12345')
|