Skip to content

Commit

Permalink
GitHub Actions Update (#17)
Browse files Browse the repository at this point in the history
* Added models. Finished transferring Calvin's previous work.

* Updated wrong models.

* Updated models in custom.py, added githubactionclient.

* Updated envelope to be correct.

* Small bug fixes.

* Updated error handling. Fixed bugs. Initial working state.

* Added better error handling.

* Added error messages for tokens with inappropriate access rights.

* Added back get_acr_cred.

* Fixed problems from merge conflict.

* Updated names of imports from ._models.py to fix pylance erros.

* Removed random imports.

Co-authored-by: Haroon Feisal <[email protected]>
  • Loading branch information
2 people authored and calvinsID committed Mar 22, 2022
1 parent a5acf07 commit 9b66d17
Show file tree
Hide file tree
Showing 8 changed files with 473 additions and 6 deletions.
84 changes: 82 additions & 2 deletions src/containerapp/azext_containerapp/_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from ast import NotEq
import json
import time
import sys
Expand Down Expand Up @@ -523,3 +521,85 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x)
env_list.append(formatted)

return env_list

class GitHubActionClient():
@classmethod
def create_or_update(cls, cmd, resource_group_name, name, github_action_envelope, headers, no_wait=False):
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
api_version = NEW_API_VERSION
sub_id = get_subscription_id(cmd.cli_ctx)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/sourcecontrols/current?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
name,
api_version)

r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(github_action_envelope), headers=headers)

if no_wait:
return r.json()
elif r.status_code == 201:
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/sourcecontrols/current?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
name,
api_version)
return poll(cmd, request_url, "inprogress")

return r.json()

@classmethod
def show(cls, cmd, resource_group_name, name):
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
api_version = NEW_API_VERSION
sub_id = get_subscription_id(cmd.cli_ctx)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/sourcecontrols/current?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
name,
api_version)

r = send_raw_request(cmd.cli_ctx, "GET", request_url)
return r.json()

#TODO
@classmethod
def delete(cls, cmd, resource_group_name, name, headers, no_wait=False):
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
api_version = NEW_API_VERSION
sub_id = get_subscription_id(cmd.cli_ctx)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/sourcecontrols/current?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
name,
api_version)

r = send_raw_request(cmd.cli_ctx, "DELETE", request_url, headers=headers)

if no_wait:
return # API doesn't return JSON (it returns no content)
elif r.status_code in [200, 201, 202, 204]:
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/sourcecontrols/current?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
name,
api_version)

if r.status_code == 202:
from azure.cli.core.azclierror import ResourceNotFoundError
try:
poll(cmd, request_url, "cancelled")
except ResourceNotFoundError:
pass
logger.warning('Containerapp github action successfully deleted')
return
86 changes: 86 additions & 0 deletions src/containerapp/azext_containerapp/_github_oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure.cli.core.azclierror import (ValidationError, CLIInternalError, UnclassifiedUserFault)
from knack.log import get_logger

logger = get_logger(__name__)


'''
Get Github personal access token following Github oauth for command line tools
https://docs.github.com/en/developers/apps/authorizing-oauth-apps#device-flow
'''


GITHUB_OAUTH_CLIENT_ID = "8d8e1f6000648c575489"
GITHUB_OAUTH_SCOPES = [
"admin:repo_hook",
"repo",
"workflow"
]

def get_github_access_token(cmd, scope_list=None): # pylint: disable=unused-argument
if scope_list:
for scope in scope_list:
if scope not in GITHUB_OAUTH_SCOPES:
raise ValidationError("Requested github oauth scope is invalid")
scope_list = ' '.join(scope_list)

authorize_url = 'https://github.com/login/device/code'
authorize_url_data = {
'scope': scope_list,
'client_id': GITHUB_OAUTH_CLIENT_ID
}

import requests
import time
from urllib.parse import parse_qs

try:
response = requests.post(authorize_url, data=authorize_url_data)
parsed_response = parse_qs(response.content.decode('ascii'))

device_code = parsed_response['device_code'][0]
user_code = parsed_response['user_code'][0]
verification_uri = parsed_response['verification_uri'][0]
interval = int(parsed_response['interval'][0])
expires_in_seconds = int(parsed_response['expires_in'][0])
logger.warning('Please navigate to %s and enter the user code %s to activate and '
'retrieve your github personal access token', verification_uri, user_code)

timeout = time.time() + expires_in_seconds
logger.warning("Waiting up to '%s' minutes for activation", str(expires_in_seconds // 60))

confirmation_url = 'https://github.com/login/oauth/access_token'
confirmation_url_data = {
'client_id': GITHUB_OAUTH_CLIENT_ID,
'device_code': device_code,
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code'
}

pending = True
while pending:
time.sleep(interval)

if time.time() > timeout:
raise UnclassifiedUserFault('Activation did not happen in time. Please try again')

confirmation_response = requests.post(confirmation_url, data=confirmation_url_data)
parsed_confirmation_response = parse_qs(confirmation_response.content.decode('ascii'))

if 'error' in parsed_confirmation_response and parsed_confirmation_response['error'][0]:
if parsed_confirmation_response['error'][0] == 'slow_down':
interval += 5 # if slow_down error is received, 5 seconds is added to minimum polling interval
elif parsed_confirmation_response['error'][0] != 'authorization_pending':
pending = False

if 'access_token' in parsed_confirmation_response and parsed_confirmation_response['access_token'][0]:
return parsed_confirmation_response['access_token'][0]
except Exception as e:
raise CLIInternalError(
'Error: {}. Please try again, or retrieve personal access token from the Github website'.format(e))

raise UnclassifiedUserFault('Activation did not happen in time. Please try again')
47 changes: 47 additions & 0 deletions src/containerapp/azext_containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,50 @@
text: |
az containerapp env list -g MyResourceGroup
"""
helps['containerapp github-action add'] = """
type: command
short-summary: Adds GitHub Actions to the Containerapp
examples:
- name: Add GitHub Actions, using Azure Container Registry and personal access token.
text: az containerapp github-action add -g MyResourceGroup -n MyContainerapp --repo-url https://github.com/userid/repo --branch main
--registry-url myregistryurl.azurecr.io
--service-principal-client-id 00000000-0000-0000-0000-00000000
--service-principal-tenant-id 00000000-0000-0000-0000-00000000
--service-principal-client-secret ClientSecret
--token MyAccessToken
- name: Add GitHub Actions, using Azure Container Registry and log in to GitHub flow to retrieve personal access token.
text: az containerapp github-action add -g MyResourceGroup -n MyContainerapp --repo-url https://github.com/userid/repo --branch main
--registry-url myregistryurl.azurecr.io
--service-principal-client-id 00000000-0000-0000-0000-00000000
--service-principal-tenant-id 00000000-0000-0000-0000-00000000
--service-principal-client-secret ClientSecret
--login-with-github
- name: Add GitHub Actions, using Dockerhub and log in to GitHub flow to retrieve personal access token.
text: az containerapp github-action add -g MyResourceGroup -n MyContainerapp --repo-url https://github.com/userid/repo --branch main
--registry-username MyUsername
--registry-password MyPassword
--service-principal-client-id 00000000-0000-0000-0000-00000000
--service-principal-tenant-id 00000000-0000-0000-0000-00000000
--service-principal-client-secret ClientSecret
--login-with-github
"""

helps['containerapp github-action delete'] = """
type: command
short-summary: Removes GitHub Actions from the Containerapp
examples:
- name: Removes GitHub Actions, personal access token.
text: az containerapp github-action delete -g MyResourceGroup -n MyContainerapp
--token MyAccessToken
- name: Removes GitHub Actions, using log in to GitHub flow to retrieve personal access token.
text: az containerapp github-action delete -g MyResourceGroup -n MyContainerapp
--login-with-github
"""

helps['containerapp github-action show'] = """
type: command
short-summary: Show the GitHub Actions configuration on a Containerapp
examples:
- name: Show the GitHub Actions configuration on a Containerapp
text: az containerapp github-action show -g MyResourceGroup -n MyContainerapp
"""
32 changes: 32 additions & 0 deletions src/containerapp/azext_containerapp/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,35 @@
},
"tags": None
}

SourceControl = {
"properties": {
"repoUrl": None,
"branch": None,
"githubActionConfiguration": None # [GitHubActionConfiguration]
}

}

GitHubActionConfiguration = {
"registryInfo": None, # [RegistryInfo]
"azureCredentials": None, # [AzureCredentials]
"dockerfilePath": None, # str
"publishType": None, # str
"os": None, # str
"runtimeStack": None, # str
"runtimeVersion": None # str
}

RegistryInfo = {
"registryUrl": None, # str
"registryUserName": None, # str
"registryPassword": None # str
}

AzureCredentials = {
"clientId": None, # str
"clientSecret": None, # str
"tenantId": None, #str
"subscriptionId": None #str
}
17 changes: 17 additions & 0 deletions src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,22 @@ def load_arguments(self, _):
with self.argument_context('containerapp env show') as c:
c.argument('name', name_type, help='Name of the managed Environment.')

with self.argument_context('containerapp github-action add') as c:
c.argument('repo_url', help='The GitHub repository to which the workflow file will be added. In the format: https://github.com/<owner>/<repository-name>')
c.argument('token', help='A Personal Access Token with write access to the specified repository. For more information: https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line')
c.argument('branch', options_list=['--branch', '-b'], help='The branch of the GitHub repo. Defaults to "master" if not specified.')
c.argument('login_with_github', help='Interactively log in with Github to retrieve the Personal Access Token')
c.argument('registry_url', help='The url of the registry, e.g. myregistry.azurecr.io')
c.argument('registry_username', help='The username of the registry. If using Azure Container Registry, we will try to infer the credentials if not supplied')
c.argument('registry_password', help='The password of the registry. If using Azure Container Registry, we will try to infer the credentials if not supplied')
c.argument('docker_file_path', help='The dockerfile location, e.g. ./Dockerfile')
c.argument('service_principal_client_id', help='The service principal client ID. ')
c.argument('service_principal_client_secret', help='The service principal client secret.')
c.argument('service_principal_tenant_id', help='The service principal tenant ID.')

with self.argument_context('containerapp github-action delete') as c:
c.argument('token', help='A Personal Access Token with write access to the specified repository. For more information: https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line')
c.argument('login_with_github', help='Interactively log in with Github to retrieve the Personal Access Token')

with self.argument_context('containerapp revision') as c:
c.argument('revision_name', type=str, help='Name of the revision')
9 changes: 8 additions & 1 deletion src/containerapp/azext_containerapp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

from distutils.filelist import findall
from operator import is_
from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError)
from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError, RequiredArgumentMissingError)

from azure.cli.core.commands.client_factory import get_subscription_id
from knack.log import get_logger
from msrestazure.tools import parse_resource_id
Expand Down Expand Up @@ -159,6 +160,12 @@ def parse_list_of_strings(comma_separated_string):
comma_separated = comma_separated_string.split(',')
return [s.strip() for s in comma_separated]

def raise_missing_token_suggestion():
pat_documentation = "https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line"
raise RequiredArgumentMissingError("GitHub access token is required to authenticate to your repositories. "
"If you need to create a Github Personal Access Token, "
"please run with the '--login-with-github' flag or follow "
"the steps found at the following link:\n{0}".format(pat_documentation))

def _get_default_log_analytics_location(cmd):
default_location = "eastus"
Expand Down
5 changes: 5 additions & 0 deletions src/containerapp/azext_containerapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ def load_command_table(self, _):
# g.custom_command('update', 'update_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory())
g.custom_command('delete', 'delete_managed_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory())

with self.command_group('containerapp github-action') as g:
g.custom_command('add', 'create_or_update_github_action', exception_handler=ex_handler_factory())
g.custom_command('show', 'show_github_action', exception_handler=ex_handler_factory())
g.custom_command('delete', 'delete_github_action', exception_handler=ex_handler_factory())

with self.command_group('containerapp revision') as g:
g.custom_command('activate', 'activate_revision')
g.custom_command('deactivate', 'deactivate_revision')
Expand Down
Loading

0 comments on commit 9b66d17

Please sign in to comment.