Skip to content

Commit

Permalink
Merge pull request #146 from opensafely-core/evansd/add-exec-command
Browse files Browse the repository at this point in the history
Add `opensafely exec` command
  • Loading branch information
evansd authored Oct 21, 2022
2 parents d6f908a + 76cb7d7 commit b5138de
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 1 deletion.
3 changes: 2 additions & 1 deletion opensafely/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
pkg_resources.working_set.add_entry(f"{opensafely_module_dir}/_vendor")

from opensafely import (
check, codelists, extract_stats, jupyter, pull, unzip, upgrade, info
check, codelists, execute, extract_stats, jupyter, pull, unzip, upgrade, info
)
from opensafely._vendor.jobrunner.cli import local_run

Expand Down Expand Up @@ -52,6 +52,7 @@ def add_subcommand(cmd, module):
add_subcommand("unzip", unzip)
add_subcommand("extract-stats", extract_stats)
add_subcommand("info", info)
add_subcommand("exec", execute)

# we version check before parse_args is called so that if a user is
# following recent documentation but has an old opensafely installed,
Expand Down
56 changes: 56 additions & 0 deletions opensafely/execute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import argparse
import os
import pathlib
import subprocess

from opensafely._vendor.jobrunner import config
from opensafely._vendor.jobrunner.cli.local_run import docker_preflight_check

DESCRIPTION = "Run an OpenSAFELY action outside of the `project.yaml` pipeline"


def add_arguments(parser):
parser.add_argument(
"image",
metavar="IMAGE_NAME:VERSION",
help="OpenSAFELY image and version (e.g. databuilder:v1)",
)
parser.add_argument(
"docker_args",
nargs=argparse.REMAINDER,
metavar="...",
help="Any additional arguments to pass to the image",
)
return parser


def main(image, docker_args):
if not docker_preflight_check():
return False

try:
# In order for any files that get created to have the appropriate owner/group we
# run the command using the current user's UID/GID
uid = os.getuid()
gid = os.getgid()
except Exception:
# These aren't available on Windows; but then on Windows we don't have to deal
# with the same file ownership problems which require us to match the UID in the
# first place.
user_args = []
else:
user_args = ["--user", f"{uid}:{gid}"]

proc = subprocess.run(
[
"docker",
"run",
"--rm",
"-it",
f"--volume={pathlib.Path.cwd()}:/workspace",
*user_args,
f"{config.DOCKER_REGISTRY}/{image}",
*docker_args,
]
)
return proc.returncode == 0
48 changes: 48 additions & 0 deletions tests/test_execute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import pathlib
from unittest import mock

from opensafely import execute


@mock.patch("opensafely.execute.os")
def test_execute_main(mock_os, run):
mock_os.getuid.return_value = 12345
mock_os.getgid.return_value = 67890
run.expect(["docker", "info"])
run.expect(
[
"docker",
"run",
"--rm",
"-it",
f"--volume={pathlib.Path.cwd()}:/workspace",
"--user",
"12345:67890",
"ghcr.io/opensafely-core/databuilder:v1",
"foo",
"bar",
"baz",
]
)
execute.main("databuilder:v1", ["foo", "bar", "baz"])


@mock.patch("opensafely.execute.os")
def test_execute_main_on_windows(mock_os, run):
mock_os.getuid.side_effect = AttributeError()
mock_os.getgid.side_effect = AttributeError()
run.expect(["docker", "info"])
run.expect(
[
"docker",
"run",
"--rm",
"-it",
f"--volume={pathlib.Path.cwd()}:/workspace",
"ghcr.io/opensafely-core/databuilder:v1",
"foo",
"bar",
"baz",
]
)
execute.main("databuilder:v1", ["foo", "bar", "baz"])

0 comments on commit b5138de

Please sign in to comment.