Skip to content

Commit

Permalink
implement HTTPS support for cloning (#225)
Browse files Browse the repository at this point in the history
  • Loading branch information
Matthias Balke committed May 19, 2021
1 parent e465b36 commit 134c47f
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 36 deletions.
43 changes: 34 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,38 +72,40 @@ optional arguments:
[env var: MARGE_AUTH_TOKEN_FILE] (default: None)
--gitlab-url URL Your GitLab instance, e.g. "https://gitlab.example.com".
[env var: MARGE_GITLAB_URL] (default: None)
--use-https use HTTP(S) instead of SSH for GIT repository access
[env var: MARGE_USE_HTTPS] (default: False)
--ssh-key KEY The private ssh key for marge so it can clone/push.
DISABLED because passing credentials on the command line is insecure:
You can still set it via ENV variable or config file, or use "--ssh-key-file" flag.
[env var: MARGE_SSH_KEY] (default: None)
--ssh-key-file FILE Path to the private ssh key for marge so it can clone/push.
[env var: MARGE_SSH_KEY_FILE] (default: None)
--embargo INTERVAL[,..]
Time(s) during which no merging is to take place, e.g. "Friday 1pm - Monday 9am"
or "Fri 12:30 Europe/London - Mon 08:00 Europe/London"
Time(s) during which no merging is to take place, e.g. "Friday 1pm - Monday 9am".
[env var: MARGE_EMBARGO] (default: None)
--use-merge-strategy Use git merge instead of git rebase to update the *source* branch (EXPERIMENTAL)
If you need to use a strict no-rebase workflow (in most cases
you don't want this, even if you configured gitlab to use merge requests
to use merge commits on the *target* branch (the default).)
[env var: MARGE_USE_MERGE_STRATEGY] (default: False)
--rebase-remotely Instead of rebasing in a local clone of the repository, use GitLab's
built-in rebase functionality, via their API. Note that Marge can't add
information in the commits in this case.
[env var: MARGE_REBASE_REMOTELY] (default: False)
--add-tested Add "Tested: marge-bot <$MR_URL>" for the final commit on branch after it passed CI.
[env var: MARGE_ADD_TESTED] (default: False)
--batch Enable processing MRs in batches
[env var: MARGE_BATCH] (default: False)
--use-no-ff-batches Disable fast forwarding when merging MR batches.
[env var: MARGE_USE_NO_FF_BATCHES] (default: False)
--add-part-of Add "Part-of: <$MR_URL>" to each commit in MR.
[env var: MARGE_ADD_PART_OF] (default: False)
--add-reviewers Add "Reviewed-by: $approver" for each approver of MR to each commit in MR.
[env var: MARGE_ADD_REVIEWERS] (default: False)
--impersonate-approvers
Marge-bot pushes effectively don't change approval status.
[env var: MARGE_IMPERSONATE_APPROVERS] (default: False)
--merge-order The order you want marge to merge its requests.
As of earliest merge request creation time (created_at), update time (updated_at)
or assigned to 'marge-bot' user time (assigned_at)
[env var: MARGE_MERGE_ORDER] (default: created_at)
--merge-order {created_at,updated_at,assigned_at}
Order marge merges assigned requests. created_at (default), updated_at or assigned_at.
[env var: MARGE_MERGE_ORDER] (default: created_at)
--approval-reset-timeout APPROVAL_RESET_TIMEOUT
How long to wait for approvals to reset after pushing.
Only useful with the "new commits remove all approvals" option in a project's settings.
Expand Down Expand Up @@ -136,6 +138,11 @@ optional arguments:
--cli Run marge-bot as a single CLI command, not as a long-running service.
This may be used to run marge-bot in scheduled CI pipelines or cronjobs.
[env var: MARGE_CLI] (default: False)
--use-no-ff-batches Disable fast forwarding when merging MR batches [env var: MARGE_USE_NO_FF_BATCHES] (default: False)
--use-merge-commit-batches
Use merge commit when creating batches, so that the commits in the batch MR will be the same with in individual MRs. Requires sudo scope in the access token.
[env var: MARGE_USE_MERGE_COMMIT_BATCHES] (default: False)
--skip-ci-batches Skip CI when updating individual MRs when using batches [env var: MARGE_SKIP_CI_BATCHES] (default: False)
```
Here is a config file example
```yaml
Expand All @@ -156,6 +163,8 @@ project-regexp: .*
# choose one way of specifying the SSH key
#ssh-key: KEY
ssh-key-file: token.FILE
# OR use HTTPS instead of SSH
#use-https: true
```
For more information about configuring marge-bot see `--help`
Expand Down Expand Up @@ -203,7 +212,7 @@ ssh-keygen -t ed25519 -C marge-bot@invalid -f marge-bot-ssh-key -P ''
Add the public key (`marge-bot-ssh-key.pub`) to the user's `SSH Keys` in GitLab
and keep the private one handy.
### Running marge-bot in docker (what we do)
### Running marge-bot in docker using SSH (what we do)
Assuming you have already got docker installed, the quickest and most minimal
way to run marge is like so (*but see note about passing secrets on the
Expand Down Expand Up @@ -256,6 +265,22 @@ may contain bugs.
You can also specify a particular version as a tag, e.g.
`smarkets/marge-bot:0.7.0`.
### Running marge-bot in docker using HTTPS
It is also possible to use Git over HTTPS instead of Git over SSH. To use HTTPS instead of SSH,
add the `--use-https` flag and do not provide any SSH keys. Alternatively you can set the
environment variable `MARGE_USE_HTTPS` or the config file property `use-https`.
```bash
docker run --restart=on-failure \ # restart if marge crashes because GitLab is flaky
-e MARGE_AUTH_TOKEN="$(cat marge-bot.token)" \
smarkets/marge-bot \
--use-https \
--gitlab-url='http://your.gitlab.instance.com'
```
HTTPS can be used using any other deployment technique as well.
### Running marge-bot in kubernetes
It's also possible to run marge in kubernetes, e.g. here's how you use a ktmpl
template:
Expand Down
3 changes: 2 additions & 1 deletion dockerize.nix
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ in
busybox
gitMinimal
openssh
cacert
] ++ [ marge ];
config = {
Entrypoint = [ "/bin/marge.app" ];
Env = ["LANG=en_US.UTF-8" ''LOCALE_ARCHIVE=/lib/locale/locale-archive''];
Env = ["LANG=en_US.UTF-8" ''LOCALE_ARCHIVE=/lib/locale/locale-archive'' "GIT_SSL_CAINFO=/etc/ssl/certs/ca-bundle.crt" "SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt"];
};
}
18 changes: 14 additions & 4 deletions marge/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,14 @@ def regexp(str_regex):
metavar='URL',
help='Your GitLab instance, e.g. "https://gitlab.example.com".\n',
)
ssh_key_group = parser.add_mutually_exclusive_group(required=True)
ssh_key_group.add_argument(
repo_access = parser.add_mutually_exclusive_group(required=True)
repo_access.add_argument(
'--use-https',
env_var='MARGE_USE_HTTPS',
action='store_true',
help='use HTTP(S) instead of SSH for GIT repository access\n',
)
repo_access.add_argument(
'--ssh-key',
type=str,
metavar='KEY',
Expand All @@ -89,7 +95,7 @@ def regexp(str_regex):
'You can still set it via ENV variable or config file, or use "--ssh-key-file" flag.\n'
),
)
ssh_key_group.add_argument(
repo_access.add_argument(
'--ssh-key-file',
type=str, # because we want a file location, not the content
metavar='FILE',
Expand Down Expand Up @@ -261,7 +267,9 @@ def regexp(str_regex):
@contextlib.contextmanager
def _secret_auth_token_and_ssh_key(options):
auth_token = options.auth_token or options.auth_token_file.readline().strip()
if options.ssh_key_file:
if options.use_https:
yield auth_token, None
elif options.ssh_key_file:
yield auth_token, options.ssh_key_file
else:
with tempfile.NamedTemporaryFile(mode='w', prefix='ssh-key-') as tmp_ssh_key_file:
Expand Down Expand Up @@ -313,6 +321,8 @@ def main(args=None):

config = bot.BotConfig(
user=user,
use_https=options.use_https,
auth_token=auth_token,
ssh_key_file=ssh_key_file,
project_regexp=options.project_regexp,
git_timeout=options.git_timeout,
Expand Down
27 changes: 18 additions & 9 deletions marge/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,22 @@ def __init__(self, *, api, config):

def start(self):
with TemporaryDirectory() as root_dir:
repo_manager = store.RepoManager(
user=self.user,
root_dir=root_dir,
ssh_key_file=self._config.ssh_key_file,
timeout=self._config.git_timeout,
reference=self._config.git_reference_repo,
)
if self._config.use_https:
repo_manager = store.HttpsRepoManager(
user=self.user,
root_dir=root_dir,
auth_token=self._config.auth_token,
timeout=self._config.git_timeout,
reference=self._config.git_reference_repo,
)
else:
repo_manager = store.SshRepoManager(
user=self.user,
root_dir=root_dir,
ssh_key_file=self._config.ssh_key_file,
timeout=self._config.git_timeout,
reference=self._config.git_reference_repo,
)
self._run(repo_manager)

@property
Expand Down Expand Up @@ -189,8 +198,8 @@ def _get_single_job(self, project, merge_request, repo, options):


class BotConfig(namedtuple('BotConfig',
'user ssh_key_file project_regexp merge_order merge_opts git_timeout ' +
'git_reference_repo branch_regexp source_branch_regexp batch cli')):
'user use_https auth_token ssh_key_file project_regexp merge_order merge_opts ' +
'git_timeout git_reference_repo branch_regexp source_branch_regexp batch cli')):
pass


Expand Down
4 changes: 4 additions & 0 deletions marge/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ def path_with_namespace(self):
def ssh_url_to_repo(self):
return self.info['ssh_url_to_repo']

@property
def http_url_to_repo(self):
return self.info['http_url_to_repo']

@property
def merge_requests_enabled(self):
return self.info['merge_requests_enabled']
Expand Down
64 changes: 52 additions & 12 deletions marge/store.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
import re
import tempfile

from . import git


class RepoManager:

def __init__(self, user, root_dir, ssh_key_file=None, timeout=None, reference=None):
def __init__(self, user, root_dir, timeout=None, reference=None):
self._root_dir = root_dir
self._user = user
self._ssh_key_file = ssh_key_file
self._repos = {}
self._timeout = timeout
self._reference = reference

def forget_repo(self, project):
self._repos.pop(project.id, None)

@property
def user(self):
return self._user

@property
def root_dir(self):
return self._root_dir


class SshRepoManager(RepoManager):

def __init__(self, user, root_dir, ssh_key_file=None, timeout=None, reference=None):
super().__init__(user, root_dir, timeout, reference)
self._ssh_key_file = ssh_key_file

def repo_for_project(self, project):
repo = self._repos.get(project.id)
if not repo or repo.remote_url != project.ssh_url_to_repo:
Expand All @@ -31,17 +49,39 @@ def repo_for_project(self, project):

return repo

def forget_repo(self, project):
self._repos.pop(project.id, None)

@property
def user(self):
return self._user
def ssh_key_file(self):
return self._ssh_key_file

@property
def root_dir(self):
return self._root_dir

class HttpsRepoManager(RepoManager):

def __init__(self, user, root_dir, auth_token=None, timeout=None, reference=None):
super().__init__(user, root_dir, timeout, reference)
self._auth_token = auth_token

def repo_for_project(self, project):
repo = self._repos.get(project.id)
if not repo or repo.remote_url != project.http_url_to_repo:
credentials = "oauth2:" + self._auth_token
# insert token auth "oauth2:<auth_token>@"
pattern = "(http(s)?://)"
replacement = r"\1" + credentials + "@"
repo_url = re.sub(pattern, replacement, project.http_url_to_repo, 1)
local_repo_dir = tempfile.mkdtemp(dir=self._root_dir)

repo = git.Repo(repo_url, local_repo_dir, ssh_key_file=None,
timeout=self._timeout, reference=self._reference)
repo.clone()
repo.config_user_info(
user_email=self._user.email,
user_name=self._user.name,
)

self._repos[project.id] = repo

return repo

@property
def ssh_key_file(self):
return self._ssh_key_file
def auth_token(self):
return self._auth_token
2 changes: 1 addition & 1 deletion tests/test_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class TestRepoManager:
def setup_method(self, _method):
user = marge.user.User(api=None, info=dict(USER_INFO, name='Peter Parker', email='pparker@bugle.com'))
self.root_dir = tempfile.TemporaryDirectory()
self.repo_manager = marge.store.RepoManager(
self.repo_manager = marge.store.SshRepoManager(
user=user, root_dir=self.root_dir.name, ssh_key_file='/ssh/key',
)

Expand Down

0 comments on commit 134c47f

Please sign in to comment.