1# -------------------------------------------------------------------------------------------- 2# Copyright (c) Microsoft Corporation. All rights reserved. 3# Licensed under the MIT License. See License.txt in the project root for license information. 4# -------------------------------------------------------------------------------------------- 5 6import os 7import time 8import json 9import logging 10from knack.prompting import prompt_choice_list, prompt_y_n, prompt 11from knack.util import CLIError 12from azure_functions_devops_build.constants import ( 13 LINUX_CONSUMPTION, 14 LINUX_DEDICATED, 15 WINDOWS, 16 PYTHON, 17 NODE, 18 DOTNET, 19 POWERSHELL 20) 21from azure_functions_devops_build.exceptions import ( 22 GitOperationException, 23 RoleAssignmentException, 24 LanguageNotSupportException, 25 BuildErrorException, 26 ReleaseErrorException, 27 GithubContentNotFound, 28 GithubUnauthorizedError, 29 GithubIntegrationRequestError 30) 31from .azure_devops_build_provider import AzureDevopsBuildProvider 32from .custom import list_function_app, show_webapp, get_app_settings 33from .utils import str2bool 34 35# pylint: disable=too-many-instance-attributes,too-many-public-methods 36 37SUPPORTED_SCENARIOS = ['AZURE_DEVOPS', 'GITHUB_INTEGRATION'] 38SUPPORTED_SOURCECODE_LOCATIONS = ['Current Directory', 'Github'] 39SUPPORTED_LANGUAGES = { 40 'python': PYTHON, 41 'node': NODE, 42 'dotnet': DOTNET, 43 'powershell': POWERSHELL, 44} 45BUILD_QUERY_FREQUENCY = 5 # sec 46RELEASE_COMPOSITION_DELAY = 1 # sec 47 48 49class AzureDevopsBuildInteractive(object): 50 """Implement the basic user flow for a new user wanting to do an Azure DevOps build for Azure Functions 51 52 Attributes: 53 cmd : the cmd input from the command line 54 logger : a knack logger to log the info/error messages 55 """ 56 57 def __init__(self, cmd, logger, functionapp_name, organization_name, project_name, repository_name, 58 overwrite_yaml, allow_force_push, github_pat, github_repository): 59 self.adbp = AzureDevopsBuildProvider(cmd.cli_ctx) 60 self.cmd = cmd 61 self.logger = logger 62 self.cmd_selector = CmdSelectors(cmd, logger, self.adbp) 63 self.functionapp_name = functionapp_name 64 self.storage_name = None 65 self.resource_group_name = None 66 self.functionapp_language = None 67 self.functionapp_type = None 68 self.organization_name = organization_name 69 self.project_name = project_name 70 self.repository_name = repository_name 71 72 self.github_pat = github_pat 73 self.github_repository = github_repository 74 self.github_service_endpoint_name = None 75 76 self.repository_remote_name = None 77 self.service_endpoint_name = None 78 self.build_definition_name = None 79 self.release_definition_name = None 80 self.build_pool_name = "Default" 81 self.release_pool_name = "Hosted VS2017" 82 self.artifact_name = "drop" 83 84 self.settings = [] 85 self.build = None 86 # These are used to tell if we made new objects 87 self.scenario = None # see SUPPORTED_SCENARIOS 88 self.created_organization = False 89 self.created_project = False 90 self.overwrite_yaml = str2bool(overwrite_yaml) 91 self.allow_force_push = allow_force_push 92 93 def interactive_azure_devops_build(self): 94 """Main interactive flow which is the only function that should be used outside of this 95 class (the rest are helpers)""" 96 97 scenario = self.check_scenario() 98 if scenario == 'AZURE_DEVOPS': 99 return self.azure_devops_flow() 100 if scenario == 'GITHUB_INTEGRATION': 101 return self.github_flow() 102 raise CLIError('Unknown scenario') 103 104 def azure_devops_flow(self): 105 self.process_functionapp() 106 self.pre_checks_azure_devops() 107 self.process_organization() 108 self.process_project() 109 self.process_yaml_local() 110 self.process_local_repository() 111 self.process_remote_repository() 112 self.process_functionapp_service_endpoint('AZURE_DEVOPS') 113 self.process_extensions() 114 self.process_build_and_release_definition_name('AZURE_DEVOPS') 115 self.process_build('AZURE_DEVOPS') 116 self.wait_for_build() 117 self.process_release() 118 self.logger.warning("Pushing your code to {remote}:master will now trigger another build.".format( 119 remote=self.repository_remote_name 120 )) 121 return { 122 'source_location': 'local', 123 'functionapp_name': self.functionapp_name, 124 'storage_name': self.storage_name, 125 'resource_group_name': self.resource_group_name, 126 'functionapp_language': self.functionapp_language, 127 'functionapp_type': self.functionapp_type, 128 'organization_name': self.organization_name, 129 'project_name': self.project_name, 130 'repository_name': self.repository_name, 131 'service_endpoint_name': self.service_endpoint_name, 132 'build_definition_name': self.build_definition_name, 133 'release_definition_name': self.release_definition_name 134 } 135 136 def github_flow(self): 137 self.process_github_personal_access_token() 138 self.process_github_repository() 139 self.process_functionapp() 140 self.pre_checks_github() 141 self.process_organization() 142 self.process_project() 143 self.process_yaml_github() 144 self.process_functionapp_service_endpoint('GITHUB_INTEGRATION') 145 self.process_github_service_endpoint() 146 self.process_extensions() 147 self.process_build_and_release_definition_name('GITHUB_INTEGRATION') 148 self.process_build('GITHUB_INTEGRATION') 149 self.wait_for_build() 150 self.process_release() 151 self.logger.warning("Setup continuous integration between {github_repo} and Azure Pipelines".format( 152 github_repo=self.github_repository 153 )) 154 return { 155 'source_location': 'Github', 156 'functionapp_name': self.functionapp_name, 157 'storage_name': self.storage_name, 158 'resource_group_name': self.resource_group_name, 159 'functionapp_language': self.functionapp_language, 160 'functionapp_type': self.functionapp_type, 161 'organization_name': self.organization_name, 162 'project_name': self.project_name, 163 'repository_name': self.github_repository, 164 'service_endpoint_name': self.service_endpoint_name, 165 'build_definition_name': self.build_definition_name, 166 'release_definition_name': self.release_definition_name 167 } 168 169 def check_scenario(self): 170 if self.repository_name: 171 self.scenario = 'AZURE_DEVOPS' 172 elif self.github_pat or self.github_repository: 173 self.scenario = 'GITHUB_INTEGRATION' 174 else: 175 choice_index = prompt_choice_list( 176 'Please choose Azure function source code location: ', 177 SUPPORTED_SOURCECODE_LOCATIONS 178 ) 179 self.scenario = SUPPORTED_SCENARIOS[choice_index] 180 return self.scenario 181 182 def pre_checks_azure_devops(self): 183 if not AzureDevopsBuildProvider.check_git(): 184 raise CLIError("The program requires git source control to operate, please install git.") 185 186 if not os.path.exists('host.json'): 187 raise CLIError("There is no host.json in the current directory.{ls}" 188 "Functionapps must contain a host.json in their root".format(ls=os.linesep)) 189 190 if not os.path.exists('local.settings.json'): 191 raise CLIError("There is no local.settings.json in the current directory.{ls}" 192 "Functionapps must contain a local.settings.json in their root".format(ls=os.linesep)) 193 194 try: 195 local_runtime_language = self._find_local_repository_runtime_language() 196 except LanguageNotSupportException as lnse: 197 raise CLIError("Sorry, currently we do not support {language}.".format(language=lnse.message)) 198 199 if local_runtime_language != self.functionapp_language: 200 raise CLIError("The language stack setting found in your local repository ({setting}) does not match " 201 "the language stack of your Azure function app in Azure ({functionapp}).{ls}" 202 "Please look at the FUNCTIONS_WORKER_RUNTIME setting both in your local.settings.json file " 203 "and in your Azure function app's application settings, " 204 "and ensure they match.".format( 205 setting=local_runtime_language, 206 functionapp=self.functionapp_language, 207 ls=os.linesep, 208 )) 209 210 def pre_checks_github(self): 211 if not AzureDevopsBuildProvider.check_github_file(self.github_pat, self.github_repository, 'host.json'): 212 raise CLIError("There is no host.json file in the provided Github repository {repo}.{ls}" 213 "Each function app must contain a host.json in their root.{ls}" 214 "Please ensure you have read permission to the repository.".format( 215 repo=self.github_repository, 216 ls=os.linesep, 217 )) 218 try: 219 github_runtime_language = self._find_github_repository_runtime_language() 220 except LanguageNotSupportException as lnse: 221 raise CLIError("Sorry, currently we do not support {language}.".format(language=lnse.message)) 222 223 if github_runtime_language is not None and github_runtime_language != self.functionapp_language: 224 raise CLIError("The language stack setting found in the provided repository ({setting}) does not match " 225 "the language stack of your Azure function app ({functionapp}).{ls}" 226 "Please look at the FUNCTIONS_WORKER_RUNTIME setting both in your local.settings.json file " 227 "and in your Azure function app's application settings, " 228 "and ensure they match.".format( 229 setting=github_runtime_language, 230 functionapp=self.functionapp_language, 231 ls=os.linesep, 232 )) 233 234 def process_functionapp(self): 235 """Helper to retrieve information about a functionapp""" 236 if self.functionapp_name is None: 237 functionapp = self._select_functionapp() 238 self.functionapp_name = functionapp.name 239 else: 240 functionapp = self.cmd_selector.cmd_functionapp(self.functionapp_name) 241 242 kinds = show_webapp(self.cmd, functionapp.resource_group, functionapp.name).kind.split(',') 243 244 # Get functionapp settings in Azure 245 app_settings = get_app_settings(self.cmd, functionapp.resource_group, functionapp.name) 246 247 self.resource_group_name = functionapp.resource_group 248 self.functionapp_type = self._find_type(kinds) 249 250 try: 251 self.functionapp_language = self._get_functionapp_runtime_language(app_settings) 252 self.storage_name = self._get_functionapp_storage_name(app_settings) 253 except LanguageNotSupportException as lnse: 254 raise CLIError("Sorry, currently we do not support {language}.".format(language=lnse.message)) 255 256 def process_organization(self): 257 """Helper to retrieve information about an organization / create a new one""" 258 if self.organization_name is None: 259 response = prompt_y_n('Would you like to use an existing Azure DevOps organization? ') 260 if response: 261 self._select_organization() 262 else: 263 self._create_organization() 264 self.created_organization = True 265 else: 266 self.cmd_selector.cmd_organization(self.organization_name) 267 268 def process_project(self): 269 """Helper to retrieve information about a project / create a new one""" 270 # There is a new organization so a new project will be needed 271 if (self.project_name is None) and (self.created_organization): 272 self._create_project() 273 elif self.project_name is None: 274 use_existing_project = prompt_y_n('Would you like to use an existing Azure DevOps project? ') 275 if use_existing_project: 276 self._select_project() 277 else: 278 self._create_project() 279 else: 280 self.cmd_selector.cmd_project(self.organization_name, self.project_name) 281 282 self.logger.warning("To view your Azure DevOps project, " 283 "please visit https://dev.azure.com/{org}/{proj}".format( 284 org=self.organization_name, 285 proj=self.project_name 286 )) 287 288 def process_yaml_local(self): 289 """Helper to create the local azure-pipelines.yml file""" 290 291 if os.path.exists('azure-pipelines.yml'): 292 if self.overwrite_yaml is None: 293 self.logger.warning("There is already an azure-pipelines.yml file in your local repository.") 294 self.logger.warning("If you are using a yaml file that was not configured " 295 "through this command, this command may fail.") 296 response = prompt_y_n("Do you want to delete it and create a new one? ") 297 else: 298 response = self.overwrite_yaml 299 300 if (not os.path.exists('azure-pipelines.yml')) or response: 301 self.logger.warning('Creating a new azure-pipelines.yml') 302 try: 303 self.adbp.create_yaml(self.functionapp_language, self.functionapp_type) 304 except LanguageNotSupportException as lnse: 305 raise CLIError("Sorry, currently we do not support {language}.".format(language=lnse.message)) 306 307 def process_yaml_github(self): 308 does_yaml_file_exist = AzureDevopsBuildProvider.check_github_file( 309 self.github_pat, 310 self.github_repository, 311 "azure-pipelines.yml" 312 ) 313 if does_yaml_file_exist and self.overwrite_yaml is None: 314 self.logger.warning("There is already an azure-pipelines.yml file in the provided Github repository.") 315 self.logger.warning("If you are using a yaml file that was not configured " 316 "through this command, this command may fail.") 317 self.overwrite_yaml = prompt_y_n("Do you want to generate a new one? " 318 "(It will be committed to the master branch of the provided repository)") 319 320 # Create and commit the new yaml file to Github without asking 321 if not does_yaml_file_exist: 322 if self.github_repository: 323 self.logger.warning("Creating a new azure-pipelines.yml for Github repository") 324 try: 325 AzureDevopsBuildProvider.create_github_yaml( 326 pat=self.github_pat, 327 language=self.functionapp_language, 328 app_type=self.functionapp_type, 329 repository_fullname=self.github_repository 330 ) 331 except LanguageNotSupportException as lnse: 332 raise CLIError("Sorry, currently this command does not support {language}. To proceed, " 333 "you'll need to configure your build manually at dev.azure.com".format( 334 language=lnse.message)) 335 except GithubContentNotFound: 336 raise CLIError("Sorry, the repository you provided does not exist or " 337 "you do not have sufficient permissions to write to the repository. " 338 "Please provide an access token with the proper permissions.") 339 except GithubUnauthorizedError: 340 raise CLIError("Sorry, you do not have sufficient permissions to commit " 341 "azure-pipelines.yml to your Github repository.") 342 343 # Overwrite yaml file 344 if does_yaml_file_exist and self.overwrite_yaml: 345 self.logger.warning("Overwrite azure-pipelines.yml file in the provided Github repository") 346 try: 347 AzureDevopsBuildProvider.create_github_yaml( 348 pat=self.github_pat, 349 language=self.functionapp_language, 350 app_type=self.functionapp_type, 351 repository_fullname=self.github_repository, 352 overwrite=True 353 ) 354 except LanguageNotSupportException as lnse: 355 raise CLIError("Sorry, currently this command does not support {language}. To proceed, " 356 "you'll need to configure your build manually at dev.azure.com".format( 357 language=lnse.message)) 358 except GithubContentNotFound: 359 raise CLIError("Sorry, the repository you provided does not exist or " 360 "you do not have sufficient permissions to write to the repository. " 361 "Please provide an access token with the proper permissions.") 362 except GithubUnauthorizedError: 363 raise CLIError("Sorry, you do not have sufficient permissions to overwrite " 364 "azure-pipelines.yml in your Github repository.") 365 366 def process_local_repository(self): 367 has_local_git_repository = AzureDevopsBuildProvider.check_git_local_repository() 368 if has_local_git_repository: 369 self.logger.warning("Detected a local Git repository already exists.") 370 371 # Collect repository name on Azure Devops 372 if not self.repository_name: 373 self.repository_name = prompt("Push to which Azure DevOps repository (default: {repo}): ".format( 374 repo=self.project_name 375 )) 376 if not self.repository_name: # Select default value 377 self.repository_name = self.project_name 378 379 expected_remote_name = self.adbp.get_local_git_remote_name( 380 self.organization_name, 381 self.project_name, 382 self.repository_name 383 ) 384 expected_remote_url = self.adbp.get_azure_devops_repo_url( 385 self.organization_name, 386 self.project_name, 387 self.repository_name 388 ) 389 390 # If local repository already has a remote 391 # Let the user to know s/he can push to the remote directly for context update 392 # Or let s/he remove the git remote manually 393 has_local_git_remote = self.adbp.check_git_remote( 394 self.organization_name, 395 self.project_name, 396 self.repository_name 397 ) 398 if has_local_git_remote: 399 raise CLIError("There's a git remote bound to {url}.{ls}" 400 "To update the repository and trigger an Azure Pipelines build, please use " 401 "'git push {remote} master'".format( 402 url=expected_remote_url, 403 remote=expected_remote_name, 404 ls=os.linesep) 405 ) 406 407 # Setup a local git repository and create a new commit on top of this context 408 try: 409 self.adbp.setup_local_git_repository(self.organization_name, self.project_name, self.repository_name) 410 except GitOperationException as goe: 411 raise CLIError("Failed to setup local git repository when running '{message}'{ls}" 412 "Please ensure you have setup git user.email and user.name".format( 413 message=goe.message, ls=os.linesep 414 )) 415 416 self.repository_remote_name = expected_remote_name 417 self.logger.warning("Added git remote {remote}".format(remote=expected_remote_name)) 418 419 def process_remote_repository(self): 420 # Create remote repository if it does not exist 421 repository = self.adbp.get_azure_devops_repository( 422 self.organization_name, 423 self.project_name, 424 self.repository_name 425 ) 426 if not repository: 427 self.adbp.create_repository(self.organization_name, self.project_name, self.repository_name) 428 429 # Force push branches if repository is not clean 430 remote_url = self.adbp.get_azure_devops_repo_url( 431 self.organization_name, 432 self.project_name, 433 self.repository_name 434 ) 435 remote_branches = self.adbp.get_azure_devops_repository_branches( 436 self.organization_name, 437 self.project_name, 438 self.repository_name 439 ) 440 is_force_push = self._check_if_force_push_required(remote_url, remote_branches) 441 442 # Prompt user to generate a git credential 443 self._check_if_git_credential_required() 444 445 # If the repository does not exist, we will do a normal push 446 # If the repository exists, we will do a force push 447 try: 448 self.adbp.push_local_to_azure_devops_repository( 449 self.organization_name, 450 self.project_name, 451 self.repository_name, 452 force=is_force_push 453 ) 454 except GitOperationException: 455 self.adbp.remove_git_remote(self.organization_name, self.project_name, self.repository_name) 456 raise CLIError("Failed to push your local repository to {url}{ls}" 457 "Please check your credentials and ensure you are a contributor to the repository.".format( 458 url=remote_url, ls=os.linesep 459 )) 460 461 self.logger.warning("Local branches have been pushed to {url}".format(url=remote_url)) 462 463 def process_github_personal_access_token(self): 464 if not self.github_pat: 465 self.logger.warning("If you need to create a Github Personal Access Token, " 466 "please follow the steps found at the following link:") 467 self.logger.warning("https://help.github.com/en/articles/" 468 "creating-a-personal-access-token-for-the-command-line{ls}".format(ls=os.linesep)) 469 self.logger.warning("The required Personal Access Token permissions can be found here:") 470 self.logger.warning("https://aka.ms/azure-devops-source-repos") 471 472 while not self.github_pat or not AzureDevopsBuildProvider.check_github_pat(self.github_pat): 473 self.github_pat = prompt(msg="Github Personal Access Token: ").strip() 474 self.logger.warning("Successfully validated Github personal access token.") 475 476 def process_github_repository(self): 477 while ( 478 not self.github_repository or 479 not AzureDevopsBuildProvider.check_github_repository(self.github_pat, self.github_repository) 480 ): 481 self.github_repository = prompt(msg="Github Repository Path (e.g. Azure/azure-cli): ").strip() 482 self.logger.warning("Successfully found Github repository.") 483 484 def process_build_and_release_definition_name(self, scenario): 485 if scenario == 'AZURE_DEVOPS': 486 self.build_definition_name = self.repository_remote_name.replace("_azuredevops_", "_build_", 1)[0:256] 487 self.release_definition_name = self.repository_remote_name.replace("_azuredevops_", "_release_", 1)[0:256] 488 if scenario == 'GITHUB_INTEGRATION': 489 self.build_definition_name = "_build_github_" + self.github_repository.replace("/", "_", 1)[0:256] 490 self.release_definition_name = "_release_github_" + self.github_repository.replace("/", "_", 1)[0:256] 491 492 def process_github_service_endpoint(self): 493 service_endpoints = self.adbp.get_github_service_endpoints( 494 self.organization_name, self.project_name, self.github_repository 495 ) 496 497 if not service_endpoints: 498 service_endpoint = self.adbp.create_github_service_endpoint( 499 self.organization_name, self.project_name, self.github_repository, self.github_pat 500 ) 501 else: 502 service_endpoint = service_endpoints[0] 503 self.logger.warning("Detected a Github service endpoint already exists: {name}".format( 504 name=service_endpoint.name)) 505 506 self.github_service_endpoint_name = service_endpoint.name 507 508 def process_functionapp_service_endpoint(self, scenario): 509 repository = self.repository_name if scenario == "AZURE_DEVOPS" else self.github_repository 510 service_endpoints = self.adbp.get_service_endpoints( 511 self.organization_name, self.project_name, repository 512 ) 513 514 # If there is no matching service endpoint, we need to create a new one 515 if not service_endpoints: 516 try: 517 self.logger.warning("Creating a service principle (this may take a minute or two)") 518 service_endpoint = self.adbp.create_service_endpoint( 519 self.organization_name, self.project_name, repository 520 ) 521 except RoleAssignmentException: 522 if scenario == "AZURE_DEVOPS": 523 self.adbp.remove_git_remote(self.organization_name, self.project_name, repository) 524 raise CLIError("{ls}To create a build through Azure Pipelines,{ls}" 525 "The command will assign a contributor role to the " 526 "Azure function app release service principle.{ls}" 527 "Please ensure that:{ls}" 528 "1. You are the owner of the subscription, " 529 "or have roleAssignments/write permission.{ls}" 530 "2. You can perform app registration in Azure Active Directory.{ls}" 531 "3. The combined length of your organization name, project name and repository name " 532 "is under 68 characters.".format(ls=os.linesep)) 533 else: 534 service_endpoint = service_endpoints[0] 535 self.logger.warning("Detected a functionapp service endpoint already exists: {name}".format( 536 name=service_endpoint.name)) 537 538 self.service_endpoint_name = service_endpoint.name 539 540 def process_extensions(self): 541 if self.functionapp_type == LINUX_CONSUMPTION: 542 self.logger.warning("Installing the required extensions for the build and release") 543 self.adbp.create_extension(self.organization_name, 'AzureAppServiceSetAppSettings', 'hboelman') 544 self.adbp.create_extension(self.organization_name, 'PascalNaber-Xpirit-CreateSasToken', 'pascalnaber') 545 546 def process_build(self, scenario): 547 # need to check if the build definition already exists 548 build_definitions = self.adbp.list_build_definitions(self.organization_name, self.project_name) 549 build_definition_match = [ 550 build_definition for build_definition in build_definitions 551 if build_definition.name == self.build_definition_name 552 ] 553 554 if not build_definition_match: 555 if scenario == "AZURE_DEVOPS": 556 self.adbp.create_devops_build_definition(self.organization_name, self.project_name, 557 self.repository_name, self.build_definition_name, 558 self.build_pool_name) 559 elif scenario == "GITHUB_INTEGRATION": 560 try: 561 self.adbp.create_github_build_definition(self.organization_name, self.project_name, 562 self.github_repository, self.build_definition_name, 563 self.build_pool_name) 564 except GithubIntegrationRequestError as gire: 565 raise CLIError("{error}{ls}{ls}" 566 "Please ensure your Github personal access token has sufficient permissions.{ls}{ls}" 567 "You may visit https://aka.ms/azure-devops-source-repos for more " 568 "information.".format( 569 error=gire.message, ls=os.linesep)) 570 except GithubContentNotFound: 571 raise CLIError("Failed to create a webhook for the provided Github repository or " 572 "your repository cannot be accessed.{ls}{ls}" 573 "You may visit https://aka.ms/azure-devops-source-repos for more " 574 "information.".format( 575 ls=os.linesep)) 576 else: 577 self.logger.warning("Detected a build definition already exists: {name}".format( 578 name=self.build_definition_name)) 579 580 try: 581 self.build = self.adbp.create_build_object( 582 self.organization_name, 583 self.project_name, 584 self.build_definition_name, 585 self.build_pool_name 586 ) 587 except BuildErrorException as bee: 588 raise CLIError("{error}{ls}{ls}" 589 "Please ensure your azure-pipelines.yml file matches Azure function app's " 590 "runtime operating system and language.{ls}" 591 "You may use 'az functionapp devops-build create --overwrite-yaml true' " 592 "to force generating an azure-pipelines.yml specifically for your build".format( 593 error=bee.message, ls=os.linesep)) 594 595 url = "https://dev.azure.com/{org}/{proj}/_build/results?buildId={build_id}".format( 596 org=self.organization_name, 597 proj=self.project_name, 598 build_id=self.build.id 599 ) 600 self.logger.warning("The build for the function app has been initiated (this may take a few minutes)") 601 self.logger.warning("To follow the build process go to {url}".format(url=url)) 602 603 def wait_for_build(self): 604 build = None 605 prev_log_status = None 606 607 self.logger.info("========== Build Log ==========") 608 while build is None or build.result is None: 609 time.sleep(BUILD_QUERY_FREQUENCY) 610 build = self._get_build_by_id(self.organization_name, self.project_name, self.build.id) 611 612 # Log streaming 613 if self.logger.isEnabledFor(logging.INFO): 614 curr_log_status = self.adbp.get_build_logs_status( 615 self.organization_name, 616 self.project_name, 617 self.build.id 618 ) 619 log_content = self.adbp.get_build_logs_content_from_statuses( 620 organization_name=self.organization_name, 621 project_name=self.project_name, 622 build_id=self.build.id, 623 prev_log=prev_log_status, 624 curr_log=curr_log_status 625 ) 626 if log_content: 627 self.logger.info(log_content) 628 prev_log_status = curr_log_status 629 630 if build.result == 'failed': 631 url = "https://dev.azure.com/{org}/{proj}/_build/results?buildId={build_id}".format( 632 org=self.organization_name, 633 proj=self.project_name, 634 build_id=build.id 635 ) 636 raise CLIError("Sorry, your build has failed in Azure Pipelines.{ls}" 637 "To view details on why your build has failed please visit {url}".format( 638 url=url, ls=os.linesep 639 )) 640 if build.result == 'succeeded': 641 self.logger.warning("Your build has completed.") 642 643 def process_release(self): 644 # need to check if the release definition already exists 645 release_definitions = self.adbp.list_release_definitions(self.organization_name, self.project_name) 646 release_definition_match = [ 647 release_definition for release_definition in release_definitions 648 if release_definition.name == self.release_definition_name 649 ] 650 651 if not release_definition_match: 652 self.logger.warning("Composing a release definition...") 653 self.adbp.create_release_definition(self.organization_name, self.project_name, 654 self.build_definition_name, self.artifact_name, 655 self.release_pool_name, self.service_endpoint_name, 656 self.release_definition_name, self.functionapp_type, 657 self.functionapp_name, self.storage_name, 658 self.resource_group_name, self.settings) 659 else: 660 self.logger.warning("Detected a release definition already exists: {name}".format( 661 name=self.release_definition_name)) 662 663 # Check if a release is automatically triggered. If not, create a new release. 664 time.sleep(RELEASE_COMPOSITION_DELAY) 665 release = self.adbp.get_latest_release(self.organization_name, self.project_name, self.release_definition_name) 666 if release is None: 667 try: 668 release = self.adbp.create_release( 669 self.organization_name, 670 self.project_name, 671 self.release_definition_name 672 ) 673 except ReleaseErrorException: 674 raise CLIError("Sorry, your release has failed in Azure Pipelines.{ls}" 675 "To view details on why your release has failed please visit " 676 "https://dev.azure.com/{org}/{proj}/_release".format( 677 ls=os.linesep, org=self.organization_name, proj=self.project_name 678 )) 679 680 self.logger.warning("To follow the release process go to " 681 "https://dev.azure.com/{org}/{proj}/_releaseProgress?" 682 "_a=release-environment-logs&releaseId={release_id}".format( 683 org=self.organization_name, 684 proj=self.project_name, 685 release_id=release.id 686 )) 687 688 def _check_if_force_push_required(self, remote_url, remote_branches): 689 force_push_required = False 690 if remote_branches: 691 self.logger.warning("The remote repository is not clean: {url}".format(url=remote_url)) 692 self.logger.warning("If you wish to continue, a force push will be commited and " 693 "your local branches will overwrite the remote branches!") 694 self.logger.warning("Please ensure you have force push permission in {repo} repository.".format( 695 repo=self.repository_name 696 )) 697 698 if self.allow_force_push is None: 699 consent = prompt_y_n("I consent to force push all local branches to Azure DevOps repository") 700 else: 701 consent = str2bool(self.allow_force_push) 702 703 if not consent: 704 self.adbp.remove_git_remote(self.organization_name, self.project_name, self.repository_name) 705 raise CLIError("Failed to obtain your consent.") 706 707 force_push_required = True 708 709 return force_push_required 710 711 def _check_if_git_credential_required(self): 712 # Username and password are not required if git credential manager exists 713 if AzureDevopsBuildProvider.check_git_credential_manager(): 714 return 715 716 # Manual setup alternative credential in Azure Devops 717 self.logger.warning("Please visit https://dev.azure.com/{org}/_usersSettings/altcreds".format( 718 org=self.organization_name, 719 )) 720 self.logger.warning('Check "Enable alternate authentication credentials" and save your username and password.') 721 self.logger.warning("You may need to use this credential when pushing your code to Azure DevOps repository.") 722 consent = prompt_y_n("I have setup alternative authentication credentials for {repo}".format( 723 repo=self.repository_name 724 )) 725 if not consent: 726 self.adbp.remove_git_remote(self.organization_name, self.project_name, self.repository_name) 727 raise CLIError("Failed to obtain your consent.") 728 729 def _select_functionapp(self): 730 self.logger.info("Retrieving functionapp names.") 731 functionapps = list_function_app(self.cmd) 732 functionapp_names = sorted([functionapp.name for functionapp in functionapps]) 733 if not functionapp_names: 734 raise CLIError("You do not have any existing function apps associated with this account subscription.{ls}" 735 "1. Please make sure you are logged into the right azure account by " 736 "running 'az account show' and checking the user.{ls}" 737 "2. If you are logged in as the right account please check the subscription you are using. " 738 "Run 'az account show' and view the name.{ls}" 739 " If you need to set the subscription run " 740 "'az account set --subscription <subscription name or id>'{ls}" 741 "3. If you do not have a function app please create one".format(ls=os.linesep)) 742 choice_index = prompt_choice_list('Please select the target function app: ', functionapp_names) 743 functionapp = [functionapp for functionapp in functionapps 744 if functionapp.name == functionapp_names[choice_index]][0] 745 self.logger.info("Selected functionapp %s", functionapp.name) 746 return functionapp 747 748 def _find_local_repository_runtime_language(self): # pylint: disable=no-self-use 749 # We want to check that locally the language that they are using matches the type of application they 750 # are deploying to 751 with open('local.settings.json') as f: 752 settings = json.load(f) 753 754 runtime_language = settings.get('Values', {}).get('FUNCTIONS_WORKER_RUNTIME') 755 if not runtime_language: 756 raise CLIError("The 'FUNCTIONS_WORKER_RUNTIME' setting is not defined in the local.settings.json file") 757 758 if SUPPORTED_LANGUAGES.get(runtime_language) is not None: 759 return runtime_language 760 761 raise LanguageNotSupportException(runtime_language) 762 763 def _find_github_repository_runtime_language(self): 764 try: 765 github_file_content = AzureDevopsBuildProvider.get_github_content( 766 self.github_pat, 767 self.github_repository, 768 "local.settings.json" 769 ) 770 except GithubContentNotFound: 771 self.logger.warning("The local.settings.json is not commited to Github repository {repo}".format( 772 repo=self.github_repository 773 )) 774 self.logger.warning("Set azure-pipeline.yml language to: {language}".format( 775 language=self.functionapp_language 776 )) 777 return None 778 779 runtime_language = github_file_content.get('Values', {}).get('FUNCTIONS_WORKER_RUNTIME') 780 if not runtime_language: 781 raise CLIError("The 'FUNCTIONS_WORKER_RUNTIME' setting is not defined in the local.settings.json file") 782 783 if SUPPORTED_LANGUAGES.get(runtime_language) is not None: 784 return runtime_language 785 786 raise LanguageNotSupportException(runtime_language) 787 788 def _get_functionapp_runtime_language(self, app_settings): # pylint: disable=no-self-use 789 functions_worker_runtime = [ 790 setting['value'] for setting in app_settings if setting['name'] == "FUNCTIONS_WORKER_RUNTIME" 791 ] 792 793 if functions_worker_runtime: 794 functionapp_language = functions_worker_runtime[0] 795 if SUPPORTED_LANGUAGES.get(functionapp_language) is not None: 796 return SUPPORTED_LANGUAGES[functionapp_language] 797 798 raise LanguageNotSupportException(functionapp_language) 799 return None 800 801 def _get_functionapp_storage_name(self, app_settings): # pylint: disable=no-self-use 802 functions_worker_runtime = [ 803 setting['value'] for setting in app_settings if setting['name'] == "AzureWebJobsStorage" 804 ] 805 806 if functions_worker_runtime: 807 return functions_worker_runtime[0].split(';')[1].split('=')[1] 808 return None 809 810 def _find_type(self, kinds): # pylint: disable=no-self-use 811 if 'linux' in kinds: 812 if 'container' in kinds: 813 functionapp_type = LINUX_DEDICATED 814 else: 815 functionapp_type = LINUX_CONSUMPTION 816 else: 817 functionapp_type = WINDOWS 818 return functionapp_type 819 820 def _select_organization(self): 821 organizations = self.adbp.list_organizations() 822 organization_names = sorted([organization.accountName for organization in organizations.value]) 823 if not organization_names: 824 self.logger.warning("There are no existing organizations, you need to create a new organization.") 825 self._create_organization() 826 self.created_organization = True 827 else: 828 choice_index = prompt_choice_list('Please choose the organization: ', organization_names) 829 organization_match = [organization for organization in organizations.value 830 if organization.accountName == organization_names[choice_index]][0] 831 self.organization_name = organization_match.accountName 832 833 def _get_organization_by_name(self, organization_name): 834 organizations = self.adbp.list_organizations() 835 return [organization for organization in organizations.value 836 if organization.accountName == organization_name][0] 837 838 def _create_organization(self): 839 self.logger.info("Starting process to create a new Azure DevOps organization") 840 regions = self.adbp.list_regions() 841 region_names = sorted([region.display_name for region in regions.value]) 842 self.logger.info("The region for an Azure DevOps organization is where the organization will be located. " 843 "Try locate it near your other resources and your location") 844 choice_index = prompt_choice_list('Please select a region for the new organization: ', region_names) 845 region = [region for region in regions.value if region.display_name == region_names[choice_index]][0] 846 847 while True: 848 organization_name = prompt("Please enter a name for your new organization: ") 849 new_organization = self.adbp.create_organization(organization_name, region.name) 850 if new_organization.valid is False: 851 self.logger.warning(new_organization.message) 852 self.logger.warning("Note: all names must be globally unique") 853 else: 854 break 855 856 self.organization_name = new_organization.name 857 858 def _select_project(self): 859 projects = self.adbp.list_projects(self.organization_name) 860 if projects.count > 0: 861 project_names = sorted([project.name for project in projects.value]) 862 choice_index = prompt_choice_list('Please select your project: ', project_names) 863 project = [project for project in projects.value if project.name == project_names[choice_index]][0] 864 self.project_name = project.name 865 else: 866 self.logger.warning("There are no existing projects in this organization. " 867 "You need to create a new project.") 868 self._create_project() 869 870 def _create_project(self): 871 project_name = prompt("Please enter a name for your new project: ") 872 project = self.adbp.create_project(self.organization_name, project_name) 873 # Keep retrying to create a new project if it fails 874 while not project.valid: 875 self.logger.error(project.message) 876 project_name = prompt("Please enter a name for your new project: ") 877 project = self.adbp.create_project(self.organization_name, project_name) 878 879 self.project_name = project.name 880 self.created_project = True 881 882 def _get_build_by_id(self, organization_name, project_name, build_id): 883 builds = self.adbp.list_build_objects(organization_name, project_name) 884 return next((build for build in builds if build.id == build_id)) 885 886 887class CmdSelectors(object): 888 889 def __init__(self, cmd, logger, adbp): 890 self.cmd = cmd 891 self.logger = logger 892 self.adbp = adbp 893 894 def cmd_functionapp(self, functionapp_name): 895 functionapps = list_function_app(self.cmd) 896 functionapp_match = [functionapp for functionapp in functionapps 897 if functionapp.name == functionapp_name] 898 if not functionapp_match: 899 raise CLIError("Error finding functionapp. " 900 "Please check that the function app exists by calling 'az functionapp list'") 901 902 return functionapp_match[0] 903 904 def cmd_organization(self, organization_name): 905 organizations = self.adbp.list_organizations() 906 organization_match = [organization for organization in organizations.value 907 if organization.accountName == organization_name] 908 if not organization_match: 909 raise CLIError("Error finding organization. " 910 "Please check that the organization exists by " 911 "navigating to the Azure DevOps portal at dev.azure.com") 912 913 return organization_match[0] 914 915 def cmd_project(self, organization_name, project_name): 916 projects = self.adbp.list_projects(organization_name) 917 project_match = [project for project in projects.value if project.name == project_name] 918 919 if not project_match: 920 raise CLIError("Error finding project. " 921 "Please check that the project exists by " 922 "navigating to the Azure DevOps portal at dev.azure.com") 923 924 return project_match[0] 925