diff --git a/azure_functions_devops_build/exceptions.py b/azure_functions_devops_build/exceptions.py new file mode 100644 index 0000000..968cdd2 --- /dev/null +++ b/azure_functions_devops_build/exceptions.py @@ -0,0 +1,7 @@ +class BaseException(Exception): + def __init__(self, message=None): + self.message = message + + +class GitOperationException(BaseException): + pass \ No newline at end of file diff --git a/azure_functions_devops_build/respository/local_git_utils.py b/azure_functions_devops_build/respository/local_git_utils.py new file mode 100644 index 0000000..c8ffdef --- /dev/null +++ b/azure_functions_devops_build/respository/local_git_utils.py @@ -0,0 +1,74 @@ +import re + +# Backward Compatible with Python 2.7 +try: + from subprocess import DEVNULL +except ImportError: + DEVNULL = open(os.devnull, 'w') +from subprocess import STDOUT, check_call, check_output, CalledProcessError +from ..exceptions import GitOperationException + +def does_git_exist(): + try: + check_call("git", stdout=DEVNULL, stderr=STDOUT) + except CalledProcessError: + return False + return True + +def does_git_remote_exist(remote_name): + command = ["git", "remote", "show"] + return remote_name in check_output(command).decode('utf-8') + +def git_init(): + command = ["git", "init"] + try: + check_call(command, stdout=DEVNULL, stderr=STDOUT) + except CalledProcessError: + raise GitOperationException(message=" ".join(command)); + +def git_add_remote(remote_name, remote_url): + command = ["git", "remote", "add", remote_name, remote_url] + try: + check_call(command, stdout=DEVNULL, stderr=STDOUT) + except CalledProcessError: + raise GitOperationException(message=" ".join(command)) + +def git_stage_all(): + command = ["git", "add", "--all"] + try: + check_call(command, stdout=DEVNULL, stderr=STDOUT) + except CalledProcessError: + raise GitOperationException(message=" ".join(command)) + +def git_commit(message): + command = ["git", "commit", "--allow-empty", "--message", message] + try: + check_call(command, stdout=DEVNULL, stderr=STDOUT) + except CalledProcessError: + raise GitOperationException(message=" ".join(command)) + +def git_push(remote_name): + command = ["git", "push", "--all", remote_name] + try: + check_call(command, stdout=DEVNULL, stderr=STDOUT) + except CalledProcessError: + raise GitOperationException(message=" ".join(command)) + +def _sanitize_git_remote_name(organization_name, project_name, repository_name): + concatenated_remote_name = f"{organization_name}_{project_name}_{repository_name}" + sanitized_remote_name = re.sub(r"[^A-Za-z0-9_-]|\s", "-", concatenated_remote_name) + return sanitized_remote_name + +def construct_git_remote_name(organization_name, project_name, repository_name, remote_prefix): + remote_name = "_{prefix}_{name}".format( + prefix=remote_prefix, + name=_sanitized_remote_name(organization_name, project_name, repository_name)) + return remote_name + +def construct_git_remote_url(organization_name, project_name, repository_name, domain_name="dev.azure.com"): + url = "https://{domain}/{org}/{proj}/_git/{repo}".format( + domain=domain_name, + org=organization_name, + proj=project_name, + repo=repository_name) + return url \ No newline at end of file diff --git a/azure_functions_devops_build/respository/models/__init__.py b/azure_functions_devops_build/respository/models/__init__.py index 8072ed1..372ca99 100644 --- a/azure_functions_devops_build/respository/models/__init__.py +++ b/azure_functions_devops_build/respository/models/__init__.py @@ -3,9 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from .repository_response import RepositoryResponse from .github_connection import GithubConnection __all__ = [ - 'RepositoryResponse', 'GithubConnection' ] \ No newline at end of file diff --git a/azure_functions_devops_build/respository/models/repository_response.py b/azure_functions_devops_build/respository/models/repository_response.py deleted file mode 100644 index 630493d..0000000 --- a/azure_functions_devops_build/respository/models/repository_response.py +++ /dev/null @@ -1,10 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -class RepositoryResponse(object): - - def __init__(self, message, succeeded): - self.message = message - self.succeeded = succeeded \ No newline at end of file diff --git a/azure_functions_devops_build/respository/repository_manager.py b/azure_functions_devops_build/respository/repository_manager.py index c6f19a1..ec0a5c0 100644 --- a/azure_functions_devops_build/respository/repository_manager.py +++ b/azure_functions_devops_build/respository/repository_manager.py @@ -3,13 +3,11 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- import os - -# In python2.7 devnull is not defined in subprocess +# Backward Compatible with Python 2.7 try: from subprocess import DEVNULL except ImportError: DEVNULL = open(os.devnull, 'w') - from subprocess import STDOUT, check_call, check_output, CalledProcessError from msrest.service_client import ServiceClient from msrest import Configuration, Deserializer @@ -18,7 +16,16 @@ import vsts.git.v4_1.models.git_repository_create_options as git_repository_crea from ..base.base_manager import BaseManager from . import models - +from .local_git_utils import ( + git_init, + git_add_remote, + git_stage_all, + git_commit, + does_git_exist, + does_git_remote_exist, + construct_git_remote_name, + construct_git_remote_url +) class RepositoryManager(BaseManager): """ Manage DevOps repositories @@ -35,6 +42,9 @@ class RepositoryManager(BaseManager): self._deserialize = Deserializer(client_models) super(RepositoryManager, self).__init__(creds, organization_name=organization_name, project_name=project_name) + def check_if_git_exist(self) -> bool: + return does_git_exist() + def create_repository(self, repository_name): """Create a new azure functions git repository""" project = self._get_project_by_name(self._project_name) @@ -51,58 +61,41 @@ class RepositoryManager(BaseManager): repository = self._get_repository_by_name(project, repository_name) return self._git_client.get_commits(repository.id, None, project=project.id) - def setup_remote(self, repository_name, remote_name): + def get_local_git_remote_name(self, repository_name, remote_prefix): + return construct_git_remote_name(self._organization_name, self._project_name, repository_name, remote_prefix) + + # Since the portal url and remote url are same. We only need one function to handle portal access and git push + def get_azure_devops_repo_url(self, repository_name): + return construct_git_remote_url(self._organization_name, self._project_name, repository_name) + + # Check if the git repository exists first. If it does, check if the git remote exists. + def check_if_local_git_remote_exists(self, repository_name, remote_prefix): + if not self._repository_exists(): + return False + + remote_name = construct_git_remote_name(self._organization_name, self._project_name, repository_name, remote_prefix) + return does_git_remote_exist(remote_name) + + # The function will initialize a git repo, create git remote, stage all changes, commit and push to remote + # Exceptions: GitOperationException + def setup_local_git_repository(self, repository_name, remote_prefix): """This command sets up a remote. It is normally used if a user already has a repository locally that they don't wish to get rid of""" - if self._remote_exists(remote_name): - message = """There is already an remote with this name.""" - succeeded = False - else: - origin_command = ["git", "remote", "add", remote_name, "https://" + self._organization_name + \ - ".visualstudio.com/" + self._project_name + "/_git/" + repository_name] - check_call(origin_command, stdout=DEVNULL, stderr=STDOUT) - check_call('git add -A'.split(), stdout=DEVNULL, stderr=STDOUT) - try: - check_call(["git", "commit", "-a", "-m", "\"creating functions app\""], stdout=DEVNULL, stderr=STDOUT) - except CalledProcessError: - print("no need to commit anything") - check_call(('git push ' + remote_name + ' --all').split(), stdout=DEVNULL, stderr=STDOUT) - message = "succeeded" - succeeded = True - return models.repository_response.RepositoryResponse(message, succeeded) - def setup_repository(self, repository_name): - """This command sets up the repository locally - it initialises the git file and creates the initial push ect""" + remote_name = construct_git_remote_name(self._organization_name, self._project_name, repository_name, remote_prefix) + remote_url = construct_git_remote_url(self._organization_name, self._project_name, repository_name) + if self._repository_exists(): - message = """There is already an existing repository in this folder.""" - succeeded = False - else: - origin_command = ["git", "remote", "add", "origin", "https://" + self._organization_name + \ - ".visualstudio.com/" + self._project_name + "/_git/" + repository_name] - check_call('git init'.split(), stdout=DEVNULL, stderr=STDOUT) - check_call('git add -A'.split(), stdout=DEVNULL, stderr=STDOUT) - check_call(["git", "commit", "-a", "-m", "\"creating functions app\""], stdout=DEVNULL, stderr=STDOUT) - check_call(origin_command, stdout=DEVNULL, stderr=STDOUT) - check_call('git push -u origin --all'.split(), stdout=DEVNULL, stderr=STDOUT) - message = "succeeded" - succeeded = True - return models.repository_response.RepositoryResponse(message, succeeded) + git_init() - def setup_github_repository(self): - check_call('git add -A'.split(), stdout=DEVNULL, stderr=STDOUT) - check_call(["git", "commit", "-a", "-m", "\"creating functions app\""], stdout=DEVNULL, stderr=STDOUT) - check_call('git push'.split(), stdout=DEVNULL, stderr=STDOUT) + git_add_remote(remote_name, remote_url) + git_stage_all() + git_commit("Create function app with azure devops build. Remote repository url: {url}".format(url=remote_url)) + git_push(remote_name) def _repository_exists(self): """Helper to see if gitfile exists""" return bool(os.path.exists('.git')) - def _remote_exists(self, remote_name): - lines = (check_output('git remote show'.split())).decode('utf-8').split('\n') - for line in lines: - if line == remote_name: - return True - return False - def list_github_repositories(self): """List github repositories if there are any from the current connection""" project = self._get_project_by_name(self._project_name) diff --git a/azure_functions_devops_build/service_endpoint/service_endpoint_manager.py b/azure_functions_devops_build/service_endpoint/service_endpoint_manager.py index 2047f9a..e0edfb9 100644 --- a/azure_functions_devops_build/service_endpoint/service_endpoint_manager.py +++ b/azure_functions_devops_build/service_endpoint/service_endpoint_manager.py @@ -44,6 +44,8 @@ class ServiceEndpointManager(BaseManager): return self._service_endpoint_client.create_service_endpoint(service_endpoint, project.id) + # This function requires user permission of Microsoft.Authorization/roleAssignments/write + # i.e. only the owner of the subscription can use this function def create_service_endpoint(self, servicePrincipalName): """Create a new service endpoint within a project with an associated service principal""" project = self._get_project_by_name(self._project_name) @@ -59,7 +61,7 @@ class ServiceEndpointManager(BaseManager): data["scopeLevel"] = "Subscription" # A service principal name has to include the http to be valid - servicePrincipalNameHttp = "http://" + servicePrincipalName + servicePrincipalNameHttp = "https://dev.azure.com/" + servicePrincipalName command = "az ad sp create-for-rbac --o json --name " + servicePrincipalNameHttp token_resp = subprocess.check_output(command, shell=True).decode() token_resp_dict = json.loads(token_resp)