Skip to content

Commit 9b66d17

Browse files
runefaHaroon Feisal
authored andcommitted
GitHub Actions Update (Azure#17)
* 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]>
1 parent a5acf07 commit 9b66d17

File tree

8 files changed

+473
-6
lines changed

8 files changed

+473
-6
lines changed

src/containerapp/azext_containerapp/_clients.py

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
# Copyright (c) Microsoft Corporation. All rights reserved.
33
# Licensed under the MIT License. See License.txt in the project root for license information.
44
# --------------------------------------------------------------------------------------------
5-
6-
from ast import NotEq
75
import json
86
import time
97
import sys
@@ -523,3 +521,85 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x)
523521
env_list.append(formatted)
524522

525523
return env_list
524+
525+
class GitHubActionClient():
526+
@classmethod
527+
def create_or_update(cls, cmd, resource_group_name, name, github_action_envelope, headers, no_wait=False):
528+
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
529+
api_version = NEW_API_VERSION
530+
sub_id = get_subscription_id(cmd.cli_ctx)
531+
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/sourcecontrols/current?api-version={}"
532+
request_url = url_fmt.format(
533+
management_hostname.strip('/'),
534+
sub_id,
535+
resource_group_name,
536+
name,
537+
api_version)
538+
539+
r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(github_action_envelope), headers=headers)
540+
541+
if no_wait:
542+
return r.json()
543+
elif r.status_code == 201:
544+
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/sourcecontrols/current?api-version={}"
545+
request_url = url_fmt.format(
546+
management_hostname.strip('/'),
547+
sub_id,
548+
resource_group_name,
549+
name,
550+
api_version)
551+
return poll(cmd, request_url, "inprogress")
552+
553+
return r.json()
554+
555+
@classmethod
556+
def show(cls, cmd, resource_group_name, name):
557+
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
558+
api_version = NEW_API_VERSION
559+
sub_id = get_subscription_id(cmd.cli_ctx)
560+
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/sourcecontrols/current?api-version={}"
561+
request_url = url_fmt.format(
562+
management_hostname.strip('/'),
563+
sub_id,
564+
resource_group_name,
565+
name,
566+
api_version)
567+
568+
r = send_raw_request(cmd.cli_ctx, "GET", request_url)
569+
return r.json()
570+
571+
#TODO
572+
@classmethod
573+
def delete(cls, cmd, resource_group_name, name, headers, no_wait=False):
574+
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
575+
api_version = NEW_API_VERSION
576+
sub_id = get_subscription_id(cmd.cli_ctx)
577+
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/sourcecontrols/current?api-version={}"
578+
request_url = url_fmt.format(
579+
management_hostname.strip('/'),
580+
sub_id,
581+
resource_group_name,
582+
name,
583+
api_version)
584+
585+
r = send_raw_request(cmd.cli_ctx, "DELETE", request_url, headers=headers)
586+
587+
if no_wait:
588+
return # API doesn't return JSON (it returns no content)
589+
elif r.status_code in [200, 201, 202, 204]:
590+
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/sourcecontrols/current?api-version={}"
591+
request_url = url_fmt.format(
592+
management_hostname.strip('/'),
593+
sub_id,
594+
resource_group_name,
595+
name,
596+
api_version)
597+
598+
if r.status_code == 202:
599+
from azure.cli.core.azclierror import ResourceNotFoundError
600+
try:
601+
poll(cmd, request_url, "cancelled")
602+
except ResourceNotFoundError:
603+
pass
604+
logger.warning('Containerapp github action successfully deleted')
605+
return
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
6+
from azure.cli.core.azclierror import (ValidationError, CLIInternalError, UnclassifiedUserFault)
7+
from knack.log import get_logger
8+
9+
logger = get_logger(__name__)
10+
11+
12+
'''
13+
Get Github personal access token following Github oauth for command line tools
14+
https://docs.github.com/en/developers/apps/authorizing-oauth-apps#device-flow
15+
'''
16+
17+
18+
GITHUB_OAUTH_CLIENT_ID = "8d8e1f6000648c575489"
19+
GITHUB_OAUTH_SCOPES = [
20+
"admin:repo_hook",
21+
"repo",
22+
"workflow"
23+
]
24+
25+
def get_github_access_token(cmd, scope_list=None): # pylint: disable=unused-argument
26+
if scope_list:
27+
for scope in scope_list:
28+
if scope not in GITHUB_OAUTH_SCOPES:
29+
raise ValidationError("Requested github oauth scope is invalid")
30+
scope_list = ' '.join(scope_list)
31+
32+
authorize_url = 'https://github.com/login/device/code'
33+
authorize_url_data = {
34+
'scope': scope_list,
35+
'client_id': GITHUB_OAUTH_CLIENT_ID
36+
}
37+
38+
import requests
39+
import time
40+
from urllib.parse import parse_qs
41+
42+
try:
43+
response = requests.post(authorize_url, data=authorize_url_data)
44+
parsed_response = parse_qs(response.content.decode('ascii'))
45+
46+
device_code = parsed_response['device_code'][0]
47+
user_code = parsed_response['user_code'][0]
48+
verification_uri = parsed_response['verification_uri'][0]
49+
interval = int(parsed_response['interval'][0])
50+
expires_in_seconds = int(parsed_response['expires_in'][0])
51+
logger.warning('Please navigate to %s and enter the user code %s to activate and '
52+
'retrieve your github personal access token', verification_uri, user_code)
53+
54+
timeout = time.time() + expires_in_seconds
55+
logger.warning("Waiting up to '%s' minutes for activation", str(expires_in_seconds // 60))
56+
57+
confirmation_url = 'https://github.com/login/oauth/access_token'
58+
confirmation_url_data = {
59+
'client_id': GITHUB_OAUTH_CLIENT_ID,
60+
'device_code': device_code,
61+
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code'
62+
}
63+
64+
pending = True
65+
while pending:
66+
time.sleep(interval)
67+
68+
if time.time() > timeout:
69+
raise UnclassifiedUserFault('Activation did not happen in time. Please try again')
70+
71+
confirmation_response = requests.post(confirmation_url, data=confirmation_url_data)
72+
parsed_confirmation_response = parse_qs(confirmation_response.content.decode('ascii'))
73+
74+
if 'error' in parsed_confirmation_response and parsed_confirmation_response['error'][0]:
75+
if parsed_confirmation_response['error'][0] == 'slow_down':
76+
interval += 5 # if slow_down error is received, 5 seconds is added to minimum polling interval
77+
elif parsed_confirmation_response['error'][0] != 'authorization_pending':
78+
pending = False
79+
80+
if 'access_token' in parsed_confirmation_response and parsed_confirmation_response['access_token'][0]:
81+
return parsed_confirmation_response['access_token'][0]
82+
except Exception as e:
83+
raise CLIInternalError(
84+
'Error: {}. Please try again, or retrieve personal access token from the Github website'.format(e))
85+
86+
raise UnclassifiedUserFault('Activation did not happen in time. Please try again')

src/containerapp/azext_containerapp/_help.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,50 @@
244244
text: |
245245
az containerapp env list -g MyResourceGroup
246246
"""
247+
helps['containerapp github-action add'] = """
248+
type: command
249+
short-summary: Adds GitHub Actions to the Containerapp
250+
examples:
251+
- name: Add GitHub Actions, using Azure Container Registry and personal access token.
252+
text: az containerapp github-action add -g MyResourceGroup -n MyContainerapp --repo-url https://github.com/userid/repo --branch main
253+
--registry-url myregistryurl.azurecr.io
254+
--service-principal-client-id 00000000-0000-0000-0000-00000000
255+
--service-principal-tenant-id 00000000-0000-0000-0000-00000000
256+
--service-principal-client-secret ClientSecret
257+
--token MyAccessToken
258+
- name: Add GitHub Actions, using Azure Container Registry and log in to GitHub flow to retrieve personal access token.
259+
text: az containerapp github-action add -g MyResourceGroup -n MyContainerapp --repo-url https://github.com/userid/repo --branch main
260+
--registry-url myregistryurl.azurecr.io
261+
--service-principal-client-id 00000000-0000-0000-0000-00000000
262+
--service-principal-tenant-id 00000000-0000-0000-0000-00000000
263+
--service-principal-client-secret ClientSecret
264+
--login-with-github
265+
- name: Add GitHub Actions, using Dockerhub and log in to GitHub flow to retrieve personal access token.
266+
text: az containerapp github-action add -g MyResourceGroup -n MyContainerapp --repo-url https://github.com/userid/repo --branch main
267+
--registry-username MyUsername
268+
--registry-password MyPassword
269+
--service-principal-client-id 00000000-0000-0000-0000-00000000
270+
--service-principal-tenant-id 00000000-0000-0000-0000-00000000
271+
--service-principal-client-secret ClientSecret
272+
--login-with-github
273+
"""
274+
275+
helps['containerapp github-action delete'] = """
276+
type: command
277+
short-summary: Removes GitHub Actions from the Containerapp
278+
examples:
279+
- name: Removes GitHub Actions, personal access token.
280+
text: az containerapp github-action delete -g MyResourceGroup -n MyContainerapp
281+
--token MyAccessToken
282+
- name: Removes GitHub Actions, using log in to GitHub flow to retrieve personal access token.
283+
text: az containerapp github-action delete -g MyResourceGroup -n MyContainerapp
284+
--login-with-github
285+
"""
286+
287+
helps['containerapp github-action show'] = """
288+
type: command
289+
short-summary: Show the GitHub Actions configuration on a Containerapp
290+
examples:
291+
- name: Show the GitHub Actions configuration on a Containerapp
292+
text: az containerapp github-action show -g MyResourceGroup -n MyContainerapp
293+
"""

src/containerapp/azext_containerapp/_models.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,35 @@
180180
},
181181
"tags": None
182182
}
183+
184+
SourceControl = {
185+
"properties": {
186+
"repoUrl": None,
187+
"branch": None,
188+
"githubActionConfiguration": None # [GitHubActionConfiguration]
189+
}
190+
191+
}
192+
193+
GitHubActionConfiguration = {
194+
"registryInfo": None, # [RegistryInfo]
195+
"azureCredentials": None, # [AzureCredentials]
196+
"dockerfilePath": None, # str
197+
"publishType": None, # str
198+
"os": None, # str
199+
"runtimeStack": None, # str
200+
"runtimeVersion": None # str
201+
}
202+
203+
RegistryInfo = {
204+
"registryUrl": None, # str
205+
"registryUserName": None, # str
206+
"registryPassword": None # str
207+
}
208+
209+
AzureCredentials = {
210+
"clientId": None, # str
211+
"clientSecret": None, # str
212+
"tenantId": None, #str
213+
"subscriptionId": None #str
214+
}

src/containerapp/azext_containerapp/_params.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,22 @@ def load_arguments(self, _):
103103
with self.argument_context('containerapp env show') as c:
104104
c.argument('name', name_type, help='Name of the managed Environment.')
105105

106+
with self.argument_context('containerapp github-action add') as c:
107+
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>')
108+
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')
109+
c.argument('branch', options_list=['--branch', '-b'], help='The branch of the GitHub repo. Defaults to "master" if not specified.')
110+
c.argument('login_with_github', help='Interactively log in with Github to retrieve the Personal Access Token')
111+
c.argument('registry_url', help='The url of the registry, e.g. myregistry.azurecr.io')
112+
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')
113+
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')
114+
c.argument('docker_file_path', help='The dockerfile location, e.g. ./Dockerfile')
115+
c.argument('service_principal_client_id', help='The service principal client ID. ')
116+
c.argument('service_principal_client_secret', help='The service principal client secret.')
117+
c.argument('service_principal_tenant_id', help='The service principal tenant ID.')
118+
119+
with self.argument_context('containerapp github-action delete') as c:
120+
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')
121+
c.argument('login_with_github', help='Interactively log in with Github to retrieve the Personal Access Token')
122+
106123
with self.argument_context('containerapp revision') as c:
107124
c.argument('revision_name', type=str, help='Name of the revision')

src/containerapp/azext_containerapp/_utils.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
from distutils.filelist import findall
77
from operator import is_
8-
from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError)
8+
from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError, RequiredArgumentMissingError)
9+
910
from azure.cli.core.commands.client_factory import get_subscription_id
1011
from knack.log import get_logger
1112
from msrestazure.tools import parse_resource_id
@@ -159,6 +160,12 @@ def parse_list_of_strings(comma_separated_string):
159160
comma_separated = comma_separated_string.split(',')
160161
return [s.strip() for s in comma_separated]
161162

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

163170
def _get_default_log_analytics_location(cmd):
164171
default_location = "eastus"

src/containerapp/azext_containerapp/commands.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ def load_command_table(self, _):
5656
# g.custom_command('update', 'update_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory())
5757
g.custom_command('delete', 'delete_managed_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory())
5858

59+
with self.command_group('containerapp github-action') as g:
60+
g.custom_command('add', 'create_or_update_github_action', exception_handler=ex_handler_factory())
61+
g.custom_command('show', 'show_github_action', exception_handler=ex_handler_factory())
62+
g.custom_command('delete', 'delete_github_action', exception_handler=ex_handler_factory())
63+
5964
with self.command_group('containerapp revision') as g:
6065
g.custom_command('activate', 'activate_revision')
6166
g.custom_command('deactivate', 'deactivate_revision')

0 commit comments

Comments
 (0)