Bug fixes and privacy disable feature for github state

This commit is contained in:
Garrett Heel 2016-09-16 15:01:13 +09:00
parent b433e88d03
commit f1e293c28a
2 changed files with 317 additions and 75 deletions

View File

@ -24,6 +24,10 @@ For example:
# optional: some functions require a repo_name, which
# can be set in the config file, or passed in at the CLI.
repo_name: my_repo
# optional: it can be dangerous to change the privacy of a repository
# in an automated way. set this to True to allow privacy modifications
allow_repo_privacy_changes: False
'''
# Import python libs
@ -81,7 +85,7 @@ def _get_config_value(profile, config_name):
)
config_value = config.get(config_name)
if not config_value:
if config_value is None:
raise CommandExecutionError(
'The \'{0}\' parameter was not found in the \'{1}\' '
'profile.'.format(
@ -106,6 +110,7 @@ def _get_client(profile):
if key not in __context__:
__context__[key] = github.Github(
token,
per_page=100
)
return __context__[key]
@ -121,9 +126,8 @@ def _get_members(organization, params=None):
def _get_repos(profile, params=None, ignore_cache=False):
# Use cache when no params are given
key = 'github.{0}:repos'.format(
_get_config_value(profile, 'org_name')
)
org_name = _get_config_value(profile, 'org_name')
key = 'github.{0}:repos'.format(org_name)
if key not in __context__ or ignore_cache or params is not None:
org_name = _get_config_value(profile, 'org_name')
@ -136,9 +140,22 @@ def _get_repos(profile, params=None, ignore_cache=False):
organization.url + '/repos',
params
)
# Only cache results if no params were given (full scan)
if params is not None:
return result
__context__[key] = list(result) # force evaluation of all pages
next_result = []
for repo in result:
next_result.append(repo)
# Cache a copy of each repo for single lookups
repo_key = "github.{0}:{1}:repo_info".format(org_name, repo.name.lower())
__context__[repo_key] = _repo_to_dict(repo)
__context__[key] = next_result
return __context__[key]
@ -706,7 +723,33 @@ def get_milestone(number=None,
return ret
def get_repo_info(repo_name, profile='github'):
def _repo_to_dict(repo):
ret = {}
ret['id'] = repo.id
ret['name'] = repo.name
ret['full_name'] = repo.full_name
ret['owner'] = repo.owner.login
ret['private'] = repo.private
ret['html_url'] = repo.html_url
ret['description'] = repo.description
ret['fork'] = repo.fork
ret['homepage'] = repo.homepage
ret['size'] = repo.size
ret['stargazers_count'] = repo.stargazers_count
ret['watchers_count'] = repo.watchers_count
ret['language'] = repo.language
ret['open_issues_count'] = repo.open_issues_count
ret['forks'] = repo.forks
ret['open_issues'] = repo.open_issues
ret['watchers'] = repo.watchers
ret['default_branch'] = repo.default_branch
ret['has_issues'] = repo.has_issues
ret['has_wiki'] = repo.has_wiki
ret['has_downloads'] = repo.has_downloads
return ret
def get_repo_info(repo_name, profile='github', ignore_cache=False):
'''
Return information for a given repo.
@ -725,46 +768,80 @@ def get_repo_info(repo_name, profile='github'):
salt myminion github.get_repo_info salt
salt myminion github.get_repo_info salt profile='my-github-profile'
'''
ret = {}
org_name = _get_config_value(profile, 'org_name')
key = "github.{0}:{1}:repo_info".format(
_get_config_value(profile, 'org_name'),
repo_name.lower()
)
if key not in __context__ or ignore_cache:
client = _get_client(profile)
try:
repo = client.get_repo('/'.join([org_name, repo_name]))
if not repo:
return {}
# client.get_repo can return a github.Repository.Repository object,
# even if the repo is invalid. We need to catch the exception when
# we try to perform actions on the repo object, rather than above
# the if statement.
ret = _repo_to_dict(repo)
__context__[key] = ret
except github.UnknownObjectException:
raise CommandExecutionError(
'The \'{0}\' repository under the \'{1}\' organization could not '
'be found.'.format(
repo_name,
org_name
)
)
return __context__[key]
def get_repo_teams(repo_name, profile='github'):
'''
Return teams belonging to a repository.
.. versionadded:: Nitrogen
repo_name
The name of the repository from which to retrieve teams.
profile
The name of the profile configuration to use. Defaults to ``github``.
CLI Example:
.. code-block:: bash
salt myminion github.get_repo_teams salt
salt myminion github.get_repo_teams salt profile='my-github-profile'
'''
ret = []
org_name = _get_config_value(profile, 'org_name')
client = _get_client(profile)
try:
repo = client.get_repo('/'.join([org_name, repo_name]))
if repo:
# client.get_repo can return a github.Repository.Repository object,
# even if the repo is invalid. We need to catch the exception when
# we try to perform actions on the repo object, rather than above
# the if statement.
ret['id'] = repo.id
ret['name'] = repo.name
ret['full_name'] = repo.full_name
ret['owner'] = repo.owner.login
ret['private'] = repo.private
ret['html_url'] = repo.html_url
ret['description'] = repo.description
ret['fork'] = repo.fork
ret['homepage'] = repo.homepage
ret['size'] = repo.size
ret['stargazers_count'] = repo.stargazers_count
ret['watchers_count'] = repo.watchers_count
ret['language'] = repo.language
ret['open_issues_count'] = repo.open_issues_count
ret['forks'] = repo.forks
ret['open_issues'] = repo.open_issues
ret['watchers'] = repo.watchers
ret['default_branch'] = repo.default_branch
ret['has_issues'] = repo.has_issues
ret['has_wiki'] = repo.has_wiki
ret['has_downloads'] = repo.has_downloads
except github.UnknownObjectException:
raise CommandExecutionError(
'The \'{0}\' repository under the \'{1}\' organization could not '
'be found.'.format(
repo_name,
org_name
)
'be found.'.format(repo_name, org_name)
)
try:
teams = repo.get_teams()
for team in teams:
ret.append({
'id': team.id,
'name': team.name,
'permission': team.permission
})
except github.UnknownObjectException:
raise CommandExecutionError(
'Unable to retrieve teams for repository \'{0}\' under the \'{1}\' '
'organization.'.format(repo_name, org_name)
)
return ret
@ -969,6 +1046,17 @@ def edit_repo(name,
.. versionadded:: Carbon
'''
try:
allow_private_change = _get_config_value(profile, 'allow_repo_privacy_changes')
except CommandExecutionError:
allow_private_change = False
if private is not None and not allow_private_change:
raise CommandExecutionError("The private field is set to be changed for "
"repo {0} but allow_repo_privacy_changes "
"disallows this.".format(name))
try:
client = _get_client(profile)
organization = client.get_organization(
@ -994,6 +1082,7 @@ def edit_repo(name,
repo.url,
input=parameters
)
get_repo_info(name, profile=profile, ignore_cache=True) # Refresh cache
return True
except github.GithubException as e:
log.exception('Error editing a repo: {0}'.format(str(e)))
@ -1222,7 +1311,8 @@ def remove_team(name, profile="github"):
def list_team_repos(team_name, profile="github", ignore_cache=False):
'''
Gets the names of team repos in lower case.
Gets the repo details for a given team as a dict from repo_name to repo details.
Note that repo names are always in lower case.
team_name
The name of the team from which to list repos.
@ -1259,14 +1349,25 @@ def list_team_repos(team_name, profile="github", ignore_cache=False):
except UnknownObjectException as e:
log.exception('Resource not found: {0}'.format(cached_team['id']))
try:
cached_team['repos'] = [repo.name.lower() for repo in team.get_repos()]
return cached_team['repos']
repos = {}
for repo in team.get_repos():
permission = 'pull'
if repo.permissions.admin:
permission = 'admin'
elif repo.permissions.push:
permission = 'push'
repos[repo.name.lower()] = {
'permission': permission
}
cached_team['repos'] = repos
return repos
except UnknownObjectException as e:
log.exception('Resource not found: {0}'.format(cached_team['id']))
return []
def add_team_repo(repo_name, team_name, profile="github"):
def add_team_repo(repo_name, team_name, profile="github", permission=None):
'''
Adds a repository to a team with team_name.
@ -1279,6 +1380,13 @@ def add_team_repo(repo_name, team_name, profile="github"):
profile
The name of the profile configuration to use. Defaults to ``github``.
permission
The permission for team members within the repository, can be 'pull',
'push' or 'admin'. If not specified, the default permission specified on
the team will be used.
.. versionadded:: Nitrogen
CLI Example:
.. code-block:: bash
@ -1301,8 +1409,18 @@ def add_team_repo(repo_name, team_name, profile="github"):
except UnknownObjectException as e:
log.exception('Resource not found: {0}'.format(team['id']))
return False
team.add_to_repos(repo)
return repo_name in list_team_repos(team_name, profile=profile, ignore_cache=True)
params = None
if permission is not None:
params = {'permission': permission}
headers, data = team._requester.requestJsonAndCheck(
"PUT",
team.url + "/repos/" + repo._identity,
input=params
)
# Try to refresh cache
list_team_repos(team_name, profile=profile, ignore_cache=True)
return True
def remove_team_repo(repo_name, team_name, profile="github"):
@ -1502,7 +1620,7 @@ def add_team_member(name, team_name, profile="github"):
except github.GithubException as e:
log.exception('Error in adding a member to a team: {0}'.format(str(e)))
return False
return team.has_in_members(member)
return True
def remove_team_member(name, team_name, profile="github"):
@ -1580,12 +1698,16 @@ def list_teams(profile="github", ignore_cache=False):
teams_data = organization.get_teams()
teams = {}
for team in teams_data:
# Note that _rawData is used to access some properties here as they
# are not exposed in older versions of PyGithub. It's VERY important
# to use team._rawData instead of team.raw_data, as the latter forces
# an API call to retrieve team details again.
teams[team.name] = {
'id': team.id,
'slug': team.slug,
'description': team.raw_data['description'],
'description': team._rawData['description'],
'permission': team.permission,
'privacy': team.raw_data['privacy']
'privacy': team._rawData['privacy']
}
__context__[key] = teams

View File

@ -17,12 +17,14 @@ This state is used to ensure presence of users in the Organization.
# Import Python libs
from __future__ import absolute_import
import time
import datetime
import logging
# Import Salt Libs
import salt.ext.six as six
from salt.exceptions import CommandExecutionError
from salt.ext.six.moves import range
log = logging.getLogger(__name__)
@ -254,7 +256,8 @@ def team_present(
return ret
manage_repos = repo_names is not None
current_repos = set(__salt__['github.list_team_repos'](name, profile=profile))
current_repos = set(__salt__['github.list_team_repos'](name, profile=profile)
.keys())
repo_names = set(repo_names or [])
repos_to_add = repo_names - current_repos
@ -473,13 +476,14 @@ def repo_present(
name,
description=None,
homepage=None,
private=False,
has_issues=True,
has_wiki=True,
has_downloads=True,
private=None,
has_issues=None,
has_wiki=None,
has_downloads=None,
auto_init=False,
gitignore_template=None,
license_template=None,
teams=None,
profile="github",
**kwargs):
'''
@ -516,6 +520,12 @@ def repo_present(
license_template
The desired LICENSE template to apply, e.g "mit" or "mozilla".
teams
The teams for which this repo should belong to, specified as a dict of
team name to permission ('pull', 'push' or 'admin').
.. versionadded:: Nitrogen
Example:
.. code-block:: yaml
@ -534,6 +544,11 @@ def repo_present(
'comment': ''
}
# This is an optimization to cache all repos in the organization up front.
# The first use of this state will collect all of the repos and save a bunch
# of API calls for future use.
__salt__['github.list_repos'](profile=profile)
try:
target = __salt__['github.get_repo_info'](name, profile=profile, **kwargs)
except CommandExecutionError:
@ -551,6 +566,9 @@ def repo_present(
'license_template': license_template
}
# Keep track of current_teams if we've fetched them after creating a new repo
current_teams = None
if target: # Repo already exists
# Some params are only valid on repo creation
ignore_params = ['auto_init', 'gitignore_template', 'license_template']
@ -564,45 +582,147 @@ def repo_present(
old_parameters[param_name] = target[param_name]
if len(parameters) > 0:
repo_change = {
'old': 'Repo properties were {0}'.format(old_parameters),
'new': 'Repo properties (that changed) are {0}'.format(parameters)
}
if __opts__['test']:
ret['comment'] = 'Repo properties are set to be edited.'
ret['changes']['repo'] = repo_change
ret['result'] = None
return ret
else:
result = __salt__['github.edit_repo'](name, profile=profile,
**parameters)
if result:
ret['changes']['repo'] = {
'old': 'Repo properties were {0}'.format(old_parameters),
'new': 'Repo properties (that changed) are {0}'.format(parameters)
}
ret['changes']['repo'] = repo_change
else:
ret['result'] = False
ret['comment'] = 'Failed to update repo properties.'
return ret
else: # Repo does not exist - it will be created.
repo_change = {
'old': None,
'new': 'Repo {0} has been created'.format(name)
}
if __opts__['test']:
ret['comment'] = 'Repo {0} is set to be created.'.format(name)
ret['changes']['repo'] = repo_change
ret['result'] = None
return ret
add_params = dict(given_params)
add_params.update(kwargs)
result = __salt__['github.add_repo'](
name,
**add_params
)
if result:
ret['changes']['repo'] = {
'old': None,
'new': 'Repo {0} has been created'.format(name)
}
else:
ret['result'] = False
ret['comment'] = 'Failed to create repo {0}.'.format(name)
return ret
add_params = dict(given_params)
add_params.update(kwargs)
result = __salt__['github.add_repo'](
name,
**add_params
)
if not result:
ret['result'] = False
ret['comment'] = 'Failed to create repo {0}.'.format(name)
return ret
# Turns out that trying to fetch teams for a new repo can 404 immediately
# after repo creation, so this waits until we can fetch teams successfully
# before continuing.
for attempt in range(3):
time.sleep(1)
try:
current_teams = __salt__['github.get_repo_teams'](
name, profile=profile, **kwargs)
break
except CommandExecutionError as e:
log.info("Attempt {0} to fetch new repo {1} failed".format(
attempt, name))
if current_teams is None:
ret['result'] = False
ret['comment'] = 'Failed to verify repo {0} after creation.'.format(name)
return ret
ret['changes']['repo'] = repo_change
if teams is not None:
if __opts__['test'] and not target:
# Assume no teams if we're in test mode and the repo doesn't exist
current_teams = []
elif current_teams is None:
current_teams = __salt__['github.get_repo_teams'](name, profile=profile)
current_team_names = set([t['name'] for t in current_teams])
# First remove any teams that aren't present
for team_name in current_team_names:
if team_name not in teams:
team_change = {
'old': 'Repo {0} is in team {1}'.format(name, team_name),
'new': 'Repo {0} is not in team {1}'.format(name, team_name)
}
if __opts__['test']:
ret['changes'][team_name] = team_change
ret['result'] = None
else:
result = __salt__['github.remove_team_repo'](name, team_name,
profile=profile)
if result:
ret['changes'][team_name] = team_change
else:
ret['result'] = False
ret['comment'] = ('Failed to remove repo {0} from team {1}.'
.format(name, team_name))
return ret
# Next add or modify any necessary teams
for team_name, permission in six.iteritems(teams):
if team_name not in current_team_names: # Need to add repo to team
team_change = {
'old': 'Repo {0} is not in team {1}'.format(name, team_name),
'new': 'Repo {0} is in team {1}'.format(name, team_name)
}
if __opts__['test']:
ret['changes'][team_name] = team_change
ret['result'] = None
else:
result = __salt__['github.add_team_repo'](name, team_name,
permission,
profile=profile)
if result:
ret['changes'][team_name] = team_change
else:
ret['result'] = False
ret['comment'] = ('Failed to remove repo {0} from team {1}.'
.format(name, team_name))
return ret
else:
current_permission = (__salt__['github.list_team_repos']
(team_name, profile=profile)
.get(name.lower(), {})
.get('permission'))
if not current_permission:
ret['result'] = False
ret['comment'] = ('Failed to determine current permission for team '
'{0} in repo {1}'.format(team_name, name))
return ret
elif current_permission != permission:
team_change = {
'old': ('Repo {0} in team {1} has permission {2}'
.format(name, team_name, current_permission)),
'new': ('Repo {0} in team {1} has permission {2}'
.format(name, team_name, permission))
}
if __opts__['test']:
ret['changes'][team_name] = team_change
ret['result'] = None
else:
result = __salt__['github.add_team_repo'](name, team_name,
permission,
profile=profile)
if result:
ret['changes'][team_name] = team_change
else:
ret['result'] = False
ret['comment'] = ('Failed to set permission on repo {0} from '
'team {1} to {2}.'
.format(name, team_name, permission))
return ret
return ret