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 automation to create a gitlab project #73

Merged
merged 18 commits into from
May 1, 2024

Conversation

dave-shawley
Copy link
Contributor

This PR builds on top of #72 by adding the imbi.automations.gitlab.create_project function that can be used as an automation callable. It does required that you have an OAuth 2 application defined in GitLab that is used to authorize Imbi to act as a specific GitLab user. You will need the OAuth 2 Client ID and Client Secret for that application to continue (details below).

Once you have the GitLab connection configured and a user connected, creating a new project of a project type that has automations enabled will add toggles for each available automation to the display.
image

After creating the Imbi project, the project creation endpoint will run the selected automations (8400ff4). If none of the automations fail, then the endpoint responds with a 👍 to the client. If any automation fails, then any successful automations are rolled back, the Imbi project is deleted, and a 👎 is returned to the client. The only automation implemented in this PR is one to create a GitLab project (e775720). The project location is determined by the namespace.gitlab_group_name and project_type.gitlab_project_prefix. If either is missing, then the slug is substituted. There are a few interesting error conditions that can pop up:

  1. since the GitLab creation request is done as the connected GitLab user, you might not have access to create projects in the target -- this one is handled with a reasonable message IIRC
  2. if the target GitLab parent doesn't exist, a "configuration error" is displayed -- made this decision since the namespace or project-type data in Imbi is misconfigured
  3. you can configure interesting combinations of GitLab group and sub-group permissions that could result in really strange errors

Feel free to play around with different settings. The error handling can be changed fairly easily... as long as we can determine what to do programatically.

You will also need AWeber-Imbi/imbi-ui#73 running to see the new UI options.

Guru meditations

  • should we make the project defaults configurable under automations:gitlab: in the configuration file?
  • is using a default of slugs appropriate instead of explicit configuration?
  • should we send automation dependency information to the client? Attempts to create a project with unmet dependencies will fail today but I didn't spend a lot of time thinking through alternatives.

Enabling the OAuth 2 connection and automation

GitLab OAuth 2 Application Details

Confidential ☑️
Scopes api
Redirect URI (one per line)

http://localhost:8000/gitlab/auth
http://127.0.0.1:8000/gitlab/auth

Script to enable GitLab OAuth 2 integration & automation

#!/usr/bin/env python3

import contextlib
import getpass
import http.client
import json
import os
import sys
import textwrap
import urllib.parse


def get_string(prompt, secret=False):
    env_var = prompt.replace(' ', '_').upper()
    with contextlib.suppress(KeyError):
        return os.environ[env_var]

    try:
        if secret:
            return getpass.getpass(prompt + ': ')
        return input(prompt + ': ')
    except (IOError, KeyboardInterrupt):
        sys.exit('Canceled')


def post(conn, path, body, allow_statuses=None):
    allow_statuses = allow_statuses or []
    headers = {'Private-Token': imbi_api_token,
               'Content-Type': 'application/json'}
    conn.request('POST', path,
                 json.dumps(body).encode(),
                 headers=headers)
    rsp = conn.getresponse()
    rsp_body = rsp.read()
    if rsp.code >= 400:
        print('Imbi API Failure: POST {} -> {}'.format(
            path, rsp.code))
        if rsp.code in allow_statuses:
            print('Failure ignored here')
            print('')
        else:
            if rsp_body:
                json.dump(json.loads(rsp_body), sys.stdout, indent=2)
                print('')
            sys.exit(1)
    return json.loads(rsp_body) if rsp_body else None


if not os.environ.get('IMBI_API_TOKEN'):
    os.environ['IMBI_API_TOKEN'] =  '00000000-0000-0000-0000-000000000000'
imbi_api_token = get_string('Imbi API Token', secret=True)
gitlab_client_id = get_string('GitLab Client ID')
gitlab_client_secret = get_string('GitLab Client Secret', secret=True)
gitlab_url = get_string('GitLab URL')
imbi_url = get_string('Imbi URL')

parsed = urllib.parse.urlparse(gitlab_url)
gitlab_url = parsed.scheme + '://' + parsed.netloc

parsed = urllib.parse.urlparse(imbi_url)
if parsed.scheme == 'http':
    conn = http.client.HTTPConnection(parsed.hostname, parsed.port or 80)
elif parsed.scheme == 'https':
    conn = http.client.HTTPSConnection(parsed.hostname, parsed.port or 443)
else:
    sys.exit('Unhandled scheme: ' + parsed.scheme)
imbi_url = parsed.scheme + '://' + parsed.netloc

with contextlib.closing(conn):
    print('Created HTTP API project type')
    post(conn, '/project-types', {
        'name': 'HTTP API',
        'plural_name': 'HTTP APIs',
        'slug': 'api',
        'icon_class': 'fas cog',
        'environment_urls': True,
        'gitlab_project_prefix': 'api',
    }, allow_statuses=[409])

    print('Creating GitLab integration')
    post(conn, '/integrations', {
        'name': 'gitlab2',
        'api_endpoint': gitlab_url + '/api/v4',
        'api_secret': None,
    })

    print('Configuring GitLab OAuth2 connection')
    post(conn, '/integrations/gitlab2/oauth2', {
        'authorization_endpoint': gitlab_url + '/oauth/authorize',
        'token_endpoint': gitlab_url + '/oauth/token',
        'revoke_endpoint': None,
        'callback_url': imbi_url + '/gitlab/auth',
        'client_id': gitlab_client_id,
        'client_secret': gitlab_client_secret,
        'use_pkce': True,
    })

    print('Creating Create GitLab Project automation')
    post(conn, '/integrations/gitlab2/automations', {
        'name': 'Create GitLab Project',
        'callable': 'imbi.automations.gitlab.create_project',
        'categories': ['create-project'],
        'applies_to': ['api']
    })


term_size = os.get_terminal_size()

def display(*paragraphs):
    print()
    for p in paragraphs:
        msg = textwrap.dedent(p.format(**globals()))
        for line in textwrap.wrap(msg, term_size.columns * 0.7):
            print(line.strip())
        print()

display(
'''
    Your Imbi instance has been configured to connect to {gitlab_url}.
    Please open {imbi_url}/ui/user/profile and click the "Connect to
    GitLab" button.
''', '''
    If the button is not showing, reload the page. If that doesn't work
    then restart your Imbi instance.
'''
)

We can't use the `_post` helper because it sends the response before it
returns :(
This is pretty meaty so I created it as a free-standing commit with
associated tests. The context will be used in future commits.
This is the primary interface for this module.
This is a useful helper that creates a CompensatingAction that runs a
single SQL query.
I only verify the first order dependencies currently.
Using a protocol instead of type alias here since it lets us match the
default parameters.
I'm going to be adding a new parameter so this makes things a lot more
readable ;)
It turns out that we need this to interact with the GitLabClient.
I bumped the version of flake8 due to some pyflakes defects around
`# type: ignore[misc]`. Unfortunately this will have to be followed up
by another commit to adjust for changes in the flake8 plugins :(
This required a handle on the active user. Since there is only one
user of this client, I just add a new param.
We should pass along the status code from GitLab in the vast majority of
cases. For example, anything that makes Imbi produce a "Bad Request" on
the GitLab API shouldn't be retried... it is usually caused by incorrect
configuration somewhere in the Imbi data but that is nothing that an
automatic retry is going to fix. The only exception is that we should
never reflect a Not Authorized response from GitLab since it would be
interpreted as the Imbi user not being authorized against Imbi.
This is in the imbi.automations namespace since that is where our allow
listed functionality lives. The creation code itself is straight
forward. I do need to figure out how to isolate the SQL code to create
identifiers and links... I'm going to live with the technical debt for
the time being since we need a more functional model layer to really
address the problem.
pre-commit needs to have the git command available and it only works in
a git repository so .... install git, and git add in /tmp/test.
imbi/clients/gitlab.py Outdated Show resolved Hide resolved
This didn't work on some long past version of GitLab even though it is
the recommended approach in the RFC. Works now though!
@in-op in-op merged commit 0e429e2 into AWeber-Imbi:main May 1, 2024
3 checks passed
@dave-shawley dave-shawley deleted the create-gitlab-project branch May 2, 2024 20:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants