Skip to content

Commit

Permalink
Merge pull request #127 from canonical/allow-specifying-base-dotrun
Browse files Browse the repository at this point in the history
allow-specifying-base-image
  • Loading branch information
samhotep authored Jul 18, 2024
2 parents 409a527 + 4580763 commit 8b458d6
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 51 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ jobs:
uses: actions/checkout@v2

- name: Install dotrun
run: pip3 install .
run: |
pip3 install . requests==2.31.0
- name: Install newest version of curl for --retry-all-errors support
run: sudo snap install curl
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,13 @@ $ dotrun -s {script} # Run {script} but skip installing dependencies
$ dotrun --env FOO=bar {script} # Run {script} with FOO environment variable
$ dotrun -m "/path/to/mount":"localname" # Mount additional directory and run `dotrun`
$ dotrun serve -m "/path/to/mount":"localname" # Mount additional directory and run `dotrun serve`
$ dotrun refresh image # Download the latest version of dotrun-image
$ dotrun --release {release-version} # Use a specific image tag for dotrun. Useful for switching versions
$ dotrun --image {image-name} # Use a specific image for dotrun. Useful for running dotrun off local images
```

- Note that the `--image` and `--release` arguments cannot be used together, as `--image` will take precedence over `--release`

## Installation

### Docker
Expand Down Expand Up @@ -105,3 +110,34 @@ docker buildx create --name mybuilder
docker buildx use mybuilder
docker buildx build --push --platform linux/arm/v7,linux/arm64/v8,linux/amd64 --tag canonicalwebteam/dotrun-image:latest .
```

## Hacking

You can install the package locally using either pip or poetry.

### Using pip
```bash
pip3 install . requests==2.31.0
```

### Using Poetry
```bash
pip install poetry
poetry install --no-interaction
```

To run dotrun off alternative base images such as local images, you can use the `--image` flag.
```bash
dotrun --image "localimage" exec echo hello
```

To run dotrun off alternative releases, besides the `:latest` release, you can use the `--release` flag.
```bash
dotrun --release "latest" serve
```

Note that before changing the base image you should run
```bash
dotrun clean
```
to get rid of the old virtualenv.
136 changes: 125 additions & 11 deletions dotrun.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@


class Dotrun:
BASE_IMAGE_NAME = "canonicalwebteam/dotrun-image:latest"

def __init__(self):
self.cwd = os.getcwd()
self.project_name = slugify(os.path.basename(self.cwd))
Expand All @@ -29,10 +31,20 @@ def __init__(self):
sys.platform.startswith("linux")
and "microsoft" not in platform.platform()
)

self._get_docker_client()
self._check_image_updates()
self._create_cache_volume()

def _get_image_name(self, image_name):
"""
Return a fully qualified image name from a given image
name, defaulting to the :latest tag if none is provided.
"""
if ":" not in image_name:
return image_name + ":latest"
return image_name

def _get_docker_client(self):
try:
self.docker_client = docker.from_env()
Expand All @@ -47,21 +59,29 @@ def _get_docker_client(self):

def _check_image_updates(self):
try:
self.docker_client.images.get(
"canonicalwebteam/dotrun-image:latest"
)
self.docker_client.images.get(self.BASE_IMAGE_NAME)
# Pull the image in the background
print("Checking for dotrun image updates...")
threading.Thread(target=self._pull_image)
except docker.errors.ImageNotFound:
print("Getting the dotrun image...")
self._pull_image()

def _pull_image(self):
def _pull_image(self, image_name=None, exit_on_download_error=True):
"""Pull the dotrun image (if updated) from Docker Hub"""
self.docker_client.images.pull(
repository="canonicalwebteam/dotrun-image", tag="latest"
)
if not image_name:
image_name = self.BASE_IMAGE_NAME
image_uri = self._get_image_name(image_name)
repository, tag = image_uri.split(":")
try:
self.docker_client.images.pull(repository=repository, tag=tag)
except (docker.errors.APIError, docker.errors.ImageNotFound) as e:
print(f"Unable to download image: {image_name}")
# Optionally quit if image download fails
if exit_on_download_error:
print(e)
sys.exit(1)
print(f"Attempting to use local image: {image_name}")

def _create_cache_volume(self):
try:
Expand All @@ -71,7 +91,7 @@ def _create_cache_volume(self):

# We need to fix the volume ownership
self.docker_client.containers.run(
"canonicalwebteam/dotrun-image",
self.BASE_IMAGE_NAME,
f"chown -R ubuntu:ubuntu {self.container_home}.cache",
user="root",
mounts=self._prepare_mounts([]),
Expand Down Expand Up @@ -154,7 +174,9 @@ def get_mount(command, mounts):

return get_mount(command, [])

def create_container(self, command):
def create_container(self, command, image_name=None):
if not image_name:
image_name = self.BASE_IMAGE_NAME
ports = {self.project_port: self.project_port}
# Run on the same network mode as the host
network_mode = None
Expand All @@ -175,7 +197,7 @@ def create_container(self, command):
network_mode = "host"

return self.docker_client.containers.create(
image="canonicalwebteam/dotrun-image",
image=image_name,
name=name,
hostname=name,
mounts=self._prepare_mounts(command),
Expand All @@ -189,15 +211,103 @@ def create_container(self, command):
)


def _extract_cli_command_arg(pattern, command_list):
"""
Return the value from the format
--command <value>
and remove the command from the command list.
"""
pattern = re.compile(f"--{pattern} [^\s]+") # noqa
if match := re.search(pattern, " ".join(command_list)):
# Extract the value from the cli arg
command_arg = match.group(0)
try:
value = command_arg.split(" ")[1]
except IndexError:
print(f"Value for arg {command_arg} not supplied.")
sys.exit(1)

# Remove the command from command list
new_command_list = (
" ".join(command_list).replace(command_arg, "").replace(" ", " ")
)
return value, new_command_list.split(" ")
return None


def _handle_image_cli_param(dotrun, command_list):
"""
Handle the --image cli parameter, if supplied, and return the
created container and the modified command list.
"""
if result := _extract_cli_command_arg("image", command_list):
image_name, commands = result
# Sanitize the image name
image_name = dotrun._get_image_name(image_name)
return (
_start_container_with_image(dotrun, image_name, commands),
commands,
)


def _handle_release_cli_param(dotrun, command_list):
"""
Handle the --release cli parameter, if supplied, and return the
created container and the modified command list.
"""
if result := _extract_cli_command_arg("release", command_list):
image_tag, commands = result
# Get the release image uri
image_name, _ = dotrun.BASE_IMAGE_NAME.split(":")
image_tag = f"{image_name}:{image_tag}"
return (
_start_container_with_image(dotrun, image_tag, commands),
commands,
)


def _start_container_with_image(dotrun, image_uri, command_list):
"""
Utility function to start dotrun using a specified
image.
"""

print(f"Using image: {image_uri}")

# Download the image
dotrun._pull_image(image_uri, exit_on_download_error=True)

# Start dotrun from the supplied base image
try:
return dotrun.create_container(command_list, image_name=image_uri)
except docker.errors.ImageNotFound as e:
print(e)
sys.exit(1)


def cli():
dotrun = Dotrun()
command = ["dotrun"]
command.extend(sys.argv[1:])

if command[-1] == "version":
print(f"dotrun v{__version__}")
sys.exit(1)

container = dotrun.create_container(command)
if command[-1] == "refresh":
dotrun._pull_image()
print("Latest image pulled successfully.")
sys.exit(1)

# Options for starting the container using different base images
if result := _handle_image_cli_param(dotrun, command):
container, command = result
elif result := _handle_release_cli_param(dotrun, command):
container, command = result
else:
container = dotrun.create_container(command)

# 1 by default
status_code = 1
Expand All @@ -210,3 +320,7 @@ def cli():
container.remove()

return status_code


if __name__ == "__main__":
cli()
Loading

0 comments on commit 8b458d6

Please sign in to comment.