Merge pull request #47975 from terminalmage/issue47937

Add a new git.cloned state
This commit is contained in:
Nicole Thomas 2018-06-27 16:53:41 -04:00 committed by GitHub
commit 67303d7901
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 454 additions and 17 deletions

View file

@ -177,6 +177,18 @@ def _fail(ret, msg, comments=None):
return ret
def _already_cloned(ret, target, branch=None, comments=None):
ret['result'] = True
ret['comment'] = 'Repository already exists at {0}{1}'.format(
target,
' and is checked out to branch \'{0}\''.format(branch) if branch else ''
)
if comments:
ret['comment'] += '\n\nChanges already made: '
ret['comment'] += _format_comments(comments)
return ret
def _failed_fetch(ret, exc, comments=None):
msg = (
'Fetch failed. Set \'force_fetch\' to True to force the fetch if the '
@ -2660,6 +2672,202 @@ def detached(name,
return ret
def cloned(name,
target=None,
branch=None,
user=None,
password=None,
identity=None,
https_user=None,
https_pass=None,
output_encoding=None):
'''
.. versionadded:: 2018.3.2,Fluorine
Ensure that a repository has been cloned to the specified target directory.
If not, clone that repository. No fetches will be performed once cloned.
name
Address of the remote repository
target
Name of the target directory where repository should be cloned
branch
Remote branch to check out. If unspecified, the default branch (i.e.
the one to the remote HEAD points) will be checked out.
.. note::
The local branch name will match the remote branch name. If the
branch name is changed, then that branch will be checked out
locally, but keep in mind that remote repository will not be
fetched. If your use case requires that you keep the clone up to
date with the remote repository, then consider using
:py:func:`git.latest <salt.states.git.latest>`.
user
User under which to run git commands. By default, commands are run by
the user under which the minion is running.
password
Windows only. Required when specifying ``user``. This parameter will be
ignored on non-Windows platforms.
identity
Path to a private key to use for ssh URLs. Works the same way as in
:py:func:`git.latest <salt.states.git.latest>`, see that state's
documentation for more information.
https_user
HTTP Basic Auth username for HTTPS (only) clones
https_pass
HTTP Basic Auth password for HTTPS (only) clones
output_encoding
Use this option to specify which encoding to use to decode the output
from any git commands which are run. This should not be needed in most
cases.
.. note::
This should only be needed if the files in the repository were
created with filenames using an encoding other than UTF-8 to handle
Unicode characters.
'''
ret = {'name': name, 'result': False, 'comment': '', 'changes': {}}
if target is None:
ret['comment'] = '\'target\' argument is required'
return ret
elif not isinstance(target, six.string_types):
target = six.text_type(target)
if not os.path.isabs(target):
ret['comment'] = '\'target\' path must be absolute'
return ret
if branch is not None:
if not isinstance(branch, six.string_types):
branch = six.text_type(branch)
if not branch:
ret['comment'] = 'Invalid \'branch\' argument'
return ret
if not os.path.exists(target):
need_clone = True
else:
try:
__salt__['git.status'](target,
user=user,
password=password,
output_encoding=output_encoding)
except Exception as exc:
ret['comment'] = six.text_type(exc)
return ret
else:
need_clone = False
comments = []
def _clone_changes(ret):
ret['changes']['new'] = name + ' => ' + target
def _branch_changes(ret, old, new):
ret['changes']['branch'] = {'old': old, 'new': new}
if need_clone:
if __opts__['test']:
_clone_changes(ret)
comment = '{0} would be cloned to {1}{2}'.format(
name,
target,
' with branch \'{0}\''.format(branch)
if branch is not None
else ''
)
return _neutral_test(ret, comment)
clone_opts = ['--branch', branch] if branch is not None else None
try:
__salt__['git.clone'](target,
name,
opts=clone_opts,
user=user,
password=password,
identity=identity,
https_user=https_user,
https_pass=https_pass,
output_encoding=output_encoding)
except CommandExecutionError as exc:
msg = 'Clone failed: {0}'.format(_strip_exc(exc))
return _fail(ret, msg, comments)
comments.append(
'{0} cloned to {1}{2}'.format(
name,
target,
' with branch \'{0}\''.format(branch)
if branch is not None
else ''
)
)
_clone_changes(ret)
ret['comment'] = _format_comments(comments)
ret['result'] = True
return ret
else:
if branch is None:
return _already_cloned(ret, target, branch, comments)
else:
current_branch = __salt__['git.current_branch'](
target,
user=user,
password=password,
output_encoding=output_encoding)
if current_branch == branch:
return _already_cloned(ret, target, branch, comments)
else:
if __opts__['test']:
_branch_changes(ret, current_branch, branch)
return _neutral_test(
ret,
'Branch would be changed to \'{0}\''.format(branch))
try:
__salt__['git.rev_parse'](
target,
rev=branch,
user=user,
password=password,
ignore_retcode=True,
output_encoding=output_encoding)
except CommandExecutionError:
# Local head does not exist, so we need to check out a new
# branch at the remote rev
checkout_rev = '/'.join(('origin', branch))
checkout_opts = ['-b', branch]
else:
# Local head exists, so we just need to check it out
checkout_rev = branch
checkout_opts = None
try:
__salt__['git.checkout'](
target,
rev=checkout_rev,
opts=checkout_opts,
user=user,
password=password,
output_encoding=output_encoding)
except CommandExecutionError as exc:
msg = 'Failed to change branch to \'{0}\': {1}'.format(branch, exc)
return _fail(ret, msg, comments)
else:
comments.append('Branch changed to \'{0}\''.format(branch))
_branch_changes(ret, current_branch, branch)
ret['comment'] = _format_comments(comments)
ret['result'] = True
return ret
def config_unset(name,
value_regex=None,
repo=None,

View file

@ -81,15 +81,16 @@ class GitTest(ModuleCase, SaltReturnAssertsMixin):
Validate the git state
'''
def setUp(self):
self.__domain = 'github.com'
domain = 'github.com'
self.test_repo = 'https://{0}/saltstack/salt-test-repo.git'.format(domain)
try:
if hasattr(socket, 'setdefaulttimeout'):
# 10 second dns timeout
socket.setdefaulttimeout(10)
socket.gethostbyname(self.__domain)
socket.gethostbyname(domain)
except socket.error:
msg = 'error resolving {0}, possible network issue?'
self.skipTest(msg.format(self.__domain))
self.skipTest(msg.format(domain))
def tearDown(self):
# Reset the dns timeout after the test is over
@ -102,7 +103,7 @@ class GitTest(ModuleCase, SaltReturnAssertsMixin):
'''
ret = self.run_state(
'git.latest',
name='https://{0}/saltstack/salt-test-repo.git'.format(self.__domain),
name=self.test_repo,
target=target
)
self.assertSaltTrueReturn(ret)
@ -115,7 +116,7 @@ class GitTest(ModuleCase, SaltReturnAssertsMixin):
'''
ret = self.run_state(
'git.latest',
name='https://{0}/saltstack/salt-test-repo.git'.format(self.__domain),
name=self.test_repo,
rev='develop',
target=target,
submodules=True
@ -145,7 +146,7 @@ class GitTest(ModuleCase, SaltReturnAssertsMixin):
'''
ret = self.run_state(
'git.latest',
name='https://{0}/saltstack/salt-test-repo.git'.format(self.__domain),
name=self.test_repo,
rev='develop',
target=target,
submodules=True
@ -161,7 +162,7 @@ class GitTest(ModuleCase, SaltReturnAssertsMixin):
'''
ret = self.run_state(
'git.latest',
name='https://{0}/saltstack/salt-test-repo.git'.format(self.__domain),
name=self.test_repo,
rev='develop',
target=target,
unless='test -e {0}'.format(target),
@ -177,7 +178,7 @@ class GitTest(ModuleCase, SaltReturnAssertsMixin):
'''
ret = self.run_state(
'git.latest',
name='https://{0}/saltstack/salt-test-repo.git'.format(self.__domain),
name=self.test_repo,
rev=0.11,
target=target,
submodules=True,
@ -195,7 +196,7 @@ class GitTest(ModuleCase, SaltReturnAssertsMixin):
# Clone repo
ret = self.run_state(
'git.latest',
name='https://{0}/saltstack/salt-test-repo.git'.format(self.__domain),
name=self.test_repo,
target=target
)
self.assertSaltTrueReturn(ret)
@ -211,7 +212,7 @@ class GitTest(ModuleCase, SaltReturnAssertsMixin):
# Re-run state with force_reset=False
ret = self.run_state(
'git.latest',
name='https://{0}/saltstack/salt-test-repo.git'.format(self.__domain),
name=self.test_repo,
target=target,
force_reset=False
)
@ -225,7 +226,7 @@ class GitTest(ModuleCase, SaltReturnAssertsMixin):
# Now run the state with force_reset=True
ret = self.run_state(
'git.latest',
name='https://{0}/saltstack/salt-test-repo.git'.format(self.__domain),
name=self.test_repo,
target=target,
force_reset=True
)
@ -246,12 +247,11 @@ class GitTest(ModuleCase, SaltReturnAssertsMixin):
def _head(cwd):
return self.run_function('git.rev_parse', [cwd, 'HEAD'])
repo_url = 'https://{0}/saltstack/salt-test-repo.git'.format(self.__domain)
mirror_url = 'file://' + mirror_dir
# Mirror the repo
self.run_function(
'git.clone', [mirror_dir], url=repo_url, opts='--mirror')
'git.clone', [mirror_dir], url=self.test_repo, opts='--mirror')
# Make sure the directory for the mirror now exists
self.assertTrue(os.path.exists(mirror_dir))
@ -299,7 +299,7 @@ class GitTest(ModuleCase, SaltReturnAssertsMixin):
# Clone repo
ret = self.run_state(
'git.latest',
name='https://{0}/saltstack/salt-test-repo.git'.format(self.__domain),
name=self.test_repo,
rev=rev,
target=target
)
@ -320,7 +320,7 @@ class GitTest(ModuleCase, SaltReturnAssertsMixin):
# comment field.
ret = self.run_state(
'git.latest',
name='https://{0}/saltstack/salt-test-repo.git'.format(self.__domain),
name=self.test_repo,
rev=rev,
target=target
)
@ -409,7 +409,7 @@ class GitTest(ModuleCase, SaltReturnAssertsMixin):
'''
ret = self.run_state(
'git.latest',
name='https://{0}/saltstack/salt-test-repo.git'.format(self.__domain),
name=self.test_repo,
rev='HEAD',
target=target,
depth=1
@ -423,7 +423,7 @@ class GitTest(ModuleCase, SaltReturnAssertsMixin):
ret = self.run_state(
'git.latest',
name='https://{0}/saltstack/salt-test-repo.git'.format(self.__domain),
name=self.test_repo,
rev='non-default-branch',
target=target,
depth=1
@ -431,6 +431,235 @@ class GitTest(ModuleCase, SaltReturnAssertsMixin):
self.assertSaltTrueReturn(ret)
self.assertTrue(os.path.isdir(os.path.join(target, '.git')))
@with_tempdir(create=False)
def test_cloned(self, target):
'''
Test git.cloned state
'''
# Test mode
ret = self.run_state(
'git.cloned',
name=self.test_repo,
target=target,
test=True)
ret = ret[next(iter(ret))]
assert ret['result'] is None
assert ret['changes'] == {
'new': '{0} => {1}'.format(self.test_repo, target)
}
assert ret['comment'] == '{0} would be cloned to {1}'.format(
self.test_repo,
target
)
# Now actually run the state
ret = self.run_state(
'git.cloned',
name=self.test_repo,
target=target)
ret = ret[next(iter(ret))]
assert ret['result'] is True
assert ret['changes'] == {
'new': '{0} => {1}'.format(self.test_repo, target)
}
assert ret['comment'] == '{0} cloned to {1}'.format(self.test_repo, target)
# Run the state again to test idempotence
ret = self.run_state(
'git.cloned',
name=self.test_repo,
target=target)
ret = ret[next(iter(ret))]
assert ret['result'] is True
assert not ret['changes']
assert ret['comment'] == 'Repository already exists at {0}'.format(target)
# Run the state again to test idempotence (test mode)
ret = self.run_state(
'git.cloned',
name=self.test_repo,
target=target,
test=True)
ret = ret[next(iter(ret))]
assert not ret['changes']
assert ret['result'] is True
assert ret['comment'] == 'Repository already exists at {0}'.format(target)
@with_tempdir(create=False)
def test_cloned_with_branch(self, target):
'''
Test git.cloned state with branch provided
'''
old_branch = 'master'
new_branch = 'develop'
bad_branch = 'thisbranchdoesnotexist'
# Test mode
ret = self.run_state(
'git.cloned',
name=self.test_repo,
target=target,
branch=old_branch,
test=True)
ret = ret[next(iter(ret))]
assert ret['result'] is None
assert ret['changes'] == {
'new': '{0} => {1}'.format(self.test_repo, target)
}
assert ret['comment'] == (
'{0} would be cloned to {1} with branch \'{2}\''.format(
self.test_repo,
target,
old_branch
)
)
# Now actually run the state
ret = self.run_state(
'git.cloned',
name=self.test_repo,
target=target,
branch=old_branch)
ret = ret[next(iter(ret))]
assert ret['result'] is True
assert ret['changes'] == {
'new': '{0} => {1}'.format(self.test_repo, target)
}
assert ret['comment'] == (
'{0} cloned to {1} with branch \'{2}\''.format(
self.test_repo,
target,
old_branch
)
)
# Run the state again to test idempotence
ret = self.run_state(
'git.cloned',
name=self.test_repo,
target=target,
branch=old_branch)
ret = ret[next(iter(ret))]
assert ret['result'] is True
assert not ret['changes']
assert ret['comment'] == (
'Repository already exists at {0} '
'and is checked out to branch \'{1}\''.format(target, old_branch)
)
# Run the state again to test idempotence (test mode)
ret = self.run_state(
'git.cloned',
name=self.test_repo,
target=target,
test=True,
branch=old_branch)
ret = ret[next(iter(ret))]
assert ret['result'] is True
assert not ret['changes']
assert ret['comment'] == (
'Repository already exists at {0} '
'and is checked out to branch \'{1}\''.format(target, old_branch)
)
# Change branch (test mode)
ret = self.run_state(
'git.cloned',
name=self.test_repo,
target=target,
branch=new_branch,
test=True)
ret = ret[next(iter(ret))]
assert ret['result'] is None
assert ret['changes'] == {
'branch': {'old': old_branch, 'new': new_branch}
}
assert ret['comment'] == 'Branch would be changed to \'{0}\''.format(
new_branch
)
# Now really change the branch
ret = self.run_state(
'git.cloned',
name=self.test_repo,
target=target,
branch=new_branch)
ret = ret[next(iter(ret))]
assert ret['result'] is True
assert ret['changes'] == {
'branch': {'old': old_branch, 'new': new_branch}
}
assert ret['comment'] == 'Branch changed to \'{0}\''.format(
new_branch
)
# Change back to original branch. This tests that we don't attempt to
# checkout a new branch (i.e. git checkout -b) for a branch that exists
# locally, as that would fail.
ret = self.run_state(
'git.cloned',
name=self.test_repo,
target=target,
branch=old_branch)
ret = ret[next(iter(ret))]
assert ret['result'] is True
assert ret['changes'] == {
'branch': {'old': new_branch, 'new': old_branch}
}
assert ret['comment'] == 'Branch changed to \'{0}\''.format(
old_branch
)
# Test switching to a nonexistant branch. This should fail.
ret = self.run_state(
'git.cloned',
name=self.test_repo,
target=target,
branch=bad_branch)
ret = ret[next(iter(ret))]
assert ret['result'] is False
assert not ret['changes']
assert ret['comment'].startswith(
'Failed to change branch to \'{0}\':'.format(bad_branch)
)
@with_tempdir(create=False)
def test_cloned_with_nonexistant_branch(self, target):
'''
Test git.cloned state with a nonexistant branch provided
'''
branch = 'thisbranchdoesnotexist'
# Test mode
ret = self.run_state(
'git.cloned',
name=self.test_repo,
target=target,
branch=branch,
test=True)
ret = ret[next(iter(ret))]
assert ret['result'] is None
assert ret['changes']
assert ret['comment'] == (
'{0} would be cloned to {1} with branch \'{2}\''.format(
self.test_repo,
target,
branch
)
)
# Now actually run the state
ret = self.run_state(
'git.cloned',
name=self.test_repo,
target=target,
branch=branch)
ret = ret[next(iter(ret))]
assert ret['result'] is False
assert not ret['changes']
assert ret['comment'].startswith('Clone failed:')
assert 'not found in upstream origin' in ret['comment']
@with_tempdir(create=False)
def test_present(self, name):
'''