Is your feature request related to a problem? Please describe.
When developing custom themes or MFEs locally (using dev mode) and iterating through them quickly (moving changes from dev mode on Machine1 to prod environment - local mode on Machine2) I find myself having to do a combination of actions repeatedly, pretty often. Namely:
- creating an additional (backup) tag for currently used images in prod environment in case the new pull breaks something
- restoring to the previous (backed up) version of the image if the new pulled image broke something (because of discrepancies between the dev and local environment).
- deleting (pruning) old image tags no longer required since newly pulled image works correctly
Describe the solution you'd like
A new subset of commands for the tutor images ... command.
backup
Usage: tutor images backup [OPTIONS] {openedx|openedx-dev|mfe|all}
Create a backup tag for the current images.
Options:
-t, --tag TEXT Tag suffix (default: BACKUP)
-h, --help Show this message and exit.
restore
Usage: tutor images restore [OPTIONS] {openedx|openedx-dev|mfe|all}
Restore a broken image from a backup tag.
Options:
-t, --tag TEXT Tag suffix to restore from
-h, --help Show this message and exit.
prune
Usage: tutor images prune [OPTIONS] {openedx|openedx-dev|mfe|all}
Remove local backup images to free up space.
Options:
-t, --tag TEXT Tag suffix to remove
-h, --help Show this message and exit.
##Describe alternatives you've considered
Well, up until a point I was doing all of that by hand using
docker tag OLD_IMAGE_TAG NEW_IMAGE_TAG
Recently after being fed up with the amount of manual input it required I looked up the docs and implemented a tutor plugin as follows. If it's handy enough maybe this could become part of Tutor itself.
import click
import subprocess
from tutor.commands.images import images_command
from utils import get_tutor_config
# 1. Map simple CLI names to Tutor's internal configuration keys
IMAGE_KEYS = {
"openedx": "DOCKER_IMAGE_OPENEDX",
"openedx-dev": "DOCKER_IMAGE_OPENEDX_DEV",
"mfe": "MFE_DOCKER_IMAGE",
}
def get_image_string(target_name):
config = get_tutor_config()
config_key = IMAGE_KEYS[target_name]
print(config.get(config_key,"test"))
return config.get(config_key)
def generate_backup_name(image, tag_suffix):
"""Safely creates a backup tag name, preventing invalid Docker syntax."""
if ":" in image.split("/")[-1]:
# If it already has a tag (repo:20.0.1), make it repo:20.0.1-BACKUP
return f"{image}-{tag_suffix}"
else:
# If no tag is present, make it repo:BACKUP
return f"{image}:{tag_suffix}"
# --- COMMAND: BACKUP ---
@click.command()
@click.argument("target", type=click.Choice(list(IMAGE_KEYS.keys()) + ["all"]))
@click.option("-t", "--tag", default="BACKUP", help="Tag suffix (default: BACKUP)")
def backup(target, tag):
"""Tag current Docker images as backups before pulling/building new ones."""
targets = list(IMAGE_KEYS.keys()) if target == "all" else [target]
for t in targets:
image = get_image_string(t)
backup = generate_backup_name(image, tag)
click.echo(f"Creating backup: {image} -> {backup}")
subprocess.run(["docker", "tag", image, backup], check=True)
click.echo("Backup process complete.")
# --- COMMAND: RESTORE ---
@click.command()
@click.argument("target", type=click.Choice(list(IMAGE_KEYS.keys()) + ["all"]))
@click.option("-t", "--tag", default="BACKUP", help="Tag suffix to restore from")
def restore(target, tag):
"""Restore a broken image by reverting to the backup tag."""
targets = list(IMAGE_KEYS.keys()) if target == "all" else [target]
for t in targets:
original = get_image_string(t)
backup = generate_backup_name(original, tag)
click.echo(f"Restoring backup: {backup} -> {original}")
# Retag the backup as the primary image name
subprocess.run(["docker", "tag", backup, original], check=True)
click.echo("Restore complete. You may need to run `tutor local restart`.")
# --- COMMAND: CLEANUP/REMOVE ---
@click.command()
@click.argument("target", type=click.Choice(list(IMAGE_KEYS.keys()) + ["all"]))
@click.option("-t", "--tag", default="BACKUP", help="Tag suffix to remove")
def prune(target, tag):
"""Remove backup images to free up disk space."""
targets = list(IMAGE_KEYS.keys()) if target == "all" else [target]
for t in targets:
image = get_image_string(t)
backup = generate_backup_name(image, tag)
click.echo(f"Removing backup image: {backup}")
# check=False prevents the script from crashing if the backup doesn't exist
subprocess.run(["docker", "rmi", backup], check=False)
click.echo("Cleanup complete.")
# Register all commands with Tutor
images_command.add_command(backup)
images_command.add_command(restore)
images_command.add_command(prune)
Additional context
- I am not an experienced developer yet so I might be missing something about the whole docker images workflow
- I maintain custom cloned MFEs (private) and that's what I mean by "developing them"
- I am not sure if that is a niche problem or something that would be appreciated by other tutor site operators hence this issue
- Since thanks to the flexibility of tutor this solution may be implemented as a plugin, maybe it's not worth merging with base tutor
- I already use a plugin that links my custom registry by leveraging the IMAGES_PULL and IMAGES_PUSH hooks.
- I know some Tutor plugins add new images to IMAGES_BUILD_REQUIRED (i.e. Aspects) so IMAGE_KEYS from my implementation would need to be linked with IMAGES_BUILD_REQUIRED somehow?
- Code is illustrative only as it can't be run directly. It uses my util function which I would say isself explanatory (just a wrapper around tutor's config so I don't have to find the root everytime)
If this is something worse pursuing I'd be happy to create a PR after possibly some implementation suggestions.
AI Declaration
Plugin created w the help of gemini 3.1 Pro
Is your feature request related to a problem? Please describe.
When developing custom themes or MFEs locally (using dev mode) and iterating through them quickly (moving changes from dev mode on Machine1 to prod environment - local mode on Machine2) I find myself having to do a combination of actions repeatedly, pretty often. Namely:
Describe the solution you'd like
A new subset of commands for the tutor images ... command.
backup
restore
prune
##Describe alternatives you've considered
Well, up until a point I was doing all of that by hand using
Recently after being fed up with the amount of manual input it required I looked up the docs and implemented a tutor plugin as follows. If it's handy enough maybe this could become part of Tutor itself.
Additional context
If this is something worse pursuing I'd be happy to create a PR after possibly some implementation suggestions.
AI Declaration
Plugin created w the help of gemini 3.1 Pro