Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to update scm project using github app #15472

Open
wants to merge 1 commit into
base: devel
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions awx/main/models/credential/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,9 @@ def create(self):
{'id': 'password', 'label': gettext_noop('Password'), 'type': 'string', 'secret': True},
{'id': 'ssh_key_data', 'label': gettext_noop('SCM Private Key'), 'type': 'string', 'format': 'ssh_private_key', 'secret': True, 'multiline': True},
{'id': 'ssh_key_unlock', 'label': gettext_noop('Private Key Passphrase'), 'type': 'string', 'secret': True},
{'id': 'github_app_id', 'label': gettext_noop('GitHub App ID'), 'type': 'string'},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this also surfaced to the end-users? Why? It should be a platform-global value, it's not directly a credential.

{'id': 'github_app_installation_id', 'label': gettext_noop('GitHub App Installation ID'), 'type': 'string'},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you trying to surface this into a UI field? This is usually discoverable automatically.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

automatic

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So ideally, there should be no UI inputs at all. Log into https://pre-commit.ci or Codecov / RTD to see their UX for working with repos accessible through GH app installations.

Whenever the app is installed, you already get an event with all the information needed via webhooks.

{'id': 'github_api_url', 'label': gettext_noop('GitHub API URL'), 'type': 'string'},
],
},
)
Expand Down
37 changes: 35 additions & 2 deletions awx/main/tasks/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import traceback
import time
import urllib.parse as urlparse
import jwt
import requests

# Django
from django.conf import settings
Expand Down Expand Up @@ -1153,6 +1155,30 @@ def build_private_data(self, project_update, private_data_dir):
private_data['credentials'][credential] = credential.get_input('ssh_key_data', default='')
return private_data

def _get_github_app_installation_access_token(self, project_update):
jwt_token = jwt.encode(
{
'iat': int(time.time()), # Issued at time
'exp': int(time.time()) + (10 * 60), # JWT expiration time (10 minute maximum)
'iss': project_update.credential.get_input('github_app_id', default=''), # GitHub App's identifier
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GH App should be global for the entire AWX. The whole idea is that the users don't need to set up and maintain multiple apps. They just need to click “Install” on GH UI.

},
project_update.credential.get_input('ssh_key_data', default=''),
algorithm='RS256',
)

headers = {'Authorization': f'Bearer {jwt_token}', 'Accept': 'application/vnd.github.v3+json'}

github_api_url = project_update.credential.get_input('github_api_url', default='https://api.github.com')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This as well should be a deployment-global value rather than bound to a credential.

installation_id = project_update.credential.get_input('github_app_installation_id', default='')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be discovered from the GitHub repository URL and querying https://docs.github.com/en/rest/apps/installations?apiVersion=2022-11-28#list-app-installations-accessible-to-the-user-access-token, and stored somewhere in the DB, not surfaced to the users.

url = f'{github_api_url}/app/installations/{installation_id}/access_tokens'
response = requests.post(url, headers=headers)

if response.status_code == 201:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you invert it to !=, you can dedent a few lines of code.

access_token = response.json()['token']
return access_token
else:
raise Exception(f"Failed to get access token: {response.status_code} {response.text}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chance not to use an unspecified exception?


def build_passwords(self, project_update, runtime_passwords):
"""
Build a dictionary of passwords for SSH private key unlock and SCM
Expand All @@ -1161,8 +1187,15 @@ def build_passwords(self, project_update, runtime_passwords):
passwords = super(RunProjectUpdate, self).build_passwords(project_update, runtime_passwords)
if project_update.credential:
passwords['scm_key_unlock'] = project_update.credential.get_input('ssh_key_unlock', default='')
passwords['scm_username'] = project_update.credential.get_input('username', default='')
passwords['scm_password'] = project_update.credential.get_input('password', default='')
passwords['scm_key_data'] = project_update.credential.get_input('ssh_key_data', default='')
if project_update.credential.get_input('github_app_id', default=''):
github_installation_access_token = self._get_github_app_installation_access_token(project_update)
passwords['scm_username'] = 'x-access-token'
passwords['scm_password'] = github_installation_access_token
else:
passwords['scm_username'] = project_update.credential.get_input('username', default='')
passwords['scm_password'] = project_update.credential.get_input('password', default='')

return passwords

def build_env(self, project_update, private_data_dir, private_data_files=None):
Expand Down
Loading