Repository: jefflester/minitrino (language: python)

Issue:
Add compatible versions range to `metadata.json` files
Add an optional key to `metadata.json` files to enforce version requirements:

```json
{
  "versions": [453]
}
```

```json
{
  "versions": [429, 453]
}
```

The first value indicates the minimum version, second value indicates the last version. If no second value is present, it is assumed that all version >= the minimum version are supported.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yaml
similarity index 95%
rename from .github/workflows/release.yml
rename to .github/workflows/release.yaml
index 6846b694..183234e2 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yaml
@@ -1,3 +1,4 @@
+---
 name: Publish Release on Merge to Master
 
 on:
@@ -79,7 +80,8 @@ jobs:
           PYPI_URL="https://pypi.org/pypi/minitrino/${GITHUB_HEAD_REF}/json"
           RESPONSE=$(curl --silent "${PYPI_URL}")
           if echo "${RESPONSE}" | grep -q "\"${GITHUB_HEAD_REF}\""; then
-            echo "Package version ${GITHUB_HEAD_REF} already exists on PyPI. Skipping upload."
+            echo "Package version ${GITHUB_HEAD_REF} already exists on PyPI. \
+            Skipping upload."
           else
             echo "Uploading package version ${GITHUB_HEAD_REF} to PyPI..."
             twine upload src/cli/dist/*
@@ -91,7 +93,8 @@ jobs:
           pip uninstall -y minitrino
           until pip install minitrino==${GITHUB_HEAD_REF} --no-cache-dir; do
             if [ $ATTEMPT -ge $MAX_ATTEMPTS ]; then
-              echo "Failed to install the latest version after $ATTEMPT attempts."
+              echo "Failed to install the latest version after \
+              $ATTEMPT attempts."
               exit 1
             fi
             echo "Attempt $ATTEMPT failed. Retrying in 30 seconds..."
diff --git a/.github/workflows/update-version-files.yml b/.github/workflows/update-version-files.yaml
similarity index 85%
rename from .github/workflows/update-version-files.yml
rename to .github/workflows/update-version-files.yaml
index 07a9ab03..59f00e33 100644
--- a/.github/workflows/update-version-files.yml
+++ b/.github/workflows/update-version-files.yaml
@@ -1,3 +1,4 @@
+---
 name: Update Version Files on New Release Branch
 
 on:
@@ -20,20 +21,23 @@ jobs:
             echo "This workflow can only be triggered by a branch"
             exit 0
           fi
-          
+
           echo "Updating version files to version ${BRANCH}"
           echo "Update ./src/lib/version"
           echo "${BRANCH}" > ./src/lib/version
-          
+
           echo "Update ./src/cli/setup.py"
-          sed -i "s|version=\"[0-9]\.[0-9]\.[0-9]\"|version=\"${BRANCH}\"|" ./src/cli/setup.py
+          sed -i "s|version=\"[0-9]\.[0-9]\.[0-9]\"|version=\"${BRANCH}\"|" \
+            ./src/cli/setup.py
 
           echo "Update ./readme.md"
-          sed -i "s|\*\*Latest Stable Release\*\*: [0-9]\.[0-9]\.[0-9]|\*\*Latest Stable Release\*\*: ${BRANCH}|" ./readme.md
-          
+          sed -i "s|\*\*Latest Stable Release\*\*: [0-9]\.[0-9]\.[0-9]|\
+          \*\*Latest Stable Release\*\*: ${BRANCH}|" ./readme.md
+
           if [[ -n $(git status --porcelain) ]]; then
             echo "Commit version changes"
-            git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com"
+            git config \
+              --global user.email "${GITHUB_ACTOR}@users.noreply.github.com"
             git config --global user.name "${GITHUB_ACTOR}"
             git add ./src/lib/version ./src/cli/setup.py ./readme.md
             git commit -m "Update version files to ${BRANCH}"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..ca1a8561
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,33 @@
+---
+repos:
+  - repo: https://github.com/psf/black
+    rev: 24.10.0
+    hooks:
+      - id: black
+
+  - repo: local
+    hooks:
+      - id: jq-format
+        name: Format JSON with jq
+        entry: |-
+          bash -c '
+          for file in $(find . -name "*.json" ! -path "./.vscode/*"); do
+            echo "Processing $file"
+            tmpfile=$(mktemp)
+            if ! jq . "$file" > "$tmpfile"; then
+              echo "Failed to process $file"
+              exit 1
+            fi
+            mv "$tmpfile" "$file"
+          done
+          '
+        language: system
+        files: \.json$
+        always_run: true
+        pass_filenames: false
+
+  - repo: https://github.com/adrienverge/yamllint
+    rev: v1.35.1
+    hooks:
+      - id: yamllint
+        always_run: true
diff --git a/.yamllint b/.yamllint
new file mode 100644
index 00000000..70a26db4
--- /dev/null
+++ b/.yamllint
@@ -0,0 +1,6 @@
+---
+extends: default
+
+rules:
+  line-length:  # Compose volume mounts cannot be wrapped
+    level: warning
diff --git a/readme.md b/readme.md
index ad73c8ec..4b19bd02 100644
--- a/readme.md
+++ b/readme.md
@@ -10,15 +10,15 @@ locally. Compatible with Starburst versions 429-e and later.
 [![PyPI
 version](https://badge.fury.io/py/minitrino.svg)](https://badge.fury.io/py/minitrino)
 ![CLI
-Tests](https://github.com/jefflester/minitrino/actions/workflows/cli-tests.yml/badge.svg)
+Tests](https://github.com/jefflester/minitrino/actions/workflows/cli-tests.yaml/badge.svg)
 ![Library
-Tests](https://github.com/jefflester/minitrino/actions/workflows/lib-tests.yml/badge.svg)
+Tests](https://github.com/jefflester/minitrino/actions/workflows/lib-tests.yaml/badge.svg)
 [![Trino
 Slack](https://img.shields.io/static/v1?logo=slack&logoColor=959DA5&label=Slack&labelColor=333a41&message=join%20conversation&color=3AC358)](https://trinodb.io/slack.html)
 
 -----
 
-**Latest Stable Release**: 2.1.9
+**Latest Stable Release**: 2.2.0
 
 -----
 
diff --git a/release-notes/2.2.0.md b/release-notes/2.2.0.md
new file mode 100644
index 00000000..78f1a35e
--- /dev/null
+++ b/release-notes/2.2.0.md
@@ -0,0 +1,32 @@
+# Minitrino Release Notes: 2.2.0
+
+## Release Overview
+
+- [Minitrino Release Notes: 2.2.0](#minitrino-release-notes-220)
+  - [Release Overview](#release-overview)
+  - [CLI Changes and Additions](#cli-changes-and-additions)
+  - [Library Changes and Additions](#library-changes-and-additions)
+  - [Other](#other)
+
+## CLI Changes and Additions
+
+- Added min/max Starburst version enforcement for modules in the `metadata.json`
+  file. The new JSON key is optional. Resolves #89.
+
+## Library Changes and Additions
+
+- Added `clickhouse`, `mysql-event-listener`, `session-property-manager`, and
+  `resource-groups` modules.
+- Added named volume to `insights` module to persist Postgres data.
+- Changed all `/etc/starburst/` references to `etc/` in properties files.
+- Replaced `/etc/starburst/` references in volume mounts with environment
+  variable to shorten line lengths and make Compose files more readable.
+
+## Other
+
+- Refactored from `.yml` to `.yaml`. All file extensions were updated along with
+  all `.yml` text/code references.
+- Linted all YAML, JSON, and Python files. Added a pre-commit hook for future
+  pre-commit linting.
+- Centralized certain logic and refactored a number of naming conventions in the
+  testing library.
diff --git a/src/cli/minitrino/cmd/cmd_modules.py b/src/cli/minitrino/cmd/cmd_modules.py
index a2445ddf..ca532f86 100644
--- a/src/cli/minitrino/cmd/cmd_modules.py
+++ b/src/cli/minitrino/cmd/cmd_modules.py
@@ -76,7 +76,13 @@ def log_info(ctx, module_name="", module_dict={}, json_format=False):
         ctx.logger.info(json.dumps(module_dict, indent=2))
     else:
         log_msg = [f"Module: {module_name}\n"]
-        keys = ["description", "incompatibleModules", "dependentModules", "enterprise"]
+        keys = [
+            "description",
+            "incompatibleModules",
+            "dependentModules",
+            "versions",
+            "enterprise",
+        ]
         for key in keys:
             val = module_dict.get(key, None)
             if val is not None:
diff --git a/src/cli/minitrino/cmd/cmd_provision.py b/src/cli/minitrino/cmd/cmd_provision.py
index 6047a554..ababee44 100644
--- a/src/cli/minitrino/cmd/cmd_provision.py
+++ b/src/cli/minitrino/cmd/cmd_provision.py
@@ -88,6 +88,7 @@ def cli(ctx, modules, workers, no_rollback, docker_native):
     utils.check_daemon(ctx.docker_client)
     utils.check_lib(ctx)
     utils.check_starburst_ver(ctx)
+    utils.check_version_requirements(ctx, modules)
     modules = list(modules)
     modules = append_running_modules(modules)
     modules = utils.check_dependent_modules(ctx, modules)
@@ -176,14 +177,14 @@ def check_enterprise(ctx, modules=[]):
         "Checking for SEP license for enterprise modules...",
     )
 
-    yaml_path = os.path.join(ctx.minitrino_lib_dir, "docker-compose.yml")
+    yaml_path = os.path.join(ctx.minitrino_lib_dir, "docker-compose.yaml")
     with open(yaml_path) as f:
         yaml_file = yaml.load(f, Loader=yaml.FullLoader)
     volumes = yaml_file.get("services", {}).get("trino", {}).get("volumes", [])
 
     if LIC_VOLUME_MOUNT not in volumes:
         raise err.UserError(
-            f"The required license volume in the library's root docker-compose.yml "
+            f"The required license volume in the library's root docker-compose.yaml "
             f"is either commented out or deleted: {yaml_path}. For reference, "
             f"the proper volume mount is: '{LIC_VOLUME_MOUNT}'"
         )
@@ -261,7 +262,7 @@ def build_command(ctx, docker_native="", chunk=""):
             compose_env_string,
             "\\\n",
             "docker compose -f ",
-            os.path.join(ctx.minitrino_lib_dir, "docker-compose.yml"),
+            os.path.join(ctx.minitrino_lib_dir, "docker-compose.yaml"),
             " \\\n",
             chunk,  # Module YAML paths
             "up -d --force-recreate",
@@ -435,7 +436,7 @@ def handle_password_authenticators(cfgs):
     # Check configs passed through Docker Compose YAMLs
     for module in modules:
         if module == "trino":
-            with open(os.path.join(ctx.minitrino_lib_dir, "docker-compose.yml")) as f:
+            with open(os.path.join(ctx.minitrino_lib_dir, "docker-compose.yaml")) as f:
                 yaml_file = yaml.load(f, Loader=yaml.FullLoader)
         else:
             yaml_file = ctx.modules.data.get(module, {}).get("yaml_dict")
diff --git a/src/cli/minitrino/components.py b/src/cli/minitrino/components.py
index 20c5576a..955332a3 100644
--- a/src/cli/minitrino/components.py
+++ b/src/cli/minitrino/components.py
@@ -344,7 +344,7 @@ def get_running_modules(self):
             ):
                 raise err.UserError(
                     f"Missing Minitrino labels for container '{container.name}'.",
-                    f"Check this module's 'docker-compose.yml' file and ensure you are "
+                    f"Check this module's 'docker-compose.yaml' file and ensure you are "
                     f"following the documentation on labels.",
                 )
 
@@ -391,7 +391,7 @@ def _load_modules(self):
                 # List inner-module files
                 module_files = os.listdir(module_dir)
 
-                yaml_basename = f"{os.path.basename(module_dir)}.yml"
+                yaml_basename = f"{os.path.basename(module_dir)}.yaml"
                 if not yaml_basename in module_files:
                     raise err.UserError(
                         f"Missing Docker Compose file in module directory {_dir}. "
@@ -475,7 +475,7 @@ def execute_commands(self, *args, **kwargs):
         - A list of dicts with each dict containing the following keys:
             - `command`: the original command passed to the function
             - `output`: the combined output of stdout and stderr
-            - `return_code`: the return code of the command"""
+            - `exit_code`: the exit code of the command"""
 
         output = []
         if kwargs.get("container", None):
@@ -539,7 +539,7 @@ def _execute_in_shell(self, command="", **kwargs):
         return {
             "command": command,
             "output": self._strip_ansi(output),
-            "return_code": process.returncode,
+            "exit_code": process.returncode,
         }
 
     def _execute_in_container(self, command="", **kwargs):
@@ -602,24 +602,24 @@ def _execute_in_container(self, command="", **kwargs):
             self._ctx.logger.verbose(full_line, stream=True)
 
         # Get the exit code
-        return_code = self._ctx.api_client.exec_inspect(exec_handler["Id"]).get(
+        exit_code = self._ctx.api_client.exec_inspect(exec_handler["Id"]).get(
             "ExitCode"
         )
         # https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html
-        if return_code == 126:
+        if exit_code == 126:
             self._ctx.logger.warn(
                 f"The command exited with a 126 code which typically means an "
                 f"executable is not accessible or installed. Does this image have "
                 f"all required dependencies installed?\nCommand: {command}",
             )
 
-        if return_code != 0 and kwargs.get("trigger_error", True):
+        if exit_code != 0 and kwargs.get("trigger_error", True):
             raise err.MinitrinoError(
                 f"Failed to execute command in container '{container.name}':\n{command}\n"
-                f"Exit code: {return_code}"
+                f"Exit code: {exit_code}"
             )
 
-        return {"command": command, "output": output, "return_code": return_code}
+        return {"command": command, "output": output, "exit_code": exit_code}
 
     def _construct_environment(self, environment={}, container=None):
         """Merges provided environment dictionary with user's shell environment
diff --git a/src/cli/minitrino/settings.py b/src/cli/minitrino/settings.py
index 1f8ff2df..7d86fac8 100644
--- a/src/cli/minitrino/settings.py
+++ b/src/cli/minitrino/settings.py
@@ -24,7 +24,7 @@
 
 # Snapshots
 SNAPSHOT_ROOT_FILES = [
-    "docker-compose.yml",
+    "docker-compose.yaml",
     "minitrino.env",
     "version",
     "image",
diff --git a/src/cli/minitrino/utils.py b/src/cli/minitrino/utils.py
index c1a6fb5e..616c4e44 100644
--- a/src/cli/minitrino/utils.py
+++ b/src/cli/minitrino/utils.py
@@ -217,6 +217,43 @@ def check_starburst_ver(ctx):
         raise err.UserError(error_msg)
 
 
+def check_version_requirements(ctx, modules=[]):
+    """Checks for SEP version validity per version requirements specified in any
+    module(s)."""
+
+    for module in modules:
+        versions = ctx.modules.data.get(module, {}).get("versions", [])
+
+        if not versions:
+            continue
+        if len(versions) > 2:
+            raise err.UserError(
+                f"Invalid versions specification for module '{module}' in metadata.json file: {versions}",
+                f'The valid structure is: {{"versions": [min-ver, max-ver]}}. If the versions key is '
+                f"present, the minimum version is required, and the maximum version is optional.",
+            )
+
+        starburst_ver = int(ctx.env.get("STARBURST_VER", "")[0:3])
+        min_ver = int(versions.pop(0))
+        max_ver = None
+        if versions:
+            max_ver = int(versions.pop())
+
+        begin_msg = (
+            f"The supplied Starburst version {starburst_ver} is incompatible with module '{module}'. "
+            f"Per the module's metadata.json file, the"
+        )
+
+        if starburst_ver < min_ver:
+            raise err.UserError(
+                f"{begin_msg} minimum required Starburst version for the module is: {min_ver}."
+            )
+        if max_ver and starburst_ver > max_ver:
+            raise err.UserError(
+                f"{begin_msg} maximum required Starburst version for the module is: {max_ver}."
+            )
+
+
 def check_dependent_modules(ctx, modules=[]):
     """Checks if any of the provided modules have module dependencies."""
 
diff --git a/src/cli/setup.py b/src/cli/setup.py
index 5c311f71..3893a128 100644
--- a/src/cli/setup.py
+++ b/src/cli/setup.py
@@ -10,7 +10,7 @@
 
 setup(
     name="minitrino",
-    version="2.1.9",
+    version="2.2.0",
     description="A command line tool that makes it easy to run modular Trino environments locally.",
     long_description=README,
     long_description_content_type="text/markdown",
diff --git a/src/lib/docker-compose.yml b/src/lib/docker-compose.yaml
similarity index 86%
rename from src/lib/docker-compose.yml
rename to src/lib/docker-compose.yaml
index a9333f6e..01c5aac2 100644
--- a/src/lib/docker-compose.yml
+++ b/src/lib/docker-compose.yaml
@@ -1,5 +1,6 @@
+---
 services:
-  
+
   trino:
     build:
       context: ./image/
@@ -19,10 +20,10 @@ services:
       - catalogs:/etc/starburst/catalog
     labels:
       - com.starburst.tests=minitrino
-      - com.starburst.tests.module=trino 
+      - com.starburst.tests.module=trino
 
 volumes:
   catalogs:
     labels:
       - com.starburst.tests=minitrino
-      - com.starburst.tests.module=trino 
+      - com.starburst.tests.module=trino
diff --git a/src/lib/minitrino.env b/src/lib/minitrino.env
index 1d71ccdb..1cbd5ef0 100644
--- a/src/lib/minitrino.env
+++ b/src/lib/minitrino.env
@@ -1,13 +1,16 @@
 COMPOSE_PROJECT_NAME=minitrino
 
+CLICKHOUSE_VER=23.10-alpine
 CURL_VER=8.4.0
 DB2_VER=11.5.8.0
 ELASTICSEARCH_VER=8.11.0
+ETC=/etc/starburst
 HMS_VER=3.1.3
 ICEBERG_REST_VER=1.6.0
 MARIADB_VER=10.11
 MINIO_MC_VER=RELEASE.2023-10-14T01-57-03Z
 MINIO_VER=RELEASE.2023-11-01T18-37-25Z
+MYSQL_EVENT_LISTENER_VER=8
 MYSQL_VER=8
 OAUTH2_SERVER_VER=0.5.9
 OPEN_LDAP_VER=1.3.0
diff --git a/src/lib/modules/admin/cache-service/cache-service.yml b/src/lib/modules/admin/cache-service/cache-service.yaml
similarity index 81%
rename from src/lib/modules/admin/cache-service/cache-service.yml
rename to src/lib/modules/admin/cache-service/cache-service.yaml
index 3c264f12..4ad42097 100644
--- a/src/lib/modules/admin/cache-service/cache-service.yml
+++ b/src/lib/modules/admin/cache-service/cache-service.yaml
@@ -1,3 +1,4 @@
+---
 services:
 
   trino:
@@ -6,9 +7,9 @@ services:
     environment:
       MINITRINO_BOOTSTRAP: bootstrap-trino.sh
     volumes:
-      - ./modules/admin/cache-service/resources/trino/rules.json:/etc/starburst/rules.json
-      - ./modules/admin/cache-service/resources/trino/cache.properties:/etc/starburst/cache.properties
-      - ./modules/admin/cache-service/resources/trino/cache_svc.properties:/etc/starburst/catalog/cache_svc.properties
+      - ./modules/admin/cache-service/resources/trino/rules.json:${ETC}/rules.json
+      - ./modules/admin/cache-service/resources/trino/cache.properties:${ETC}/cache.properties
+      - ./modules/admin/cache-service/resources/trino/cache_svc.properties:${ETC}/catalog/cache_svc.properties
     ports:
       - 8180:8180
 
@@ -18,9 +19,10 @@ services:
     volumes:
       - ./modules/admin/cache-service/resources/trino/hive_mv_tsr.properties:/tmp/hive_mv_tsr.properties
       - catalogs:/etc/starburst/catalog
-    command: [
-      "sh", "-c", "cp /tmp/hive_mv_tsr.properties /etc/starburst/catalog/hive_mv_tsr.properties"
-    ]
+    command:
+      - sh
+      - -c
+      - cp /tmp/hive_mv_tsr.properties /etc/starburst/catalog/hive_mv_tsr.properties
     labels:
       - com.starburst.tests=minitrino
       - com.starburst.tests.module.cache-service=admin-cache-service
diff --git a/src/lib/modules/admin/cache-service/metadata.json b/src/lib/modules/admin/cache-service/metadata.json
index 3bfe8010..5ac2041f 100644
--- a/src/lib/modules/admin/cache-service/metadata.json
+++ b/src/lib/modules/admin/cache-service/metadata.json
@@ -1,6 +1,10 @@
 {
   "description": "Cache service module (enables materialized view management and table scan redirections)",
   "incompatibleModules": [],
-  "dependentModules": ["hive", "postgres", "insights"],
+  "dependentModules": [
+    "hive",
+    "postgres",
+    "insights"
+  ],
   "enterprise": true
-}
\ No newline at end of file
+}
diff --git a/src/lib/modules/admin/cache-service/readme.md b/src/lib/modules/admin/cache-service/readme.md
index c760765e..ff4e5522 100644
--- a/src/lib/modules/admin/cache-service/readme.md
+++ b/src/lib/modules/admin/cache-service/readme.md
@@ -20,7 +20,11 @@ schema.
 
 ## Usage
 
-    minitrino --env STARBURST_VER=<ver> provision --module cache-service
+```sh
+minitrino -v provision -m cache-service
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m cache-service
+```
 
 ## Table Scan Redirections (TSRs)
 
@@ -38,17 +42,22 @@ added to this catalog, and MVs can pull data from any data source.
 
 This module uses a roundabout way to mount the `hive_mv_tsr.properties` file
 that allows for edits to be made to the file inside the Trino container without
-the source file being modified on the host. To edit the file, exec into the
-Trino container, make the desired changes, and then restart the container for
-the changes to take effect:
+the source file being modified on the host.
 
-    docker exec -it trino bash 
-    vi /etc/starburst/catalog/hive_mv_tsr.properties
-    exit
+To edit the file, exec into the Trino container, make the desired changes, and
+then restart the container for the changes to take effect:
 
-    docker restart trino
+```sh
+docker exec -it trino bash 
+vi /etc/starburst/catalog/hive_mv_tsr.properties
+exit
+
+docker restart trino
+```
 
 The properties file can also be edited directly from the module directory prior
 to provisioning the module:
 
-    lib/modules/catalog/<module>/resources/trino/<module>.properties
+```txt
+lib/modules/catalog/<module>/resources/trino/<module>.properties
+```
diff --git a/src/lib/modules/admin/cache-service/resources/trino/cache.properties b/src/lib/modules/admin/cache-service/resources/trino/cache.properties
index c08fd021..bf804686 100644
--- a/src/lib/modules/admin/cache-service/resources/trino/cache.properties
+++ b/src/lib/modules/admin/cache-service/resources/trino/cache.properties
@@ -3,4 +3,4 @@ service-database.password=trinoRocks15
 service-database.jdbc-url=jdbc:postgresql://cache-svc-backend:5432/cachesvc
 starburst.user=cachesvc
 starburst.jdbc-url=jdbc:trino://trino:8080
-rules.file=/etc/starburst/rules.json
+rules.file=etc/rules.json
diff --git a/src/lib/modules/admin/cache-service/resources/trino/rules.json b/src/lib/modules/admin/cache-service/resources/trino/rules.json
index eb61a6f4..9f0de0b8 100644
--- a/src/lib/modules/admin/cache-service/resources/trino/rules.json
+++ b/src/lib/modules/admin/cache-service/resources/trino/rules.json
@@ -22,4 +22,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/lib/modules/admin/data-products/data-products.yml b/src/lib/modules/admin/data-products/data-products.yaml
similarity index 98%
rename from src/lib/modules/admin/data-products/data-products.yml
rename to src/lib/modules/admin/data-products/data-products.yaml
index 7172f686..65e9a041 100644
--- a/src/lib/modules/admin/data-products/data-products.yml
+++ b/src/lib/modules/admin/data-products/data-products.yaml
@@ -1,3 +1,4 @@
+---
 services:
 
   trino:
diff --git a/src/lib/modules/admin/data-products/metadata.json b/src/lib/modules/admin/data-products/metadata.json
index 49fbdd99..04f729fb 100644
--- a/src/lib/modules/admin/data-products/metadata.json
+++ b/src/lib/modules/admin/data-products/metadata.json
@@ -1,6 +1,9 @@
 {
   "description": "Data products module",
   "incompatibleModules": [],
-  "dependentModules": ["insights", "hive"],
+  "dependentModules": [
+    "insights",
+    "hive"
+  ],
   "enterprise": true
 }
diff --git a/src/lib/modules/admin/data-products/readme.md b/src/lib/modules/admin/data-products/readme.md
index 828b768c..291ae29c 100644
--- a/src/lib/modules/admin/data-products/readme.md
+++ b/src/lib/modules/admin/data-products/readme.md
@@ -1,18 +1,26 @@
 # Data Products Module  
 
-A module which configures the [data
+A module which configures Starburst's [data
 products](https://docs.starburst.io/latest/data-products.html) feature.
 
 The `hive` and `insights` modules are dependencies of this module.
 
 ## Usage
 
-    minitrino --env STARBURST_VER=<ver> provision --module data-products
-    docker exec -it trino bash 
-    trino-cli
-    trino> show schemas from backend_svc;
+```sh
+minitrino -v provision -m data-products
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m data-products
 
-For configuring data product domains, use this `s3a` path, which is from a
+docker exec -it trino bash 
+trino-cli
+
+trino> SHOW SCHEMAS FROM backend_svc;
+```
+
+When configuring data product domains, use this `s3a` path, which is from a
 bucket auto-provisioned in the related MinIO container:
 
-    s3a://sample-bucket/<domain>
+```txt
+s3a://sample-bucket/<domain>
+```
diff --git a/src/lib/modules/admin/file-group-provider/file-group-provider.yml b/src/lib/modules/admin/file-group-provider/file-group-provider.yaml
similarity index 72%
rename from src/lib/modules/admin/file-group-provider/file-group-provider.yml
rename to src/lib/modules/admin/file-group-provider/file-group-provider.yaml
index a3518522..b79a102b 100644
--- a/src/lib/modules/admin/file-group-provider/file-group-provider.yml
+++ b/src/lib/modules/admin/file-group-provider/file-group-provider.yaml
@@ -1,8 +1,9 @@
+---
 services:
 
   trino:
     volumes:
-      - ./modules/admin/file-group-provider/resources/trino/group-provider.properties:/etc/starburst/group-provider.properties
-      - ./modules/admin/file-group-provider/resources/trino/groups.txt:/etc/starburst/groups.txt
+      - ./modules/admin/file-group-provider/resources/trino/group-provider.properties:${ETC}/group-provider.properties
+      - ./modules/admin/file-group-provider/resources/trino/groups.txt:${ETC}/groups.txt
     labels:
       - com.starburst.tests.module.file-group-provider=admin-file-group-provider
diff --git a/src/lib/modules/admin/file-group-provider/metadata.json b/src/lib/modules/admin/file-group-provider/metadata.json
index 374f9a8d..132dfc8f 100644
--- a/src/lib/modules/admin/file-group-provider/metadata.json
+++ b/src/lib/modules/admin/file-group-provider/metadata.json
@@ -1,7 +1,6 @@
 {
-    "description": "File group provider module",
-    "incompatibleModules": [],
-    "dependentModules": [],
-    "enterprise": false
-  }
-  
\ No newline at end of file
+  "description": "File group provider module",
+  "incompatibleModules": [],
+  "dependentModules": [],
+  "enterprise": false
+}
diff --git a/src/lib/modules/admin/file-group-provider/readme.md b/src/lib/modules/admin/file-group-provider/readme.md
index 8a833c29..0ced7da7 100644
--- a/src/lib/modules/admin/file-group-provider/readme.md
+++ b/src/lib/modules/admin/file-group-provider/readme.md
@@ -5,17 +5,25 @@ provider](https://docs.starburst.io/latest/security/group-file.html).
 
 ## Usage
 
-    # View group definitions
-    docker exec trino sh -c 'cat /etc/starburst/groups.txt'
+```sh
+minitrino -v provision -m file-group-provider
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m file-group-provider
 
-    # Get into the container and connect as a user tied to a group
-    minitrino --env STARBURST_VER=<ver> provision --module file-group-provider
-    docker exec -it trino bash 
-    trino-cli --user admin
-    trino> show schemas from tpch;
+# View group definitions
+docker exec trino sh -c 'cat /etc/starburst/groups.txt'
+
+# Get into the container and connect as a user tied to a group
+docker exec -it trino bash 
+trino-cli --user admin
+
+trino> SHOW SCHEMAS FROM tpch;
+```
 
 You will need to supply a username to the Trino CLI in order to map to a group
 (see `lib/modules/security/file-access-control/resources/trino/groups.txt` for
 which users belong to which groups). Example:
 
-    trino-cli --user admin # maps to group sepadmins
+```sh
+trino-cli --user admin # maps to group sepadmins
+```
diff --git a/src/lib/modules/admin/file-group-provider/resources/trino/group-provider.properties b/src/lib/modules/admin/file-group-provider/resources/trino/group-provider.properties
index bef10d0d..8bc878cc 100644
--- a/src/lib/modules/admin/file-group-provider/resources/trino/group-provider.properties
+++ b/src/lib/modules/admin/file-group-provider/resources/trino/group-provider.properties
@@ -1,2 +1,2 @@
 group-provider.name=file
-file.group-file=/etc/starburst/groups.txt
+file.group-file=etc/groups.txt
diff --git a/src/lib/modules/admin/file-group-provider/resources/trino/groups.txt b/src/lib/modules/admin/file-group-provider/resources/trino/groups.txt
index c132b496..2647108a 100644
--- a/src/lib/modules/admin/file-group-provider/resources/trino/groups.txt
+++ b/src/lib/modules/admin/file-group-provider/resources/trino/groups.txt
@@ -1,3 +1,3 @@
-sepadmins:admin,admin-1,admin-2
-metadata-users:metadata,metadata-1,metadata-2
-platform-users:platform,platform-1,platform-2,cachesvc
+sepadmins:admin,cachesvc
+metadata-users:metadata-user,bob
+platform-users:platform-user,alice
diff --git a/src/lib/modules/admin/insights/insights.yml b/src/lib/modules/admin/insights/insights.yaml
similarity index 72%
rename from src/lib/modules/admin/insights/insights.yml
rename to src/lib/modules/admin/insights/insights.yaml
index efc2e511..d2e0e8e4 100644
--- a/src/lib/modules/admin/insights/insights.yml
+++ b/src/lib/modules/admin/insights/insights.yaml
@@ -1,3 +1,4 @@
+---
 services:
 
   trino:
@@ -9,13 +10,21 @@ services:
         insights.persistence-enabled=true
         insights.metrics-persistence-enabled=true
     volumes:
-      - ./modules/admin/insights/resources/trino/backend_svc.properties:/etc/starburst/catalog/backend_svc.properties
-    
+      - ./modules/admin/insights/resources/trino/backend_svc.properties:${ETC}/catalog/backend_svc.properties
+
   postgres-backend-svc:
     image: postgres:${POSTGRES_SEP_BACKEND_SVC_VER}
     container_name: postgres-backend-svc
     env_file:
       - ./modules/admin/insights/resources/postgres/postgres.env
+    volumes:
+      - postgres-insights-data:/var/lib/postgresql/data
+    labels:
+      - com.starburst.tests=minitrino
+      - com.starburst.tests.module.insights=admin-insights
+
+volumes:
+  postgres-insights-data:
     labels:
       - com.starburst.tests=minitrino
       - com.starburst.tests.module.insights=admin-insights
diff --git a/src/lib/modules/admin/insights/readme.md b/src/lib/modules/admin/insights/readme.md
index a1d4dcf0..5edae2da 100644
--- a/src/lib/modules/admin/insights/readme.md
+++ b/src/lib/modules/admin/insights/readme.md
@@ -11,13 +11,52 @@ which persists the data to provide information needed for the Insights UI.
 The backend service database can be queried directly, as it is exposed as a
 catalog. For example:
 
-    minitrino --env STARBURST_VER=<ver> provision --module insights
-    docker exec -it trino bash 
-    trino-cli
-    trino> show schemas from backend_svc;
+```sh
+minitrino -v provision -m insights
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m insights
+
+docker exec -it trino bash 
+trino-cli
+
+trino> SHOW SCHEMAS FROM backend_svc;
+```
 
 ## Accessing Insights Web UI
 
 Open a web browser, navigate to
 [https://localhost:8080](https://localhost:8080), and log in with any user. The
 Insights UI should be enabled.
+
+## Persistent Storage
+
+This module uses a named volume to persist Postgres data:
+
+```yaml
+volumes:
+  postgres-insights-data:
+    labels:
+      - com.starburst.tests=minitrino
+      - com.starburst.tests.module.insights=admin-insights
+```
+
+The user-facing implication is that the data in Postgres is retained even after
+shutting down and/or removing the environment's containers. Minitrino issues a
+warning about this whenever a module with named volumes is deployed––be sure to
+look out for these warnings:
+
+```log
+[w]  Module '<module>' has persistent volumes associated with it. To delete these volumes, remember to run `minitrino remove --volumes`.
+```
+
+To remove these volumes, run:
+
+```sh
+minitrino -v remove --volumes --label com.starburst.tests.module.insights=admin-insights
+```
+
+Or, remove them directly using the Docker CLI:
+
+```sh
+docker volume rm minitrino_postgres-insights-data
+```
diff --git a/src/lib/modules/admin/mysql-event-listener/metadata.json b/src/lib/modules/admin/mysql-event-listener/metadata.json
new file mode 100644
index 00000000..691dec4d
--- /dev/null
+++ b/src/lib/modules/admin/mysql-event-listener/metadata.json
@@ -0,0 +1,6 @@
+{
+  "description": "MySQL event listener module",
+  "incompatibleModules": [],
+  "dependentModules": [],
+  "enterprise": false
+}
diff --git a/src/lib/modules/admin/mysql-event-listener/mysql-event-listener.yaml b/src/lib/modules/admin/mysql-event-listener/mysql-event-listener.yaml
new file mode 100644
index 00000000..f19fe86b
--- /dev/null
+++ b/src/lib/modules/admin/mysql-event-listener/mysql-event-listener.yaml
@@ -0,0 +1,23 @@
+---
+services:
+
+  trino:
+    environment:
+      CONFIG_PROPERTIES: |-
+        event-listener.config-files=etc/mysql-event-listener.properties
+    volumes:
+      - ./modules/admin/mysql-event-listener/resources/trino/mysql-event-listener.properties:${ETC}/mysql-event-listener.properties
+      - ./modules/admin/mysql-event-listener/resources/trino/mysql_event_listener.properties:${ETC}/catalog/mysql_event_listener.properties
+
+  mysql-event-listener-db:
+    image: mysql:${MYSQL_EVENT_LISTENER_VER}
+    container_name: mysql-event-listener-db
+    environment:
+      MINITRINO_BOOTSTRAP: bootstrap-mysql.sh
+    env_file:
+      - ./modules/admin/mysql-event-listener/resources/mysql/mysql.env
+    ports:
+      - 3308:3306  # localhost:3308
+    labels:
+      - com.starburst.tests=minitrino
+      - com.starburst.tests.module.mysql-event-listener=admin-mysql-event-listener
diff --git a/src/lib/modules/admin/mysql-event-listener/readme.md b/src/lib/modules/admin/mysql-event-listener/readme.md
new file mode 100644
index 00000000..3dd6aef8
--- /dev/null
+++ b/src/lib/modules/admin/mysql-event-listener/readme.md
@@ -0,0 +1,26 @@
+# MySQL Event Listener Module
+
+A module which utilizes Trino's [MySQL event
+listener](https://trino.io/docs/current/admin/event-listeners-mysql.html).
+
+The MySQL event listener database is exposed to `localhost` on port `3308`.
+
+Additionally, the MySQL database used for the event listener's storage is
+exposed via the `mysql_event_listener` catalog, which can be queried directly
+through Trino.
+
+## Usage
+
+```sh
+minitrino -v provision -m mysql-event-listener
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m mysql-event-listener
+
+docker exec -it trino bash 
+trino-cli
+
+# Query is logged to event listener DB
+trino> SHOW SCHEMAS FROM tpch; 
+# Query the event listener DB
+trino> SELECT * FROM mysql_event_listener.event_listener.trino_queries;
+```
diff --git a/src/lib/modules/admin/mysql-event-listener/resources/bootstrap/bootstrap-mysql.sh b/src/lib/modules/admin/mysql-event-listener/resources/bootstrap/bootstrap-mysql.sh
new file mode 100755
index 00000000..cceca27c
--- /dev/null
+++ b/src/lib/modules/admin/mysql-event-listener/resources/bootstrap/bootstrap-mysql.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+MAX_ATTEMPTS=60
+for (( ATTEMPTS=1; ATTEMPTS<=MAX_ATTEMPTS; ATTEMPTS++ ))
+do
+   mysql -ptrinoRocks15 -e "GRANT ALL PRIVILEGES ON *.* TO 'admin';"
+   if [ $? == 0 ]; then
+      break
+   fi
+   sleep 1
+done
+
+echo "Creating 'event_listener' database..."
+mysql -ptrinoRocks15 -e "CREATE DATABASE event_listener;"
diff --git a/src/lib/modules/admin/mysql-event-listener/resources/mysql/mysql.env b/src/lib/modules/admin/mysql-event-listener/resources/mysql/mysql.env
new file mode 100644
index 00000000..d75b0e62
--- /dev/null
+++ b/src/lib/modules/admin/mysql-event-listener/resources/mysql/mysql.env
@@ -0,0 +1,3 @@
+MYSQL_ROOT_PASSWORD=trinoRocks15
+MYSQL_USER=admin
+MYSQL_PASSWORD=trinoRocks15
diff --git a/src/lib/modules/admin/mysql-event-listener/resources/trino/mysql-event-listener.properties b/src/lib/modules/admin/mysql-event-listener/resources/trino/mysql-event-listener.properties
new file mode 100644
index 00000000..a35c113d
--- /dev/null
+++ b/src/lib/modules/admin/mysql-event-listener/resources/trino/mysql-event-listener.properties
@@ -0,0 +1,2 @@
+event-listener.name=mysql
+mysql-event-listener.db.url=jdbc:mysql://mysql-event-listener-db:3306/event_listener?user=admin&password=trinoRocks15
diff --git a/src/lib/modules/admin/mysql-event-listener/resources/trino/mysql_event_listener.properties b/src/lib/modules/admin/mysql-event-listener/resources/trino/mysql_event_listener.properties
new file mode 100644
index 00000000..392a10b2
--- /dev/null
+++ b/src/lib/modules/admin/mysql-event-listener/resources/trino/mysql_event_listener.properties
@@ -0,0 +1,4 @@
+connector.name=mysql
+connection-url=jdbc:mysql://mysql-event-listener-db:3306
+connection-user=admin
+connection-password=trinoRocks15
diff --git a/src/lib/modules/admin/resource-groups/metadata.json b/src/lib/modules/admin/resource-groups/metadata.json
new file mode 100644
index 00000000..e9f36d4a
--- /dev/null
+++ b/src/lib/modules/admin/resource-groups/metadata.json
@@ -0,0 +1,8 @@
+{
+  "description": "Resource groups module",
+  "incompatibleModules": [],
+  "dependentModules": [
+    "file-group-provider"
+  ],
+  "enterprise": false
+}
diff --git a/src/lib/modules/admin/resource-groups/readme.md b/src/lib/modules/admin/resource-groups/readme.md
new file mode 100644
index 00000000..1ac2a30e
--- /dev/null
+++ b/src/lib/modules/admin/resource-groups/readme.md
@@ -0,0 +1,45 @@
+# Resource Groups Module
+
+A module which implements Trino's [resource group
+functionality](https://docs.starburst.io/latest/admin/resource-groups.html).
+
+Leverages the `file-group-provider` module to define various user groups within
+the system.
+
+## Usage
+
+```sh
+minitrino -v provision -m resource-groups
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m resource-groups
+
+# Get into the container and connect as a user tied to a group
+docker exec -it trino bash 
+trino-cli --user admin
+
+trino> SELECT 1;
+```
+
+The resource groups will apply to all users, with varying weights and priorities
+assigned to certain user groups. Resource groups active at/during query
+execution can be viewed on the query details through the Trino web UI at
+`localhost:8080/ui/`. They can also be viewed through SQL via
+`system.runtime.queries`, e.g.:
+
+```sql
+SELECT resource_group_id FROM system.runtime.queries WHERE query = 'select 1' AND user = 'admin';
+```
+
+The resource group definitions are mounted to Trino as a volume and can be
+viewed/edited within the container:
+
+```sh
+docker exec -it trino bash 
+vi /etc/starburst/resource-groups.json
+```
+
+Alternatively, it can be edited directly in the library:
+
+```sh
+lib/modules/admin/resource-groups/resources/trino/resource-groups.json
+```
diff --git a/src/lib/modules/admin/resource-groups/resource-groups.yaml b/src/lib/modules/admin/resource-groups/resource-groups.yaml
new file mode 100644
index 00000000..cec8bba5
--- /dev/null
+++ b/src/lib/modules/admin/resource-groups/resource-groups.yaml
@@ -0,0 +1,9 @@
+---
+services:
+
+  trino:
+    volumes:
+      - ./modules/admin/resource-groups/resources/trino/resource-groups.json:${ETC}/resource-groups.json
+      - ./modules/admin/resource-groups/resources/trino/resource-groups.properties:${ETC}/resource-groups.properties
+    labels:
+      - com.starburst.tests.module.resource-groups=admin-resource-groups
diff --git a/src/lib/modules/admin/resource-groups/resources/trino/resource-groups.json b/src/lib/modules/admin/resource-groups/resources/trino/resource-groups.json
new file mode 100644
index 00000000..7126363a
--- /dev/null
+++ b/src/lib/modules/admin/resource-groups/resources/trino/resource-groups.json
@@ -0,0 +1,111 @@
+{
+  "rootGroups": [
+    {
+      "name": "admin",
+      "softMemoryLimit": "100%",
+      "hardConcurrencyLimit": 100,
+      "maxQueued": 100,
+      "jmxExport": true
+    },
+    {
+      "name": "metadataQueriesSkipTheQueue",
+      "hardConcurrencyLimit": 100,
+      "maxQueued": 100,
+      "softMemoryLimit": "5%",
+      "jmxExport": true
+    },
+    {
+      "name": "global",
+      "softMemoryLimit": "100%",
+      "hardConcurrencyLimit": 20,
+      "maxQueued": 20,
+      "schedulingPolicy": "weighted",
+      "jmxExport": true,
+      "subGroups": [
+        {
+          "name": "systemMetadata",
+          "softMemoryLimit": "80%",
+          "hardConcurrencyLimit": 10,
+          "maxQueued": 10,
+          "schedulingWeight": 90,
+          "schedulingPolicy": "weighted_fair",
+          "subGroups": [
+            {
+              "name": "${USER}",
+              "softMemoryLimit": "80%",
+              "hardConcurrencyLimit": 5,
+              "maxQueued": 5
+            }
+          ]
+        },
+        {
+          "name": "systemRuntime",
+          "softMemoryLimit": "20%",
+          "hardConcurrencyLimit": 1,
+          "maxQueued": 1,
+          "schedulingWeight": 10,
+          "schedulingPolicy": "weighted_fair",
+          "subGroups": [
+            {
+              "name": "${USER}",
+              "softMemoryLimit": "80%",
+              "hardConcurrencyLimit": 1,
+              "maxQueued": 1
+            }
+          ]
+        },
+        {
+          "name": "other",
+          "softMemoryLimit": "40%",
+          "hardConcurrencyLimit": 5,
+          "maxQueued": 10,
+          "schedulingWeight": 1,
+          "schedulingPolicy": "weighted_fair",
+          "jmxExport": true,
+          "subGroups": [
+            {
+              "name": "${USER}",
+              "softMemoryLimit": "10%",
+              "hardConcurrencyLimit": 5,
+              "maxQueued": 10
+            }
+          ]
+        }
+      ]
+    }
+  ],
+  "selectors": [
+    {
+      "user": "admin|cachesvc",
+      "group": "admin"
+    },
+    {
+      "userGroup": "sepadmins",
+      "group": "admin"
+    },
+    {
+      "userGroup": "metadata-users",
+      "group": "global.systemMetadata.${USER}"
+    },
+    {
+      "userGroup": "platform-users",
+      "group": "global.systemRuntime.${USER}"
+    },
+    {
+      "queryType": "DESCRIBE",
+      "group": "metadataQueriesSkipTheQueue"
+    },
+    {
+      "queryType": "EXPLAIN",
+      "group": "metadataQueriesSkipTheQueue"
+    },
+    {
+      "queryType": "DATA_DEFINITION",
+      "group": "metadataQueriesSkipTheQueue"
+    },
+    {
+      "group": "global.other.${USER}"
+    }
+  ],
+  "cpuQuotaPeriod": "1h"
+}
diff --git a/src/lib/modules/admin/resource-groups/resources/trino/resource-groups.properties b/src/lib/modules/admin/resource-groups/resources/trino/resource-groups.properties
new file mode 100644
index 00000000..4825a3fa
--- /dev/null
+++ b/src/lib/modules/admin/resource-groups/resources/trino/resource-groups.properties
@@ -0,0 +1,2 @@
+resource-groups.configuration-manager=file
+resource-groups.config-file=etc/resource-groups.json
diff --git a/src/lib/modules/admin/session-property-manager/metadata.json b/src/lib/modules/admin/session-property-manager/metadata.json
new file mode 100644
index 00000000..bae8954b
--- /dev/null
+++ b/src/lib/modules/admin/session-property-manager/metadata.json
@@ -0,0 +1,9 @@
+{
+  "description": "Session property manager module",
+  "incompatibleModules": [],
+  "dependentModules": [
+    "file-group-provider",
+    "resource-groups"
+  ],
+  "enterprise": false
+}
diff --git a/src/lib/modules/admin/session-property-manager/readme.md b/src/lib/modules/admin/session-property-manager/readme.md
new file mode 100644
index 00000000..24fcbfdf
--- /dev/null
+++ b/src/lib/modules/admin/session-property-manager/readme.md
@@ -0,0 +1,39 @@
+# Session Property Manager Module
+
+A module which implements Trino's file-based [session property
+manager](https://docs.starburst.io/latest/admin/session-property-managers.html).
+
+Leverages the `file-group-provider` and `resource-groups` module to define
+various user groups and resource management logic within the system.
+
+## Usage
+
+```sh
+minitrino -v provision -m session-property-manager
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m session-property-manager
+
+# Get into the container and connect as a user tied to a group
+docker exec -it trino bash 
+trino-cli --user admin
+
+trino> SELECT 1;
+```
+
+The resource groups will apply to all users, with varying weights and priorities
+assigned to certain user groups. Session properties applied to queries can be
+viewed on the query details through the Trino web UI at `localhost:8080/ui/`.
+
+The session property JSON file is mounted to Trino as a volume and can be
+viewed/edited within the container:
+
+```sh
+docker exec -it trino bash 
+vi /etc/starburst/session-property.json
+```
+
+Alternatively, it can be edited directly in the library:
+
+```sh
+lib/modules/admin/session-property-manager/resources/trino/session-property.json
+```
diff --git a/src/lib/modules/admin/session-property-manager/resources/trino/session-property-config.properties b/src/lib/modules/admin/session-property-manager/resources/trino/session-property-config.properties
new file mode 100644
index 00000000..15e6eb74
--- /dev/null
+++ b/src/lib/modules/admin/session-property-manager/resources/trino/session-property-config.properties
@@ -0,0 +1,2 @@
+session-property-config.configuration-manager=file
+session-property-manager.config-file=etc/session-property.json
diff --git a/src/lib/modules/admin/session-property-manager/resources/trino/session-property.json b/src/lib/modules/admin/session-property-manager/resources/trino/session-property.json
new file mode 100644
index 00000000..50d8f9fa
--- /dev/null
+++ b/src/lib/modules/admin/session-property-manager/resources/trino/session-property.json
@@ -0,0 +1,33 @@
+[
+  {
+    "group": "admin",
+    "sessionProperties": {
+      "resource_overcommit": true,
+      "query_max_execution_time": "8h"
+    }
+  },
+  {
+    "group": "metadataQueriesSkipTheQueue",
+    "sessionProperties": {
+      "query_max_execution_time": "2h"
+    }
+  },
+  {
+    "group": "global.systemMetadata.*",
+    "sessionProperties": {
+      "query_max_execution_time": "30m"
+    }
+  },
+  {
+    "group": "global.systemRuntime.*",
+    "sessionProperties": {
+      "query_max_execution_time": "10m"
+    }
+  },
+  {
+    "group": "global.other.*",
+    "sessionProperties": {
+      "query_max_execution_time": "1h"
+    }
+  }
+]
diff --git a/src/lib/modules/admin/session-property-manager/session-property-manager.yaml b/src/lib/modules/admin/session-property-manager/session-property-manager.yaml
new file mode 100644
index 00000000..49e53898
--- /dev/null
+++ b/src/lib/modules/admin/session-property-manager/session-property-manager.yaml
@@ -0,0 +1,9 @@
+---
+services:
+
+  trino:
+    volumes:
+      - ./modules/admin/session-property-manager/resources/trino/session-property.json:${ETC}/session-property.json
+      - ./modules/admin/session-property-manager/resources/trino/session-property-config.properties:${ETC}/session-property-config.properties
+    labels:
+      - com.starburst.tests.module.session-property-manager=admin-session-property-manager
diff --git a/src/lib/modules/catalog/clickhouse/clickhouse.yaml b/src/lib/modules/catalog/clickhouse/clickhouse.yaml
new file mode 100644
index 00000000..00276240
--- /dev/null
+++ b/src/lib/modules/catalog/clickhouse/clickhouse.yaml
@@ -0,0 +1,19 @@
+---
+services:
+
+  trino:
+    volumes:
+      - ./modules/catalog/clickhouse/resources/trino/clickhouse.properties:${ETC}/catalog/clickhouse.properties
+
+  clickhouse:
+    image: clickhouse/clickhouse-server:${CLICKHOUSE_VER}
+    container_name: clickhouse
+    env_file:
+      - ./modules/catalog/clickhouse/resources/clickhouse/clickhouse.env
+    ports:
+      - :8123
+    volumes:
+      - ./modules/catalog/clickhouse/resources/clickhouse/init.sh:/docker-entrypoint-initdb.d/init.sh
+    labels:
+      - com.starburst.tests=minitrino
+      - com.starburst.tests.module.clickhouse=catalog-clickhouse
diff --git a/src/lib/modules/catalog/clickhouse/metadata.json b/src/lib/modules/catalog/clickhouse/metadata.json
new file mode 100644
index 00000000..1670132a
--- /dev/null
+++ b/src/lib/modules/catalog/clickhouse/metadata.json
@@ -0,0 +1,5 @@
+{
+  "description": "ClickHouse catalog module",
+  "incompatibleModules": [],
+  "enterprise": false
+}
diff --git a/src/lib/modules/catalog/clickhouse/readme.md b/src/lib/modules/catalog/clickhouse/readme.md
new file mode 100644
index 00000000..6c7620d5
--- /dev/null
+++ b/src/lib/modules/catalog/clickhouse/readme.md
@@ -0,0 +1,32 @@
+# ClickHouse Catalog Module
+
+This module provisions a ClickHouse server with some preloaded data. The
+preloaded tables are stored in the `minitrino` ClickHouse database, which is
+exposed as the `clickhouse.minitrino` schema in Trino.
+
+## Loading Data
+
+Data is loaded by mounting a shell script to the ClickHouse container's
+`docker-entrypoint-initdb.d/` directory. Scripts in this directory are executed
+whenever the container boots up, and is the ClickHouse-supported method for
+bootstrapping ClickHouse containers.
+
+The init script can be edited in the library to load different tables and/or
+additional data:
+
+```sh
+lib/modules/catalog/clickhouse/resources/clickhouse/init.sh
+```
+
+## Usage
+
+```sh
+minitrino -v provision -m clickhouse
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m clickhouse
+
+docker exec -it trino bash 
+trino-cli
+
+trino> SHOW TABLES IN clickhouse.minitrino;
+```
diff --git a/src/lib/modules/catalog/clickhouse/resources/clickhouse/clickhouse.env b/src/lib/modules/catalog/clickhouse/resources/clickhouse/clickhouse.env
new file mode 100644
index 00000000..57e9b87b
--- /dev/null
+++ b/src/lib/modules/catalog/clickhouse/resources/clickhouse/clickhouse.env
@@ -0,0 +1,4 @@
+CLICKHOUSE_DB=minitrino
+CLICKHOUSE_USER=admin
+CLICKHOUSE_PASSWORD=trinoRocks15
+CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1
diff --git a/src/lib/modules/catalog/clickhouse/resources/clickhouse/init.sh b/src/lib/modules/catalog/clickhouse/resources/clickhouse/init.sh
new file mode 100755
index 00000000..3f47e29f
--- /dev/null
+++ b/src/lib/modules/catalog/clickhouse/resources/clickhouse/init.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+
+set -e
+
+echo "Creating three sample tables..."
+
+clickhouse client -n <<-EOSQL
+    CREATE TABLE IF NOT EXISTS minitrino.table1 (
+        id UInt32,
+        name String,
+        value Float64
+    ) ENGINE = MergeTree()
+    ORDER BY id;
+
+    CREATE TABLE IF NOT EXISTS minitrino.table2 (
+        id UInt32,
+        category String,
+        amount Decimal(10, 2)
+    ) ENGINE = MergeTree()
+    ORDER BY id;
+
+    CREATE TABLE IF NOT EXISTS minitrino.table3 (
+        id UInt32,
+        timestamp DateTime,
+        is_active UInt8
+    ) ENGINE = MergeTree()
+    ORDER BY id;
+
+    -- Insert random data into table1
+    INSERT INTO minitrino.table1
+    SELECT
+        number AS id,
+        concat('Name_', toString(number % 100)) AS name,
+        rand() % 10000 / 100.0 AS value
+    FROM numbers(1000);
+
+    -- Insert random data into table2
+    INSERT INTO minitrino.table2
+    SELECT
+        number AS id,
+        concat('Category_', toString(rand() % 10)) AS category,
+        rand() % 5000 / 100.0 AS amount
+    FROM numbers(1000);
+
+    -- Insert random data into table3
+    INSERT INTO minitrino.table3
+    SELECT
+        number AS id,
+        now() - number * 60 AS timestamp,
+        rand() % 2 AS is_active
+    FROM numbers(1000);
+EOSQL
diff --git a/src/lib/modules/catalog/clickhouse/resources/trino/clickhouse.properties b/src/lib/modules/catalog/clickhouse/resources/trino/clickhouse.properties
new file mode 100644
index 00000000..9c92d4b4
--- /dev/null
+++ b/src/lib/modules/catalog/clickhouse/resources/trino/clickhouse.properties
@@ -0,0 +1,5 @@
+connector.name=clickhouse
+connection-url=jdbc:clickhouse://clickhouse:8123
+connection-user=admin
+connection-password=trinoRocks15
+case-insensitive-name-matching=true
diff --git a/src/lib/modules/catalog/db2/db2.yml b/src/lib/modules/catalog/db2/db2.yaml
similarity index 78%
rename from src/lib/modules/catalog/db2/db2.yml
rename to src/lib/modules/catalog/db2/db2.yaml
index a39f0527..9a1eeadb 100644
--- a/src/lib/modules/catalog/db2/db2.yml
+++ b/src/lib/modules/catalog/db2/db2.yaml
@@ -1,8 +1,9 @@
+---
 services:
 
   trino:
     volumes:
-      - ./modules/catalog/db2/resources/trino/db2.properties:/etc/starburst/catalog/db2.properties
+      - ./modules/catalog/db2/resources/trino/db2.properties:${ETC}/catalog/db2.properties
 
   db2:
     image: ibmcom/db2:${DB2_VER}
diff --git a/src/lib/modules/catalog/db2/metadata.json b/src/lib/modules/catalog/db2/metadata.json
index 26fb0f54..03208a21 100644
--- a/src/lib/modules/catalog/db2/metadata.json
+++ b/src/lib/modules/catalog/db2/metadata.json
@@ -1,5 +1,5 @@
 {
-    "description": "Db2 catalog module",
-    "incompatibleModules": [],
-    "enterprise": true
-}
\ No newline at end of file
+  "description": "Db2 catalog module",
+  "incompatibleModules": [],
+  "enterprise": true
+}
diff --git a/src/lib/modules/catalog/db2/readme.md b/src/lib/modules/catalog/db2/readme.md
index 4c7c3c1c..6529e5b3 100644
--- a/src/lib/modules/catalog/db2/readme.md
+++ b/src/lib/modules/catalog/db2/readme.md
@@ -1,16 +1,21 @@
 # Db2 Catalog Module
 
-**Note**: this module doesn't really work on newer Macs with M chips. I will
-look into fixing that.
+This module provisions a standalone Db2 service.
 
-This module provisions a standalone Db2 service. Note that the Db2 service can
-take a long time to start (10-20+ minutes), so ensure you are viewing the Db2
-container logs to check the status of the service. If you receive a `connection
-refused` error from SEP -> Db2, chances are that Db2 has not started yet.
+**Note for Mac M-chip users**: the Db2 service can take a *long* time to start
+(10-20+ minutes), so ensure you are viewing the Db2 container logs to check the
+status of the service. If you receive a `connection refused` error from SEP ->
+Db2, chances are that Db2 has not started yet.
 
 ## Usage
 
-    minitrino --env STARBURST_VER=<ver> provision --module db2
-    docker exec -it trino bash 
-    trino-cli
-    trino> show schemas from db2;
+```sh
+minitrino -v provision -m db2
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m db2
+
+docker exec -it trino bash 
+trino-cli
+
+trino> SHOW SCHEMAS FROM db2;
+```
diff --git a/src/lib/modules/catalog/delta-lake/delta-lake.yml b/src/lib/modules/catalog/delta-lake/delta-lake.yaml
similarity index 80%
rename from src/lib/modules/catalog/delta-lake/delta-lake.yml
rename to src/lib/modules/catalog/delta-lake/delta-lake.yaml
index 2f7db6b0..4201ca9d 100644
--- a/src/lib/modules/catalog/delta-lake/delta-lake.yml
+++ b/src/lib/modules/catalog/delta-lake/delta-lake.yaml
@@ -1,3 +1,4 @@
+---
 services:
 
   trino:
@@ -10,9 +11,10 @@ services:
     volumes:
       - ./modules/catalog/delta-lake/resources/trino/delta.properties:/tmp/delta.properties
       - catalogs:/etc/starburst/catalog
-    command: [
-      "sh", "-c", "cp /tmp/delta.properties /etc/starburst/catalog/delta.properties"
-    ]
+    command:
+      - sh
+      - -c
+      - cp /tmp/delta.properties /etc/starburst/catalog/delta.properties
     labels:
       - com.starburst.tests=minitrino
       - com.starburst.tests.module.delta-lake=catalog-delta-lake
@@ -21,7 +23,7 @@ services:
     image: postgres:${POSTGRES_HMS_VER}
     container_name: postgres-delta-lake
     healthcheck:
-      test: [ 'CMD-SHELL', 'pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}']
+      test: ['CMD-SHELL', 'pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}']
       interval: 10s
       timeout: 5s
       retries: 5
@@ -66,13 +68,17 @@ services:
   create-minio-delta-lake-buckets:
     image: minio/mc:${MINIO_MC_VER}
     container_name: create-minio-delta-lake-buckets
-    entrypoint: >
+    entrypoint: |-
       /bin/sh -c "
-      curl -fsSL https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh > /tmp/wait-for-it.sh && \
-      chmod +x /tmp/wait-for-it.sh && \
+      wfi=wait-for-it && \
+      curl -fsSL \\
+        https://raw.githubusercontent.com/vishnubob/$$wfi/master/$$wfi.sh \\
+        > /tmp/$$wfi.sh && \
+      chmod +x /tmp/$$wfi.sh && \
       echo 'Waiting for MinIO to come up...' && \
-      /tmp/wait-for-it.sh minio-delta-lake:9000 --strict --timeout=60 -- echo 'MinIO service is up.' && \
-      /usr/bin/mc alias set minio http://minio-delta-lake:9000 access-key secret-key && \
+      /tmp/$$wfi.sh minio-delta-lake:9000 --strict --timeout=60 && \
+      /usr/bin/mc alias set minio \\
+        http://minio-delta-lake:9000 access-key secret-key && \
       /usr/bin/mc mb minio/sample-bucket/wh/ && \
       tail -f /dev/null
       "
diff --git a/src/lib/modules/catalog/delta-lake/metadata.json b/src/lib/modules/catalog/delta-lake/metadata.json
index c5363392..17b47b7b 100644
--- a/src/lib/modules/catalog/delta-lake/metadata.json
+++ b/src/lib/modules/catalog/delta-lake/metadata.json
@@ -1,5 +1,5 @@
 {
-    "description": "Delta Lake catalog module (includes object storage (MinIO) and Hive metastore)",
-    "incompatibleModules": [],
-    "enterprise": false
-}
\ No newline at end of file
+  "description": "Delta Lake catalog module (includes object storage (MinIO) and Hive metastore)",
+  "incompatibleModules": [],
+  "enterprise": false
+}
diff --git a/src/lib/modules/catalog/delta-lake/readme.md b/src/lib/modules/catalog/delta-lake/readme.md
index 55810683..ad12f490 100644
--- a/src/lib/modules/catalog/delta-lake/readme.md
+++ b/src/lib/modules/catalog/delta-lake/readme.md
@@ -16,51 +16,67 @@ The MinIO UI can be viewed at `http://localhost:9002` using `access-key` and
 This module uses the Delta Lake connector. There is no Spark backend, so tables
 need to be created via `CREATE TABLE AS ...` queries from Trino. Example:
 
-    CREATE TABLE delta.default.customer 
-    WITH (
-        location = 's3a://sample-bucket/wh/default/'
-    )
-    AS SELECT * FROM tpch.tiny.customer;
+```sql
+CREATE TABLE delta.default.customer 
+WITH (
+    location = 's3a://sample-bucket/wh/default/'
+)
+AS SELECT * FROM tpch.tiny.customer;
+```
 
 This will create the table `delta.default.customer` and a corresponding
 `_delta_log` directory in MinIO object storage.
 
 ## Usage
 
-    minitrino --env STARBURST_VER=<ver> provision --module delta-lake
-    docker exec -it trino bash 
-    trino-cli
-    trino> show schemas from delta;
+```sh
+minitrino -v provision -m delta-lake
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m delta-lake
+
+docker exec -it trino bash 
+trino-cli
+
+trino> SHOW SCHEMAS FROM delta;
+```
 
 ## Persistent Storage
 
 This module uses named volumes to persist MinIO and metastore data:
 
-    volumes:
-      postgres-delta-lake-data:
-        labels:
-          - com.starburst.tests=minitrino
-          - com.starburst.tests.module.delta-lake=catalog-delta-lake
-      minio-delta-lake-data:
-        labels:
-          - com.starburst.tests=minitrino
-          - com.starburst.tests.module.delta-lake=catalog-delta-lake
+```yaml
+volumes:
+  postgres-delta-lake-data:
+    labels:
+      - com.starburst.tests=minitrino
+      - com.starburst.tests.module.delta-lake=catalog-delta-lake
+  minio-delta-lake-data:
+    labels:
+      - com.starburst.tests=minitrino
+      - com.starburst.tests.module.delta-lake=catalog-delta-lake
+```
 
 The user-facing implication is that the data in the Hive metastore and the data
 files stored in MinIO are retained even after shutting down and/or removing the
 environment's containers. Minitrino issues a warning about this whenever a
 module with named volumes is deployed––be sure to look out for these warnings:
 
-    [w]  Module '<module>' has persistent volumes associated with it. To delete these volumes, remember to run `minitrino remove --volumes`.
+```log
+[w]  Module '<module>' has persistent volumes associated with it. To delete these volumes, remember to run `minitrino remove --volumes`.
+```
 
 To remove these volumes, run:
 
-    minitrino -v remove --volumes --label com.starburst.tests.module.delta-lake=catalog-delta-lake
+```sh
+minitrino -v remove --volumes --label com.starburst.tests.module.delta-lake=catalog-delta-lake
+```
   
 Or, remove them directly using the Docker CLI:
 
-    docker volume rm minitrino_postgres-delta-lake-data \
-      minitrino_minio-delta-lake-data
+```sh
+docker volume rm minitrino_postgres-delta-lake-data \
+  minitrino_minio-delta-lake-data
+```
 
 ## Editing the `delta.properties` File
 
@@ -70,13 +86,17 @@ source file being modified on the host. To edit the file, exec into the Trino
 container, make the desired changes, and then restart the container for the
 changes to take effect:
 
-    docker exec -it trino bash 
-    vi /etc/starburst/catalog/delta.properties
-    exit
+```sh
+docker exec -it trino bash 
+vi /etc/starburst/catalog/delta.properties
+exit
 
-    docker restart trino
+docker restart trino
+```
 
 The properties file can also be edited directly from the module directory prior
 to provisioning the module:
 
-    lib/modules/catalog/<module>/resources/trino/<module>.properties
+```txt
+lib/modules/catalog/<module>/resources/trino/<module>.properties
+```
diff --git a/src/lib/modules/catalog/elasticsearch/elasticsearch.yml b/src/lib/modules/catalog/elasticsearch/elasticsearch.yaml
similarity index 85%
rename from src/lib/modules/catalog/elasticsearch/elasticsearch.yml
rename to src/lib/modules/catalog/elasticsearch/elasticsearch.yaml
index dbf3d54f..27dc4e87 100644
--- a/src/lib/modules/catalog/elasticsearch/elasticsearch.yml
+++ b/src/lib/modules/catalog/elasticsearch/elasticsearch.yaml
@@ -1,10 +1,11 @@
+---
 services:
-  
+
   trino:
     environment:
       MINITRINO_BOOTSTRAP: bootstrap-elasticsearch.sh
     volumes:
-      - ./modules/catalog/elasticsearch/resources/trino/elasticsearch.properties:/etc/starburst/catalog/elasticsearch.properties
+      - ./modules/catalog/elasticsearch/resources/trino/elasticsearch.properties:${ETC}/catalog/elasticsearch.properties
 
   elasticsearch:
     image: elasticsearch:${ELASTICSEARCH_VER}
@@ -14,7 +15,7 @@ services:
       bootstrap.memory_lock: true
       network.host: 0.0.0.0
       network.bind_host: 0.0.0.0
-      xpack.security.enabled: "false"
+      xpack.security.enabled: false
       ES_JAVA_OPTS: -Xms256m -Xmx256m
     ports:
       - 9200:9200
diff --git a/src/lib/modules/catalog/elasticsearch/metadata.json b/src/lib/modules/catalog/elasticsearch/metadata.json
index e1633acd..b2ee4a9d 100644
--- a/src/lib/modules/catalog/elasticsearch/metadata.json
+++ b/src/lib/modules/catalog/elasticsearch/metadata.json
@@ -1,5 +1,5 @@
 {
-    "description": "Elasticsearch catalog module",
-    "incompatibleModules": [],
-    "enterprise": false
-}
\ No newline at end of file
+  "description": "Elasticsearch catalog module",
+  "incompatibleModules": [],
+  "enterprise": false
+}
diff --git a/src/lib/modules/catalog/elasticsearch/readme.md b/src/lib/modules/catalog/elasticsearch/readme.md
index c6bad07d..0170d62e 100644
--- a/src/lib/modules/catalog/elasticsearch/readme.md
+++ b/src/lib/modules/catalog/elasticsearch/readme.md
@@ -4,53 +4,73 @@ This module contains an ES container with some preloaded data. It contains: a
 schema (ES mapping), a table (ES doc mapping), and 500 rows of fake data (ES
 docs).
 
+## Usage
+
+```sh
+minitrino -v provision -m elasticsearch
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m elasticsearch
+
+docker exec -it trino bash 
+trino-cli
+
+trino> SHOW SCHEMAS FROM elasticsearch;
+```
+
 ## Loading Data
 
 Elasticsearch is exposed on `localhost:9200`, so additional data can be loaded
 as follows:
 
-    # Create user index
-    curl -XPUT http://localhost:9200/user?pretty=true;
-
-    # Create user mapping
-    curl -XPUT http://localhost:9200/user/_mapping/profile?include_type_name=true -H 'Content-Type: application/json' -d '
-    {
-        "profile" : {
-            "properties" : {
-                "full_name" : { "type" : "text", "store" : true },
-                "bio" : { "type" : "text", "store" : true },
-                "age" : { "type" : "integer" },
-                "location" : { "type" : "geo_point" },
-                "enjoys_coffee" : { "type" : "boolean" },
-                "created_on" : {
-                    "type" : "date", 
-                    "format" : "date_time" 
-                }
-            }
-        }
+```sh
+# Create user index
+curl -XPUT http://localhost:9200/user?pretty=true;
+# Create user mapping
+curl -XPUT http://localhost:9200/user/_mapping/profile?include_type_name=true -H 'Content-Type: application/json'-d '
+{
+  "profile": {
+    "properties": {
+      "full_name": {
+        "type": "text",
+        "store": true
+      },
+      "bio": {
+        "type": "text",
+        "store": true
+      },
+      "age": {
+        "type": "integer"
+      },
+      "location": {
+        "type": "geo_point"
+      },
+      "enjoys_coffee": {
+        "type": "boolean"
+      },
+      "created_on": {
+        "type": "date",
+        "format": "date_time"
+      }
     }
-    ';
-
-    # Create user profile records
-    curl -XPOST http://localhost:9200/user/profile/1?pretty=true -H 'Content-Type: application/json' -d '
-    {
-        "full_name" : "Andrew Puch",
-        "bio" : "My name is Andrew. I have a short bio.",
-        "age" : 26,
-        "location" : "41.1246110,-73.4232880",
-        "enjoys_coffee" : true,
-        "created_on" : "2015-05-02T14:45:10.000-04:00"
-    }
-    ';
+  }
+}
+';
+# Create user profile records
+curl -XPOST http://localhost:9200/user/profile/1?pretty=true -H 'Content-Type: application/json' -d '
+{
+  "full_name": "Andrew Puch",
+  "bio": "My name is Andrew. I have a short bio.",
+  "age": 26,
+  "location": "41.1246110,-73.4232880",
+  "enjoys_coffee": true,
+  "created_on": "2015-05-02T14:45:10.000-04:00"
+}
+';
+```
 
 If scripting fake data is preferable, reference the bootstrap script leveraged
 by this module, located at:
 
-    lib/modules/catalog/elasticsearch/resources/bootstrap/bootstrap-elasticsearch.sh
-
-## Usage
-
-    minitrino --env STARBURST_VER=<ver> provision --module elasticsearch
-    docker exec -it trino bash 
-    trino-cli
-    trino> show schemas from elasticsearch;
+```sh
+lib/modules/catalog/elasticsearch/resources/bootstrap/bootstrap-elasticsearch.sh
+```
diff --git a/src/lib/modules/catalog/hive/hive.yml b/src/lib/modules/catalog/hive/hive.yaml
similarity index 80%
rename from src/lib/modules/catalog/hive/hive.yml
rename to src/lib/modules/catalog/hive/hive.yaml
index a12b02a0..afe19c63 100644
--- a/src/lib/modules/catalog/hive/hive.yml
+++ b/src/lib/modules/catalog/hive/hive.yaml
@@ -1,3 +1,4 @@
+---
 services:
 
   trino:
@@ -10,9 +11,10 @@ services:
     volumes:
       - ./modules/catalog/hive/resources/trino/hive.properties:/tmp/hive.properties
       - catalogs:/etc/starburst/catalog
-    command: [
-      "sh", "-c", "cp /tmp/hive.properties /etc/starburst/catalog/hive.properties"
-    ]
+    command:
+      - sh
+      - -c
+      - cp /tmp/hive.properties /etc/starburst/catalog/hive.properties
     labels:
       - com.starburst.tests=minitrino
       - com.starburst.tests.module.hive=catalog-hive
@@ -21,7 +23,7 @@ services:
     image: postgres:${POSTGRES_HMS_VER}
     container_name: postgres-hive
     healthcheck:
-      test: [ 'CMD-SHELL', 'pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}']
+      test: ['CMD-SHELL', 'pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}']
       interval: 10s
       timeout: 5s
       retries: 5
@@ -66,13 +68,17 @@ services:
   create-minio-buckets:
     image: minio/mc:${MINIO_MC_VER}
     container_name: create-minio-buckets
-    entrypoint: |
+    entrypoint: |-
       /bin/sh -c "
-      curl -fsSL https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh > /tmp/wait-for-it.sh && \
-      chmod +x /tmp/wait-for-it.sh && \
+      wfi=wait-for-it && \
+      curl -fsSL \\
+        https://raw.githubusercontent.com/vishnubob/$$wfi/master/$$wfi.sh \\
+        > /tmp/$$wfi.sh && \
+      chmod +x /tmp/$$wfi.sh && \
       echo 'Waiting for MinIO to come up...' && \
-      /tmp/wait-for-it.sh minio:9000 --strict --timeout=60 -- echo 'MinIO service is up.' && \
-      /usr/bin/mc alias set minio http://minio:9000 access-key secret-key && \
+      /tmp/$$wfi.sh minio:9000 --strict --timeout=60 && \
+      /usr/bin/mc alias set minio \\
+        http://minio:9000 access-key secret-key && \
       /usr/bin/mc mb minio/sample-bucket/wh/ && \
       tail -f /dev/null
       "
diff --git a/src/lib/modules/catalog/hive/metadata.json b/src/lib/modules/catalog/hive/metadata.json
index ea26a1cf..6d69b837 100644
--- a/src/lib/modules/catalog/hive/metadata.json
+++ b/src/lib/modules/catalog/hive/metadata.json
@@ -1,5 +1,5 @@
 {
-    "description": "Hive catalog module (includes object storage (MinIO) and Hive metastore)",
-    "incompatibleModules": [],
-    "enterprise": false
-}
\ No newline at end of file
+  "description": "Hive catalog module (includes object storage (MinIO) and Hive metastore)",
+  "incompatibleModules": [],
+  "enterprise": false
+}
diff --git a/src/lib/modules/catalog/hive/readme.md b/src/lib/modules/catalog/hive/readme.md
index 43201f45..e685243c 100644
--- a/src/lib/modules/catalog/hive/readme.md
+++ b/src/lib/modules/catalog/hive/readme.md
@@ -15,49 +15,62 @@ The MinIO UI can be viewed at `http://localhost:9001` using `access-key` and
 
 Tables backed by ORC data files can be easily created:
 
-    trino> create schema hive.tiny with (location='s3a://sample-bucket/wh/tiny/');
-    CREATE SCHEMA
-
-    trino> create table hive.tiny.customer as select * from tpch.tiny.customer;
-    CREATE TABLE: 1500 rows
+```sql
+CREATE SCHEMA hive.tiny WITH (location='s3a://sample-bucket/wh/tiny/');
+CREATE TABLE hive.tiny.customer AS SELECT * FROM tpch.tiny.customer;
+```
 
 The ORC data files can be viewed directly in the MinIO bucket via the MinIO UI.
 
 ## Usage
 
-    minitrino --env STARBURST_VER=<ver> provision --module hive
-    docker exec -it trino bash 
-    trino-cli
-    trino> show schemas from hive;
+```sh
+minitrino -v provision -m hive
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m hive
+
+docker exec -it trino bash 
+trino-cli
+
+trino> SHOW SCHEMAS FROM hive;
+```
 
 ## Persistent Storage
 
 This module uses named volumes to persist MinIO and metastore data:
 
-    volumes:
-      postgres-hive-data:
-        labels:
-          - com.starburst.tests=minitrino
-          - com.starburst.tests.module.hive=catalog-hive
-      minio-hive-data:
-        labels:
-          - com.starburst.tests=minitrino
-          - com.starburst.tests.module.hive=catalog-hive
+```yaml
+volumes:
+  postgres-hive-data:
+    labels:
+      - com.starburst.tests=minitrino
+      - com.starburst.tests.module.hive=catalog-hive
+  minio-hive-data:
+    labels:
+      - com.starburst.tests=minitrino
+      - com.starburst.tests.module.hive=catalog-hive
+```
 
 The user-facing implication is that the data in the Hive metastore and the data
 files stored in MinIO are retained even after shutting down and/or removing the
 environment's containers. Minitrino issues a warning about this whenever a
 module with named volumes is deployed––be sure to look out for these warnings:
 
-    [w]  Module '<module>' has persistent volumes associated with it. To delete these volumes, remember to run `minitrino remove --volumes`.
+```log
+[w]  Module '<module>' has persistent volumes associated with it. To delete these volumes, remember to run `minitrino remove --volumes`.
+```
 
 To remove these volumes, run:
 
-    minitrino -v remove --volumes --label com.starburst.tests.module.hive=catalog-hive
+```sh
+minitrino -v remove --volumes --label com.starburst.tests.module.hive=catalog-hive
+```
 
 Or, remove them directly using the Docker CLI:
 
-    docker volume rm minitrino_postgres-hive-data minitrino_minio-hive-data
+```sh
+docker volume rm minitrino_postgres-hive-data minitrino_minio-hive-data
+```
 
 ## Editing the `hive.properties` File
 
@@ -67,13 +80,17 @@ source file being modified on the host. To edit the file, exec into the Trino
 container, make the desired changes, and then restart the container for the
 changes to take effect:
 
-    docker exec -it trino bash 
-    vi /etc/starburst/catalog/hive.properties
-    exit
+```sh
+docker exec -it trino bash 
+vi /etc/starburst/catalog/hive.properties
+exit
 
-    docker restart trino
+docker restart trino
+```
 
 The properties file can also be edited directly from the module directory prior
 to provisioning the module:
 
-    lib/modules/catalog/<module>/resources/trino/<module>.properties
+```txt
+lib/modules/catalog/<module>/resources/trino/<module>.properties
+```
diff --git a/src/lib/modules/catalog/iceberg/iceberg.yml b/src/lib/modules/catalog/iceberg/iceberg.yaml
similarity index 82%
rename from src/lib/modules/catalog/iceberg/iceberg.yml
rename to src/lib/modules/catalog/iceberg/iceberg.yaml
index 26fa79fb..d1fe1c3a 100644
--- a/src/lib/modules/catalog/iceberg/iceberg.yml
+++ b/src/lib/modules/catalog/iceberg/iceberg.yaml
@@ -1,3 +1,4 @@
+---
 services:
 
   trino:
@@ -10,9 +11,10 @@ services:
     volumes:
       - ./modules/catalog/iceberg/resources/trino/iceberg.properties:/tmp/iceberg.properties
       - catalogs:/etc/starburst/catalog
-    command: [
-      "sh", "-c", "cp /tmp/iceberg.properties /etc/starburst/catalog/iceberg.properties"
-    ]
+    command:
+      - sh
+      - -c
+      - cp /tmp/iceberg.properties /etc/starburst/catalog/iceberg.properties
     labels:
       - com.starburst.tests=minitrino
       - com.starburst.tests.module.iceberg=catalog-iceberg
@@ -41,7 +43,7 @@ services:
   # Iceberg S3 filesystem.
 
   # Virtual-style path solution for MinIO:
-  # https://stackoverflow.com/a/73393416 
+  # https://stackoverflow.com/a/73393416
   # ---------------------------------------------------------------------------
 
   minio-iceberg:
@@ -64,17 +66,21 @@ services:
     labels:
       - com.starburst.tests=minitrino
       - com.starburst.tests.module.iceberg=catalog-iceberg
-  
+
   create-minio-iceberg-buckets:
     image: minio/mc:${MINIO_MC_VER}
     container_name: create-minio-iceberg-buckets
-    entrypoint: >
+    entrypoint: |-
       /bin/sh -c "
-      curl -fsSL https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh > /tmp/wait-for-it.sh && \
-      chmod +x /tmp/wait-for-it.sh && \
+      wfi=wait-for-it && \
+      curl -fsSL \\
+        https://raw.githubusercontent.com/vishnubob/$$wfi/master/$$wfi.sh \\
+        > /tmp/$$wfi.sh && \
+      chmod +x /tmp/$$wfi.sh && \
       echo 'Waiting for MinIO to come up...' && \
-      /tmp/wait-for-it.sh minio-iceberg:9000 --strict --timeout=60 -- echo 'MinIO service is up.' && \
-      /usr/bin/mc alias set minio http://minio-iceberg:9000 access-key secret-key && \
+      /tmp/$$wfi.sh minio-iceberg:9000 --strict --timeout=60 && \
+      /usr/bin/mc alias set minio \\
+        http://minio-iceberg:9000 access-key secret-key && \
       /usr/bin/mc mb minio/sample-bucket/wh/ && \
       tail -f /dev/null
       "
diff --git a/src/lib/modules/catalog/iceberg/metadata.json b/src/lib/modules/catalog/iceberg/metadata.json
index c19a1216..e75096a4 100644
--- a/src/lib/modules/catalog/iceberg/metadata.json
+++ b/src/lib/modules/catalog/iceberg/metadata.json
@@ -1,5 +1,5 @@
 {
-    "description": "Iceberg catalog module (uses Iceberg REST catalog for metadata management)",
-    "incompatibleModules": [],
-    "enterprise": false
-}
\ No newline at end of file
+  "description": "Iceberg catalog module (uses Iceberg REST catalog for metadata management)",
+  "incompatibleModules": [],
+  "enterprise": false
+}
diff --git a/src/lib/modules/catalog/iceberg/readme.md b/src/lib/modules/catalog/iceberg/readme.md
index 4272b227..88282e8e 100644
--- a/src/lib/modules/catalog/iceberg/readme.md
+++ b/src/lib/modules/catalog/iceberg/readme.md
@@ -8,40 +8,56 @@ requests](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.h
 
 ## Usage
 
-    minitrino --env STARBURST_VER=<ver> provision --module iceberg
-    docker exec -it trino bash 
-    trino-cli
-    trino> show schemas from hive;
+```sh
+minitrino -v provision -m iceberg
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m iceberg
+
+docker exec -it trino bash 
+trino-cli
+
+trino> SHOW SCHEMAS FROM iceberg;
+```
 
 Create a schema and a table:
 
-    create schema iceberg.test with (location = 's3a://sample-bucket/wh/test');
-    create table iceberg.test.test_tbl as select * from tpch.tiny.customer;
+```sql
+CREATE SCHEMA iceberg.test WITH (location = 's3a://sample-bucket/wh/test');
+create TABLE iceberg.test.test_tbl AS SELECT * FROM tpch.tiny.customer;
+```
 
 ## Persistent Storage
 
 This module uses named volumes to persist MinIO data:
 
-    volumes:
-      minio-iceberg-data:
-        labels:
-          - com.starburst.tests=minitrino
-          - com.starburst.tests.module.iceberg=catalog-iceberg
+```yaml
+volumes:
+  minio-iceberg-data:
+    labels:
+      - com.starburst.tests=minitrino
+      - com.starburst.tests.module.iceberg=catalog-iceberg
+```
 
 The user-facing implication is that the data files stored in MinIO are retained
 even after shutting down and/or removing the environment's containers. Minitrino
 issues a warning about this whenever a module with named volumes is deployed––be
 sure to look out for these warnings:
 
-    [w]  Module '<module>' has persistent volumes associated with it. To delete these volumes, remember to run `minitrino remove --volumes`.
+```log
+[w]  Module '<module>' has persistent volumes associated with it. To delete these volumes, remember to run `minitrino remove --volumes`.
+```
 
 To remove these volumes, run:
 
-    minitrino -v remove --volumes --label com.starburst.tests.module.iceberg=catalog-iceberg
+```sh
+minitrino -v remove --volumes --label com.starburst.tests.module.iceberg=catalog-iceberg
+```
   
 Or, remove them directly using the Docker CLI:
 
-    docker volume rm minitrino_minio-iceberg-data
+```sh
+docker volume rm minitrino_minio-iceberg-data
+```
 
 ## Editing the `iceberg.properties` File
 
@@ -51,13 +67,17 @@ source file being modified on the host. To edit the file, exec into the Trino
 container, make the desired changes, and then restart the container for the
 changes to take effect:
 
-    docker exec -it trino bash 
-    vi /etc/starburst/catalog/iceberg.properties
-    exit
+```sh
+docker exec -it trino bash 
+vi /etc/starburst/catalog/iceberg.properties
+exit
 
-    docker restart trino
+docker restart trino
+```
 
 The properties file can also be edited directly from the module directory prior
 to provisioning the module:
 
-    lib/modules/catalog/<module>/resources/trino/<module>.properties
+```txt
+lib/modules/catalog/<module>/resources/trino/<module>.properties
+```
diff --git a/src/lib/modules/catalog/mariadb/mariadb.yml b/src/lib/modules/catalog/mariadb/mariadb.yaml
similarity index 90%
rename from src/lib/modules/catalog/mariadb/mariadb.yml
rename to src/lib/modules/catalog/mariadb/mariadb.yaml
index ae5a2f77..c7cc881d 100644
--- a/src/lib/modules/catalog/mariadb/mariadb.yml
+++ b/src/lib/modules/catalog/mariadb/mariadb.yaml
@@ -1,8 +1,9 @@
+---
 services:
 
   trino:
     volumes:
-      - ./modules/catalog/mariadb/resources/trino/mariadb.properties:/etc/starburst/catalog/mariadb.properties
+      - ./modules/catalog/mariadb/resources/trino/mariadb.properties:${ETC}/catalog/mariadb.properties
 
   mariadb:
     image: mariadb:${MARIADB_VER}
diff --git a/src/lib/modules/catalog/mariadb/metadata.json b/src/lib/modules/catalog/mariadb/metadata.json
index b9077c44..b03fd1ad 100644
--- a/src/lib/modules/catalog/mariadb/metadata.json
+++ b/src/lib/modules/catalog/mariadb/metadata.json
@@ -1,5 +1,5 @@
 {
-    "description": "MariaDB catalog module",
-    "incompatibleModules": [],
-    "enterprise": false
-}
\ No newline at end of file
+  "description": "MariaDB catalog module",
+  "incompatibleModules": [],
+  "enterprise": false
+}
diff --git a/src/lib/modules/catalog/mariadb/readme.md b/src/lib/modules/catalog/mariadb/readme.md
index 37fe2aec..1e344259 100644
--- a/src/lib/modules/catalog/mariadb/readme.md
+++ b/src/lib/modules/catalog/mariadb/readme.md
@@ -3,13 +3,17 @@
 This module provisions a standalone MariaDB service. By default, it is exposed
 to the internal Docker network only via:
 
-    ports:
-      - :3306
+```yaml
+ports:
+  - :3306
+```
 
 To expose it at the host level, add a port to the left of the colon, e.g.:
 
-    ports:
-      - 3307:3306
+```yaml
+ports:
+  - 3307:3306
+```
 
 This will allow you to connect to the service from any SQL client that supports
 MariaDB drivers on `localhost:3307`. Note that a unique port (`3307`) was used
@@ -17,7 +21,13 @@ here as the MySQL module already claims the host port `3306`.
 
 ## Usage
 
-    minitrino --env STARBURST_VER=<ver> provision --module mariadb
-    docker exec -it trino bash 
-    trino-cli
-    trino> show schemas from mariadb;
+```sh
+minitrino -v provision -m mariadb
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m mariadb
+
+docker exec -it trino bash 
+trino-cli
+
+trino> SHOW SCHEMAS FROM mariadb;
+```
diff --git a/src/lib/modules/catalog/mysql/metadata.json b/src/lib/modules/catalog/mysql/metadata.json
index 0bc87d5a..2301503e 100644
--- a/src/lib/modules/catalog/mysql/metadata.json
+++ b/src/lib/modules/catalog/mysql/metadata.json
@@ -1,5 +1,5 @@
 {
-    "description": "MySQL catalog module",
-    "incompatibleModules": [],
-    "enterprise": false
-}
\ No newline at end of file
+  "description": "MySQL catalog module",
+  "incompatibleModules": [],
+  "enterprise": false
+}
diff --git a/src/lib/modules/catalog/mysql/mysql.yml b/src/lib/modules/catalog/mysql/mysql.yaml
similarity index 91%
rename from src/lib/modules/catalog/mysql/mysql.yml
rename to src/lib/modules/catalog/mysql/mysql.yaml
index ddccb5e9..2f3e3845 100644
--- a/src/lib/modules/catalog/mysql/mysql.yml
+++ b/src/lib/modules/catalog/mysql/mysql.yaml
@@ -1,8 +1,9 @@
+---
 services:
 
   trino:
     volumes:
-      - ./modules/catalog/mysql/resources/trino/mysql.properties:/etc/starburst/catalog/mysql.properties
+      - ./modules/catalog/mysql/resources/trino/mysql.properties:${ETC}/catalog/mysql.properties
 
   mysql:
     image: mysql:${MYSQL_VER}
diff --git a/src/lib/modules/catalog/mysql/readme.md b/src/lib/modules/catalog/mysql/readme.md
index 1400d39c..7793cd52 100644
--- a/src/lib/modules/catalog/mysql/readme.md
+++ b/src/lib/modules/catalog/mysql/readme.md
@@ -3,15 +3,22 @@
 This module provisions a standalone MySQL service. By default, it is exposed
 both to the internal Docker network and the host via:
 
-    ports:
-      - 3306:3306
+```yaml
+ports:
+  - 3306:3306
+```
 
 This will allows users to connect to the service from any SQL client that
 supports MySQL drivers on `localhost:3306`.
 
 ## Usage
 
-    minitrino --env STARBURST_VER=<ver> provision --module mysql
-    docker exec -it trino bash 
-    trino-cli
-    trino> show schemas from mysql;
+```sh
+minitrino -v provision -m mysql
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m mysql
+
+docker exec -it trino bash 
+trino-cli
+trino> SHOW SCHEMAS FROM mysql;
+```
diff --git a/src/lib/modules/catalog/pinot/metadata.json b/src/lib/modules/catalog/pinot/metadata.json
index 9d37bd9e..e5f0cb6d 100644
--- a/src/lib/modules/catalog/pinot/metadata.json
+++ b/src/lib/modules/catalog/pinot/metadata.json
@@ -1,5 +1,5 @@
 {
-    "description": "Pinot catalog module",
-    "incompatibleModules": [],
-    "enterprise": false
-}
\ No newline at end of file
+  "description": "Pinot catalog module",
+  "incompatibleModules": [],
+  "enterprise": false
+}
diff --git a/src/lib/modules/catalog/pinot/pinot.yml b/src/lib/modules/catalog/pinot/pinot.yaml
similarity index 85%
rename from src/lib/modules/catalog/pinot/pinot.yml
rename to src/lib/modules/catalog/pinot/pinot.yaml
index 1eaf4e08..58288c98 100644
--- a/src/lib/modules/catalog/pinot/pinot.yml
+++ b/src/lib/modules/catalog/pinot/pinot.yaml
@@ -1,8 +1,9 @@
+---
 services:
 
   trino:
     volumes:
-      - ./modules/catalog/pinot/resources/trino/pinot.properties:/etc/starburst/catalog/pinot.properties
+      - ./modules/catalog/pinot/resources/trino/pinot.properties:${ETC}/catalog/pinot.properties
 
   pinot-zookeeper:
     image: zookeeper:${ZOOKEEPER_VER}
@@ -15,11 +16,11 @@ services:
     labels:
       - com.starburst.tests=minitrino
       - com.starburst.tests.module.pinot=catalog-pinot
-  
+
   pinot-controller:
     image: apachepinot/pinot:${PINOT_VER}
     container_name: pinot-controller
-    command: "StartController -zkAddress pinot-zookeeper:2181"
+    command: StartController -zkAddress pinot-zookeeper:2181
     restart: unless-stopped
     environment:
       MINITRINO_BOOTSTRAP: bootstrap-pinot.sh
@@ -28,15 +29,15 @@ services:
     depends_on:
       - pinot-zookeeper
     ports:
-      - 9090:9000 # localhost:9090
+      - 9090:9000  # localhost:9090
     labels:
       - com.starburst.tests=minitrino
       - com.starburst.tests.module.pinot=catalog-pinot
-  
+
   pinot-broker:
     image: apachepinot/pinot:${PINOT_VER}
     container_name: pinot-broker
-    command: "StartBroker -zkAddress pinot-zookeeper:2181"
+    command: StartBroker -zkAddress pinot-zookeeper:2181
     restart: unless-stopped
     env_file:
       - ./modules/catalog/pinot/resources/pinot/broker.env
@@ -47,11 +48,11 @@ services:
     labels:
       - com.starburst.tests=minitrino
       - com.starburst.tests.module.pinot=catalog-pinot
-  
+
   pinot-server:
     image: apachepinot/pinot:${PINOT_VER}
     container_name: pinot-server
-    command: "StartServer -zkAddress pinot-zookeeper:2181"
+    command: StartServer -zkAddress pinot-zookeeper:2181
     restart: unless-stopped
     env_file:
       - ./modules/catalog/pinot/resources/pinot/server.env
diff --git a/src/lib/modules/catalog/pinot/readme.md b/src/lib/modules/catalog/pinot/readme.md
index 88aa75d1..a356d6a3 100644
--- a/src/lib/modules/catalog/pinot/readme.md
+++ b/src/lib/modules/catalog/pinot/readme.md
@@ -15,9 +15,15 @@ image.
 
 To deploy the module:
 
-    minitrino --env STARBURST_VER=<ver> provision --module pinot
-    docker exec -it trino bash 
-    trino-cli
-    trino> show schemas from pinot;
+```sh
+minitrino -v provision -m pinot
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m pinot
+
+docker exec -it trino bash 
+trino-cli
+
+trino> SHOW SCHEMAS FROM pinot;
+```
 
 The Pinot web UI can be viewed at `localhost:9090`.
diff --git a/src/lib/modules/catalog/postgres/metadata.json b/src/lib/modules/catalog/postgres/metadata.json
index 27e49da6..29a2b3b1 100644
--- a/src/lib/modules/catalog/postgres/metadata.json
+++ b/src/lib/modules/catalog/postgres/metadata.json
@@ -1,5 +1,5 @@
 {
-    "description": "Postgres catalog module",
-    "incompatibleModules": [],
-    "enterprise": false
-}
\ No newline at end of file
+  "description": "Postgres catalog module",
+  "incompatibleModules": [],
+  "enterprise": false
+}
diff --git a/src/lib/modules/catalog/postgres/postgres.yml b/src/lib/modules/catalog/postgres/postgres.yaml
similarity index 88%
rename from src/lib/modules/catalog/postgres/postgres.yml
rename to src/lib/modules/catalog/postgres/postgres.yaml
index 3e3f5e1b..e8e17d42 100644
--- a/src/lib/modules/catalog/postgres/postgres.yml
+++ b/src/lib/modules/catalog/postgres/postgres.yaml
@@ -1,8 +1,9 @@
+---
 services:
 
   trino:
     volumes:
-      - ./modules/catalog/postgres/resources/trino/postgres.properties:/etc/starburst/catalog/postgres.properties
+      - ./modules/catalog/postgres/resources/trino/postgres.properties:${ETC}/catalog/postgres.properties
 
   postgres:
     image: postgres:${POSTGRES_VER}
diff --git a/src/lib/modules/catalog/postgres/readme.md b/src/lib/modules/catalog/postgres/readme.md
index b808e7c5..817c8598 100644
--- a/src/lib/modules/catalog/postgres/readme.md
+++ b/src/lib/modules/catalog/postgres/readme.md
@@ -3,15 +3,23 @@
 This module provisions a standalone Postgres service. By default, it is exposed
 both to the internal Docker network and the host via:
 
-    ports:
-      - 5432:5432
+```yaml
+ports:
+  - 5432:5432
+```
 
 This will allows users to connect to the service from any SQL client that
 supports Postgres drivers on `localhost:5432`.
 
 ## Usage
 
-    minitrino --env STARBURST_VER=<ver> provision --module postgres
-    docker exec -it trino bash 
-    trino-cli
-    trino> show schemas from postgres;
+```sh
+minitrino -v provision -m postgres
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m postgres
+
+docker exec -it trino bash 
+trino-cli
+
+trino> SHOW SCHEMAS FROM postgres;
+```
diff --git a/src/lib/modules/catalog/sqlserver/metadata.json b/src/lib/modules/catalog/sqlserver/metadata.json
index 78ffb91f..e33f0c80 100644
--- a/src/lib/modules/catalog/sqlserver/metadata.json
+++ b/src/lib/modules/catalog/sqlserver/metadata.json
@@ -1,5 +1,5 @@
 {
-    "description": "SQL Server catalog module",
-    "incompatibleModules": [],
-    "enterprise": false
+  "description": "SQL Server catalog module",
+  "incompatibleModules": [],
+  "enterprise": false
 }
diff --git a/src/lib/modules/catalog/sqlserver/readme.md b/src/lib/modules/catalog/sqlserver/readme.md
index 06202eb9..42e5b606 100644
--- a/src/lib/modules/catalog/sqlserver/readme.md
+++ b/src/lib/modules/catalog/sqlserver/readme.md
@@ -3,15 +3,23 @@
 This module provisions a standalone SQL Server service. By default, it is
 exposed both to the internal Docker network and the host via:
 
-    ports:
-      - 1433:1433
+```yaml
+ports:
+  - 1433:1433
+```
 
 This will allows users to connect to the service from any SQL client that
 supports SQL Server drivers on `localhost:1433`.
 
 ## Usage
 
-    minitrino --env STARBURST_VER=<ver> provision --module sqlserver
-    docker exec -it trino bash 
-    trino-cli
-    trino> show schemas from sqlserver;
+```sh
+minitrino -v provision -m sqlserver
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m sqlserver
+
+docker exec -it trino bash 
+trino-cli
+
+trino> SHOW SCHEMAS FROM sqlserver;
+```
diff --git a/src/lib/modules/catalog/sqlserver/sqlserver.yml b/src/lib/modules/catalog/sqlserver/sqlserver.yaml
similarity index 88%
rename from src/lib/modules/catalog/sqlserver/sqlserver.yml
rename to src/lib/modules/catalog/sqlserver/sqlserver.yaml
index ad1e0a86..61f6cb95 100644
--- a/src/lib/modules/catalog/sqlserver/sqlserver.yml
+++ b/src/lib/modules/catalog/sqlserver/sqlserver.yaml
@@ -1,8 +1,9 @@
+---
 services:
 
   trino:
     volumes:
-      - ./modules/catalog/sqlserver/resources/trino/sqlserver.properties:/etc/starburst/catalog/sqlserver.properties
+      - ./modules/catalog/sqlserver/resources/trino/sqlserver.properties:${ETC}/catalog/sqlserver.properties
 
   sqlserver:
     image: mcr.microsoft.com/mssql/server:${SQLSERVER_VER}
diff --git a/src/lib/modules/security/biac/biac.yml b/src/lib/modules/security/biac/biac.yaml
similarity index 67%
rename from src/lib/modules/security/biac/biac.yml
rename to src/lib/modules/security/biac/biac.yaml
index ad95a137..087cb6aa 100644
--- a/src/lib/modules/security/biac/biac.yml
+++ b/src/lib/modules/security/biac/biac.yaml
@@ -1,15 +1,16 @@
+---
 services:
 
   trino:
-    environment: 
+    environment:
       CONFIG_PROPERTIES: |-
         starburst.access-control.enabled=true
         starburst.access-control.audit.enabled=true
-        starburst.access-control.authorized-users=starburst_service,admin
+        starburst.access-control.authorized-users=admin
         starburst.access-control.authorized-groups=admins,sepadmins
-        access-control.config-files=/etc/starburst/biac.properties
+        access-control.config-files=etc/biac.properties
       MINITRINO_BOOTSTRAP: bootstrap-trino.sh
     volumes:
-      - ./modules/security/biac/resources/trino/biac.properties:/etc/starburst/biac.properties
+      - ./modules/security/biac/resources/trino/biac.properties:${ETC}/biac.properties
     labels:
       - com.starburst.tests.module.biac=security-biac
diff --git a/src/lib/modules/security/biac/metadata.json b/src/lib/modules/security/biac/metadata.json
index 4c155177..316c51bb 100644
--- a/src/lib/modules/security/biac/metadata.json
+++ b/src/lib/modules/security/biac/metadata.json
@@ -1,6 +1,8 @@
 {
   "description": "Built-in access control (BIAC) module",
   "incompatibleModules": [],
-  "dependentModules": ["insights"],
+  "dependentModules": [
+    "insights"
+  ],
   "enterprise": true
 }
diff --git a/src/lib/modules/security/biac/readme.md b/src/lib/modules/security/biac/readme.md
index 018ead7a..5c641966 100644
--- a/src/lib/modules/security/biac/readme.md
+++ b/src/lib/modules/security/biac/readme.md
@@ -5,7 +5,11 @@ integrated with the SEP web UI.
 
 ## Usage
 
-    minitrino --env STARBURST_VER=<ver> provision --module biac  
+```sh
+minitrino -v provision -m biac
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m biac
+```
 
 ## Accessing Roles and Privileges in the SEP UI  
 
@@ -13,7 +17,7 @@ Standalone BIAC:
 
 - Open a web browser and go to [http://localhost:8080](http://localhost:8080)
   (or [https://localhost:8443](hhttps://localhost:8443) if TLS is configured)
-- Log in using an authorized sysadmin user (`admin` or `starburst_service`)
+- Log in using an authorized sysadmin user (`admin`)
 - Click on the username in the top right corner > switch role > `sysadmin`
 
 Once the `sysadmin` role has been assumed, you can begin to create roles and
@@ -28,12 +32,14 @@ Documentation](https://docs.starburst.io/latest/api/index.html#api-_).
 
 ### Example 1: List Roles  
 
-    curl -k --location \
-      -X GET 'http://localhost:8080/api/v1/biac/roles' \
-      -H 'Accept: application/json' \
-      -H 'Content-Type: application/json' \
-      -H 'X-Trino-Role: system=ROLE{sysadmin}' \
-      -u 'admin:'  
+```sh
+curl -k --location \
+  -X GET 'http://localhost:8080/api/v1/biac/roles' \
+  -H 'Accept: application/json' \
+  -H 'Content-Type: application/json' \
+  -H 'X-Trino-Role: system=ROLE{sysadmin}' \
+  -u 'admin:'  
+```
 
 ### Example 2: Adding a User to Authorized Users  
 
@@ -41,32 +47,38 @@ The following API POST request adds user `Alice` to authorized users. After
 performing the following successfully, Alice will be able to access BIAC
 features in the SEP UI.
 
-    curl -k --location \
-      -X POST 'http://localhost:8080/api/v1/biac/subjects/users/alice/assignments' \
-      -H 'Accept: application/json' \
-      -H 'Content-Type: application/json' \
-      -H 'X-Trino-Role: system=ROLE{sysadmin}' \
-      -u 'admin:' \
-      -d '{ "roleId":"-2", "roleAdmin":"true"}'
+```sh
+curl -k --location \
+  -X POST 'http://localhost:8080/api/v1/biac/subjects/users/alice/assignments' \
+  -H 'Accept: application/json' \
+  -H 'Content-Type: application/json' \
+  -H 'X-Trino-Role: system=ROLE{sysadmin}' \
+  -u 'admin:' \
+  -d '{ "roleId":"-2", "roleAdmin":"true"}'
+```
 
 ### Example 3: Get Role Assignments for a Role
 
 The following API GET request returns users/groups assigned to the `sysadmin`
 role which is defined by `roleId=-1`.  
 
-    curl -k --location \
-      -X GET 'http://localhost:8080/api/v1/biac/roles/-2/assignments?pageToken=&pageSize=&pageSort=' \
-      -H 'Accept: application/json' \
-      -H 'Content-Type: application/json' \
-      -H 'X-Trino-Role: system=ROLE{sysadmin}' \
-      -u 'admin:'
+```sh
+curl -k --location \
+  -X GET 'http://localhost:8080/api/v1/biac/roles/-2/assignments?pageToken=&pageSize=&pageSort=' \
+  -H 'Accept: application/json' \
+  -H 'Content-Type: application/json' \
+  -H 'X-Trino-Role: system=ROLE{sysadmin}' \
+  -u 'admin:'
+```
 
 ### Example 4: Create a Role
 
-    curl -k --location \
-      -X POST 'http://localhost:8080/api/v1/biac/roles' \
-      -H 'Accept: application/json' \
-      -H 'Content-Type: application/json' \
-      -H 'X-Trino-Role: system=ROLE{sysadmin}' \
-      -u 'admin:' \
-      -d '{ "name": "test_role", "description":"test creating new BIAC role"}'
+```sh
+curl -k --location \
+  -X POST 'http://localhost:8080/api/v1/biac/roles' \
+  -H 'Accept: application/json' \
+  -H 'Content-Type: application/json' \
+  -H 'X-Trino-Role: system=ROLE{sysadmin}' \
+  -u 'admin:' \
+  -d '{ "name": "test_role", "description":"test creating new BIAC role"}'
+```
diff --git a/src/lib/modules/security/file-access-control/file-access-control.yml b/src/lib/modules/security/file-access-control/file-access-control.yaml
similarity index 71%
rename from src/lib/modules/security/file-access-control/file-access-control.yml
rename to src/lib/modules/security/file-access-control/file-access-control.yaml
index 1daab1b6..8a3e5a30 100644
--- a/src/lib/modules/security/file-access-control/file-access-control.yml
+++ b/src/lib/modules/security/file-access-control/file-access-control.yaml
@@ -1,8 +1,9 @@
+---
 services:
 
   trino:
     volumes:
-      - ./modules/security/file-access-control/resources/trino/access-control.properties:/etc/starburst/access-control.properties
-      - ./modules/security/file-access-control/resources/trino/rules.json:/etc/starburst/rules.json
+      - ./modules/security/file-access-control/resources/trino/access-control.properties:${ETC}/access-control.properties
+      - ./modules/security/file-access-control/resources/trino/rules.json:${ETC}/rules.json
     labels:
       - com.starburst.tests.module.file-access-control=security-file-access-control
diff --git a/src/lib/modules/security/file-access-control/metadata.json b/src/lib/modules/security/file-access-control/metadata.json
index e78aa77a..dba3a042 100644
--- a/src/lib/modules/security/file-access-control/metadata.json
+++ b/src/lib/modules/security/file-access-control/metadata.json
@@ -1,6 +1,10 @@
 {
-    "description": "File-based access control module",
-    "incompatibleModules": [],
-    "dependentModules": ["file-group-provider"],
-    "enterprise": false
-}
\ No newline at end of file
+  "description": "File-based access control module",
+  "incompatibleModules": [
+    "biac"
+  ],
+  "dependentModules": [
+    "file-group-provider"
+  ],
+  "enterprise": false
+}
diff --git a/src/lib/modules/security/file-access-control/readme.md b/src/lib/modules/security/file-access-control/readme.md
index 90fc5c6f..05084b45 100644
--- a/src/lib/modules/security/file-access-control/readme.md
+++ b/src/lib/modules/security/file-access-control/readme.md
@@ -5,6 +5,29 @@ plugin](https://docs.starburst.io/latest/security/file-system-access-control.htm
 This module also makes used of the [file-based group
 provider](https://docs.starburst.io/latest/security/group-file.html).
 
+## Usage
+
+```sh
+minitrino -v provision -m file-access-control
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m file-access-control
+
+docker exec -it trino bash 
+trino-cli --user admin
+
+trino> SHOW SCHEMAS FROM tpch;
+```
+
+You will need to supply a username to the Trino CLI in order to map to a group
+(see `lib/modules/security/file-access-control/resources/trino/group.txt` for
+which users belong to which groups). Example:
+
+```sh
+trino-cli --user admin
+trino-cli --user metadata-user
+trino-cli --user platform-user
+```
+
 ## Policies
 
 The access policy is located in the `rules.json` file which defines groups of
@@ -12,25 +35,10 @@ users that map to certain access control permissions. The users for the groups
 are defined in the `groups.txt` file.
 
 - Users in the `sepadmins` group have full access to all objects within Trino
-- Users in the `metadata-users` group only have access to the tables within the
-  `system.metadata` schema
+- Users in the `metadata-users` group have access to the tables within the
+  `system.metadata`, `system.jdbc`, and `system.information_schema` schemas
 - Users in the `platform-users` group only have access to the tables within the
   `system.runtime` schema
 
 You can modify this module to further specify access control permissions to
 other catalogs provisioned with other Minitrino modules.
-
-## Usage
-
-    minitrino --env STARBURST_VER=<ver> provision --module file-access-control
-    docker exec -it trino bash 
-    trino-cli --user admin
-    trino> show schemas from tpch;
-
-You will need to supply a username to the Trino CLI in order to map to a group
-(see `lib/modules/security/file-access-control/resources/trino/group.txt` for
-which users belong to which groups). Example:
-
-    trino-cli --user admin-2
-    trino-cli --user metadata-1
-    trino-cli --user platform
diff --git a/src/lib/modules/security/file-access-control/resources/trino/access-control.properties b/src/lib/modules/security/file-access-control/resources/trino/access-control.properties
index e8e9c7ce..bae79b0d 100644
--- a/src/lib/modules/security/file-access-control/resources/trino/access-control.properties
+++ b/src/lib/modules/security/file-access-control/resources/trino/access-control.properties
@@ -1,3 +1,3 @@
 access-control.name=file
-security.config-file=/etc/starburst/rules.json
+security.config-file=etc/rules.json
 security.refresh-period=5s
diff --git a/src/lib/modules/security/file-access-control/resources/trino/rules.json b/src/lib/modules/security/file-access-control/resources/trino/rules.json
index dce58a06..38417d95 100644
--- a/src/lib/modules/security/file-access-control/resources/trino/rules.json
+++ b/src/lib/modules/security/file-access-control/resources/trino/rules.json
@@ -1,90 +1,112 @@
 {
-   "catalogs":[
-      {
-         "group":"sepadmins",
-         "catalog":".*",
-         "allow":"all"
-      },
-      {
-         "group":"metadata-users|platform-users",
-         "catalog":"system",
-         "allow":"all"
-      },
-      {
-         "catalog":"system",
-         "allow":"none"
-      },
-      {
-         "catalog":".*",
-         "allow":"none"
-      }
-   ],
-   "schemas":[
-      {
-         "group":"sepadmins",
-         "schema":".*",
-         "owner":true
-      },
-      {
-         "group":"metadata-users",
-         "schema":"system.metadata",
-         "owner":true
-      },
-      {
-         "group":"platform-users",
-         "schema":"system.runtime",
-         "owner":true
-      },
-      {
-         "catalog":".*",
-         "schema":".*",
-         "owner":false
-      }
-   ],
-   "tables":[
-      {
-         "group":"sepadmins",
-         "catalog":".*",
-         "schema":".*",
-         "privileges":[
-            "SELECT",
-            "INSERT",
-            "DELETE",
-            "OWNERSHIP",
-            "GRANT_SELECT"
-         ]
-      },
-      {
-         "group":"metadata-users",
-         "catalog":"system",
-         "schema":"metadata",
-         "privileges":[
-            "SELECT",
-            "INSERT",
-            "DELETE",
-            "OWNERSHIP",
-            "GRANT_SELECT"
-         ]
-      },
-      {
-         "group":"platform-users",
-         "catalog":"system",
-         "schema":"runtime",
-         "privileges":[
-            "SELECT",
-            "INSERT",
-            "DELETE",
-            "OWNERSHIP",
-            "GRANT_SELECT"
-         ]
-      },
-      {
-         "group":".*",
-         "catalog":".*",
-         "schema":".*",
-         "privileges":[
-            
-         ]
-      }
-   ]
-}
\ No newline at end of file
+  "catalogs": [
+    {
+      "group": "sepadmins",
+      "catalog": ".*",
+      "allow": "all"
+    },
+    {
+      "group": "metadata-users|platform-users",
+      "catalog": "system",
+      "allow": "all"
+    },
+    {
+      "catalog": "system",
+      "allow": "none"
+    },
+    {
+      "catalog": ".*",
+      "allow": "none"
+    }
+  ],
+  "schemas": [
+    {
+      "group": "sepadmins",
+      "schema": ".*",
+      "owner": true
+    },
+    {
+      "group": "metadata-users",
+      "schema": "system.*",
+      "owner": true
+    },
+    {
+      "group": "platform-users",
+      "schema": "system.runtime",
+      "owner": true
+    },
+    {
+      "catalog": ".*",
+      "schema": ".*",
+      "owner": false
+    }
+  ],
+  "tables": [
+    {
+      "group": "sepadmins",
+      "catalog": ".*",
+      "schema": ".*",
+      "privileges": [
+        "SELECT",
+        "INSERT",
+        "DELETE",
+        "OWNERSHIP",
+        "GRANT_SELECT"
+      ]
+    },
+    {
+      "group": "metadata-users",
+      "catalog": "system",
+      "schema": "metadata",
+      "privileges": [
+        "SELECT",
+        "INSERT",
+        "DELETE",
+        "OWNERSHIP",
+        "GRANT_SELECT"
+      ]
+    },
+    {
+      "group": "metadata-users",
+      "catalog": "system",
+      "schema": "jdbc",
+      "privileges": [
+        "SELECT",
+        "INSERT",
+        "DELETE",
+        "OWNERSHIP",
+        "GRANT_SELECT"
+      ]
+    },
+    {
+      "group": "metadata-users",
+      "catalog": "system",
+      "schema": "information_schema",
+      "privileges": [
+        "SELECT",
+        "INSERT",
+        "DELETE",
+        "OWNERSHIP",
+        "GRANT_SELECT"
+      ]
+    },
+    {
+      "group": "platform-users",
+      "catalog": "system",
+      "schema": "runtime",
+      "privileges": [
+        "SELECT",
+        "INSERT",
+        "DELETE",
+        "OWNERSHIP",
+        "GRANT_SELECT"
+      ]
+    },
+    {
+      "group": ".*",
+      "catalog": ".*",
+      "schema": ".*",
+      "privileges": []
+    }
+  ]
+}
diff --git a/src/lib/modules/security/ldap/ldap.yml b/src/lib/modules/security/ldap/ldap.yaml
similarity index 79%
rename from src/lib/modules/security/ldap/ldap.yml
rename to src/lib/modules/security/ldap/ldap.yaml
index 8d7333b5..82773561 100755
--- a/src/lib/modules/security/ldap/ldap.yml
+++ b/src/lib/modules/security/ldap/ldap.yaml
@@ -1,3 +1,4 @@
+---
 services:
 
   trino:
@@ -5,11 +6,11 @@ services:
       CONFIG_PROPERTIES: http-server.authentication.type=PASSWORD
       MINITRINO_BOOTSTRAP: bootstrap-trino.sh
     volumes:
-      - ./modules/security/ldap/resources/trino/password-authenticator.properties:/etc/starburst/password-authenticator.properties
-      - ./modules/security/ldap/resources/ldap/ldap-users:/etc/starburst/ldap-users
+      - ./modules/security/ldap/resources/trino/password-authenticator.properties:${ETC}/password-authenticator.properties
+      - ./modules/security/ldap/resources/ldap/ldap-users:${ETC}/ldap-users
     links:
       - ldap:ldap.minitrino.starburstdata.com
-  
+
   ldap:
     image: osixia/openldap:${OPEN_LDAP_VER}
     container_name: ldap
@@ -19,7 +20,7 @@ services:
       LDAP_DOMAIN: example.com
       LDAP_ADMIN_PASSWORD: trinoRocks15
       LDAP_TLS_VERIFY_CLIENT: try
-      LDAPTLS_REQCERT: never 
+      LDAPTLS_REQCERT: never
     ports:
       - 636:636
     labels:
diff --git a/src/lib/modules/security/ldap/metadata.json b/src/lib/modules/security/ldap/metadata.json
index d464a196..186ab45e 100644
--- a/src/lib/modules/security/ldap/metadata.json
+++ b/src/lib/modules/security/ldap/metadata.json
@@ -1,6 +1,8 @@
 {
-    "description": "LDAP password authenticator module",
-    "incompatibleModules": [],
-    "dependentModules": ["tls"],
-    "enterprise": false
-}
\ No newline at end of file
+  "description": "LDAP password authenticator module",
+  "incompatibleModules": [],
+  "dependentModules": [
+    "tls"
+  ],
+  "enterprise": false
+}
diff --git a/src/lib/modules/security/ldap/readme.md b/src/lib/modules/security/ldap/readme.md
index cf475041..06702188 100644
--- a/src/lib/modules/security/ldap/readme.md
+++ b/src/lib/modules/security/ldap/readme.md
@@ -4,44 +4,56 @@ This module provisions an LDAP server for authenticating users in Trino.
 
 ## Usage
 
-    minitrino --env STARBURST_VER=<ver> provision --module ldap
-    docker exec -it trino bash 
-    
-    trino-cli --server https://trino:8443 \
-       --truststore-path /etc/starburst/tls-mnt/truststore.jks --truststore-password changeit \
-       --user bob --password
+```sh
+minitrino -v provision -m ldap
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m ldap
 
-    trino> show schemas from tpch;
+docker exec -it trino bash 
+
+trino-cli --server https://trino:8443 \
+  --truststore-path /etc/starburst/tls-mnt/truststore.jks \
+  --truststore-password changeit \
+  --user bob --password
+
+trino> SHOW SCHEMAS FROM tpch;
+```
 
 ## Default Usernames and Passwords
 
-- admin / trinoRocks15
-- alice / trinoRocks15
-- bob / trinoRocks15
+- `admin` / `trinoRocks15`
+- `alice` / `trinoRocks15`
+- `bob` / `trinoRocks15`
 
 ## Add a New User to LDAP
 
 1. Open a shell to the LDAP container
 
-        docker exec -it ldap bash
+```sh
+docker exec -it ldap bash
+```
 
 2. Create an LDIF file with the new user information:
 
-        cat << EOF > jeff.ldif
-        dn: uid=jeff,dc=example,dc=com
-        changetype: add
-        uid: jeff
-        objectClass: inetOrgPerson
-        objectClass: organizationalPerson
-        objectClass: person
-        objectClass: top
-        cn: jeff
-        sn: jeff
-        mail: jeff@example.com
-        userPassword: trinoRocks15
-        EOF
+```sh
+cat << EOF > jeff.ldif
+dn: uid=jeff,dc=example,dc=com
+changetype: add
+uid: jeff
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+cn: jeff
+sn: jeff
+mail: jeff@example.com
+userPassword: trinoRocks15
+EOF
+```
 
 3. Use the `ldapmodify` tool to add the new user
 
-        ldapmodify -x -D "cn=admin,dc=example,dc=com" \
-            -w trinoRocks15 -H ldaps://ldap:636 -f jeff.ldif
+```sh
+ldapmodify -x -D "cn=admin,dc=example,dc=com" \
+  -w trinoRocks15 -H ldaps://ldap:636 -f jeff.ldif
+```
diff --git a/src/lib/modules/security/oauth2/metadata.json b/src/lib/modules/security/oauth2/metadata.json
index d207adeb..28700005 100644
--- a/src/lib/modules/security/oauth2/metadata.json
+++ b/src/lib/modules/security/oauth2/metadata.json
@@ -1,6 +1,8 @@
 {
-    "description": "OAuth2 authenticator module",
-    "incompatibleModules": [],
-    "dependentModules": ["tls"],
-    "enterprise": false
-}
\ No newline at end of file
+  "description": "OAuth2 authenticator module",
+  "incompatibleModules": [],
+  "dependentModules": [
+    "tls"
+  ],
+  "enterprise": false
+}
diff --git a/src/lib/modules/security/oauth2/oauth2.yml b/src/lib/modules/security/oauth2/oauth2.yaml
similarity index 97%
rename from src/lib/modules/security/oauth2/oauth2.yml
rename to src/lib/modules/security/oauth2/oauth2.yaml
index 16a7bab9..c5f63c7c 100755
--- a/src/lib/modules/security/oauth2/oauth2.yml
+++ b/src/lib/modules/security/oauth2/oauth2.yaml
@@ -1,3 +1,4 @@
+---
 services:
 
   trino:
@@ -14,7 +15,7 @@ services:
         condition: service_healthy
     extra_hosts:
       host.docker.internal: host-gateway
-  
+
   oauth2-server:
     image: ghcr.io/navikt/mock-oauth2-server:${OAUTH2_SERVER_VER}
     container_name: oauth2-server
@@ -42,7 +43,7 @@ services:
       retries: 3
       start_period: 10s
       timeout: 10s
-    command: [ tail, -f, /dev/null ]
+    command: [tail, -f, /dev/null]
     labels:
       - com.starburst.tests=minitrino
       - com.starburst.tests.module.oauth2=security-oauth2
diff --git a/src/lib/modules/security/oauth2/readme.md b/src/lib/modules/security/oauth2/readme.md
index 370ce004..54b017f3 100644
--- a/src/lib/modules/security/oauth2/readme.md
+++ b/src/lib/modules/security/oauth2/readme.md
@@ -2,6 +2,22 @@
 
 This module secures Trino with OAuth2 authentication.
 
+## Usage
+
+```sh
+minitrino -v provision -m oauth2
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m oauth2
+```
+
+Once deployed, visit Starburst on `https://localhost:8443` and work through the
+authentication process. You will be redirected to a service on
+`https://host.docker.internal:8100` to facilitate the OAuth2 flow.
+
+On each page, you will need to bypass your browser's security warnings since the
+TLS certificates are self-signed. On Google Chrome, you can bypass this warning
+by typing `thisisunsafe` with the browser window in focus.
+
 ## Prerequisites
 
 Prior to deploying this module, you must authenticate to the Github container
@@ -11,24 +27,14 @@ in order to pull the `ghcr.io/navikt/mock-oauth2-server` image. Additionally,
 you must set this in the `/etc/hosts` file on your machine
 [(docs)](https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host):
 
-    127.0.0.1    host.docker.internal
+```txt
+127.0.0.1    host.docker.internal
+```
 
 The `hosts` file modification allows for all Docker services to be exposed on
 your host at `host.docker.internal:${PORT}`. This is a requirement given the
 nature of the redirects and callbacks that occur during the OAuth2 flow.
 
-## Usage
-
-    minitrino --env STARBURST_VER=<ver> provision --module oauth2
-
-Once deployed, visit Starburst on `https://localhost:8443` and work through the
-authentication process. You will be redirected to a service on
-`https://host.docker.internal:8100` to facilitate the OAuth2 flow.
-
-On each page, you will need to bypass your browser's security warnings since the
-TLS certificates are self-signed. On Google Chrome, you can bypass this warning
-by typing `thisisunsafe` with the browser window in focus.
-
 ## Development Notes
 
 `host.docker.internal` allows the callbacks/redirects between SEP and OAuth2
@@ -43,12 +49,14 @@ work.
 
 The `keytool` command used to generate the OAuth2 server certificate:
 
-    keytool -genkeypair \
-      -alias oauth2-server \
-      -keyalg RSA \
-      -keystore keystore.jks \
-      -keypass changeit \
-      -validity 9999 \
-      -storepass changeit \
-      -dname "CN=host.docker.internal" \
-      -ext san=dns:host.docker.internal,dns:oauth2-server,dns:localhost
+```sh
+keytool -genkeypair \
+  -alias oauth2-server \
+  -keyalg RSA \
+  -keystore keystore.jks \
+  -keypass changeit \
+  -validity 9999 \
+  -storepass changeit \
+  -dname "CN=host.docker.internal" \
+  -ext san=dns:host.docker.internal,dns:oauth2-server,dns:localhost
+```
diff --git a/src/lib/modules/security/oauth2/resources/bootstrap/bootstrap-trino.sh b/src/lib/modules/security/oauth2/resources/bootstrap/bootstrap-trino.sh
index b0a6f53a..62388a55 100755
--- a/src/lib/modules/security/oauth2/resources/bootstrap/bootstrap-trino.sh
+++ b/src/lib/modules/security/oauth2/resources/bootstrap/bootstrap-trino.sh
@@ -8,7 +8,7 @@ openssl s_client -connect oauth2-server:8100 2>/dev/null </dev/null \
     > /tmp/oauth2-server.pem
 
 echo "Adding OAuth2 server certificate to truststore..."
-sudo keytool -import -noprompt -trustcacerts \
+keytool -import -noprompt -trustcacerts \
     -storepass changeit \
     -alias oauth2-server \
     -file /tmp/oauth2-server.pem \
diff --git a/src/lib/modules/security/oauth2/resources/oauth2/config.json b/src/lib/modules/security/oauth2/resources/oauth2/config.json
index 9b3263ea..8b8ae145 100644
--- a/src/lib/modules/security/oauth2/resources/oauth2/config.json
+++ b/src/lib/modules/security/oauth2/resources/oauth2/config.json
@@ -27,4 +27,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/lib/modules/security/password-file/metadata.json b/src/lib/modules/security/password-file/metadata.json
index b5921cc3..e47cd5ae 100644
--- a/src/lib/modules/security/password-file/metadata.json
+++ b/src/lib/modules/security/password-file/metadata.json
@@ -1,6 +1,8 @@
 {
-    "description": "File-based password authenticator module",
-    "incompatibleModules": [],
-    "dependentModules": ["tls"],
-    "enterprise": false
-}
\ No newline at end of file
+  "description": "File-based password authenticator module",
+  "incompatibleModules": [],
+  "dependentModules": [
+    "tls"
+  ],
+  "enterprise": false
+}
diff --git a/src/lib/modules/security/password-file/password-file.yml b/src/lib/modules/security/password-file/password-file.yaml
similarity index 79%
rename from src/lib/modules/security/password-file/password-file.yml
rename to src/lib/modules/security/password-file/password-file.yaml
index 861e128a..268e06e8 100755
--- a/src/lib/modules/security/password-file/password-file.yml
+++ b/src/lib/modules/security/password-file/password-file.yaml
@@ -1,3 +1,4 @@
+---
 services:
 
   trino:
@@ -5,7 +6,6 @@ services:
       CONFIG_PROPERTIES: http-server.authentication.type=PASSWORD
       MINITRINO_BOOTSTRAP: bootstrap-trino.sh
     volumes:
-      - ./modules/security/password-file/resources/trino/password-authenticator.properties:/etc/starburst/password-authenticator.properties
+      - ./modules/security/password-file/resources/trino/password-authenticator.properties:${ETC}/password-authenticator.properties
     labels:
       - com.starburst.tests.module.password-file=security-password-file
-  
\ No newline at end of file
diff --git a/src/lib/modules/security/password-file/readme.md b/src/lib/modules/security/password-file/readme.md
index 86c1dbb1..6191b24d 100644
--- a/src/lib/modules/security/password-file/readme.md
+++ b/src/lib/modules/security/password-file/readme.md
@@ -4,23 +4,30 @@ This module configures Trino to authenticate users with a password file.
 
 ## Usage
 
-    minitrino --env STARBURST_VER=<ver> provision --module password-file
-    docker exec -it trino bash 
+```sh
+minitrino -v provision -m password-file
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m password-file
 
-    trino-cli --server https://trino:8443 \
-       --truststore-path /etc/starburst/tls-mnt/truststore.jks --truststore-password changeit \
-       --user bob --password
-       
-    trino> show schemas from tpch;
+docker exec -it trino bash 
+
+trino-cli --server https://trino:8443 \
+  --truststore-path /etc/starburst/tls-mnt/truststore.jks --truststore-password changeit \
+  --user bob --password
+    
+trino> SHOW SCHEMAS FROM tpch;
+```
 
 ## Default Usernames and Passwords
 
-- admin / trinoRocks15
-- alice / trinoRocks15
-- bob / trinoRocks15
+- `admin` / `trinoRocks15`
+- `alice` / `trinoRocks15`
+- `bob` / `trinoRocks15`
 
 ## Adding a New User to the Password File
 
 Example with username `admin` and password `trinoRocks15`
 
-    docker exec trino htpasswd -bB -C 10 /etc/starburst/password.db admin trinoRocks15
+```sh
+docker exec trino htpasswd -bB -C 10 /etc/starburst/password.db admin trinoRocks15
+```
diff --git a/src/lib/modules/security/password-file/resources/trino/password-authenticator.properties b/src/lib/modules/security/password-file/resources/trino/password-authenticator.properties
index ad386170..1b09887a 100644
--- a/src/lib/modules/security/password-file/resources/trino/password-authenticator.properties
+++ b/src/lib/modules/security/password-file/resources/trino/password-authenticator.properties
@@ -1,2 +1,2 @@
 password-authenticator.name=file
-file.password-file=/etc/starburst/password.db
+file.password-file=etc/password.db
diff --git a/src/lib/modules/security/tls/metadata.json b/src/lib/modules/security/tls/metadata.json
index 221c1858..90d301c9 100644
--- a/src/lib/modules/security/tls/metadata.json
+++ b/src/lib/modules/security/tls/metadata.json
@@ -1,4 +1,4 @@
 {
-    "description": "TLS module (configures TLS on the Trino coordinator)",
-    "enterprise": false
-}
\ No newline at end of file
+  "description": "TLS module (configures TLS on the Trino coordinator)",
+  "enterprise": false
+}
diff --git a/src/lib/modules/security/tls/readme.md b/src/lib/modules/security/tls/readme.md
index 4d72a638..eecd7f52 100644
--- a/src/lib/modules/security/tls/readme.md
+++ b/src/lib/modules/security/tls/readme.md
@@ -3,6 +3,14 @@
 This module configures server TLS on the Trino container using a self-signed
 certificate.
 
+## Usage
+
+```sh
+minitrino -v provision -m tls
+# Or specify Starburst version
+minitrino -v -e STARBURST_VER=${version} provision -m tls
+```
+
 ## Client Keystore and Truststore
 
 The Java keystore and truststore needed for clients and drivers to securely
@@ -14,21 +22,25 @@ provisioned with a security module that enables SSL.
 
 Via Docker:
 
-    docker exec -it trino trino-cli \
-        --server https://trino:8443 \
-        --truststore-path /etc/starburst/tls-mnt/truststore.jks \
-        --truststore-password changeit
+```sh
+docker exec -it trino trino-cli \
+  --server https://trino:8443 \
+  --truststore-path /etc/starburst/tls-mnt/truststore.jks \
+  --truststore-password changeit
+```
 
-Via Host Machine:
+Via host machine:
 
-    trino-cli-xxx-executable.jar \
-        --server https://localhost:8443 \
-        --truststore-path ~/.minitrino/tls-mnt/truststore.jks 
-        --truststore-password changeit
+```sh
+trino-cli-xxx-executable.jar \
+  --server https://localhost:8443 \
+  --truststore-path ~/.minitrino/tls-mnt/truststore.jks 
+  --truststore-password changeit
+```
 
 ## Accessing the Trino Web UI
 
-Open a web browser and go to <https://localhost:8443>. To have the browser
+Open a web browser and go to `https://localhost:8443`. To have the browser
 accept the self-signed certificate, do the following:
 
 **Chrome**: Click anywhere on the page and type `thisisunsafe`.
diff --git a/src/lib/modules/security/tls/tls.yml b/src/lib/modules/security/tls/tls.yaml
similarity index 72%
rename from src/lib/modules/security/tls/tls.yml
rename to src/lib/modules/security/tls/tls.yaml
index 57ffd3d9..602ac7e6 100755
--- a/src/lib/modules/security/tls/tls.yml
+++ b/src/lib/modules/security/tls/tls.yaml
@@ -1,3 +1,4 @@
+---
 services:
 
   trino:
@@ -5,12 +6,12 @@ services:
       CONFIG_PROPERTIES: |-
         http-server.https.enabled=true
         http-server.https.port=8443
-        http-server.https.keystore.path=/etc/starburst/tls-mnt/keystore.jks
+        http-server.https.keystore.path=etc/tls-mnt/keystore.jks
         http-server.https.keystore.key=changeit
       MINITRINO_BOOTSTRAP: bootstrap-trino.sh
     ports:
       - 8443:8443
     volumes:
-      - ~/.minitrino/tls-mnt:/etc/starburst/tls-mnt
+      - ~/.minitrino/tls-mnt:${ETC}/tls-mnt
     labels:
       - com.starburst.tests.module.tls=security-tls
diff --git a/src/lib/version b/src/lib/version
index 63a1a1ca..ccbccc3d 100644
--- a/src/lib/version
+++ b/src/lib/version
@@ -1,1 +1,1 @@
-2.1.9
+2.2.0


Test patch:
diff --git a/.github/workflows/cli-tests.yml b/.github/workflows/cli-tests.yaml
similarity index 93%
rename from .github/workflows/cli-tests.yml
rename to .github/workflows/cli-tests.yaml
index 70cf4752..da50b7be 100644
--- a/.github/workflows/cli-tests.yml
+++ b/.github/workflows/cli-tests.yaml
@@ -1,3 +1,4 @@
+---
 name: CLI Tests
 
 on:
@@ -16,7 +17,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v4
         with:
-          python-version: '3.10'      
+          python-version: '3.10'
       - name: Install project dependencies
         run: |-
           pip install --editable ./src/cli/
diff --git a/.github/workflows/lib-tests.yml b/.github/workflows/lib-tests.yaml
similarity index 95%
rename from .github/workflows/lib-tests.yml
rename to .github/workflows/lib-tests.yaml
index 3a10eadb..ee5f3e6c 100644
--- a/.github/workflows/lib-tests.yml
+++ b/.github/workflows/lib-tests.yaml
@@ -1,3 +1,4 @@
+---
 name: Library Tests
 
 on:
@@ -16,7 +17,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v4
         with:
-          python-version: '3.10'      
+          python-version: '3.10'
       - name: Install project dependencies
         run: |-
           pip install --editable ./src/cli/
diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yaml
similarity index 94%
rename from .github/workflows/test-release.yml
rename to .github/workflows/test-release.yaml
index 9ae9a94b..6140d36b 100644
--- a/.github/workflows/test-release.yml
+++ b/.github/workflows/test-release.yaml
@@ -1,3 +1,4 @@
+---
 name: Create Test Release (0.0.0) from PR Branch
 
 on:
@@ -18,7 +19,8 @@ jobs:
           GITHUB_TOKEN: ${{ secrets.MINITRINO_TOKEN }}
         run: |-
           TAG="0.0.0"
-          DESCRIPTION="Used as a placeholder to give test builds access to the PR branch's module library."
+          DESCRIPTION="Used as a placeholder to give test builds access \
+          to the PR branch's module library."
 
           echo "Checking if release exists..."
           if gh release list | grep -q -e "${TAG}"; then
diff --git a/src/lib/modules/catalog/test-enterprise/metadata.json b/src/lib/modules/catalog/test-enterprise/metadata.json
deleted file mode 100644
index d77beccc..00000000
--- a/src/lib/modules/catalog/test-enterprise/metadata.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-    "description": "Test module.",
-    "incompatibleModules": [],
-    "dependentModules": [],
-    "enterprise": true
-}
diff --git a/src/lib/modules/catalog/test-enterprise/test-enterprise.yml b/src/lib/modules/catalog/test-enterprise/test-enterprise.yml
deleted file mode 100644
index 0eae3866..00000000
--- a/src/lib/modules/catalog/test-enterprise/test-enterprise.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-services:
-
-  trino:
-    environment:
-      TESTING: True
-    labels:
-      - com.starburst.tests.module.test-enterprise=catalog-test-enterprise
diff --git a/src/lib/modules/catalog/test/metadata.json b/src/lib/modules/catalog/test/metadata.json
index 012d8fbc..7e9cf4a0 100644
--- a/src/lib/modules/catalog/test/metadata.json
+++ b/src/lib/modules/catalog/test/metadata.json
@@ -1,6 +1,11 @@
 {
-    "description": "Test module.",
-    "incompatibleModules": ["ldap"],
-    "dependentModules": ["file-access-control", "file-access-control"],
-    "enterprise": false
+  "description": "Test module.",
+  "incompatibleModules": [
+    "ldap"
+  ],
+  "dependentModules": [
+    "file-access-control"
+  ],
+  "versions": [],
+  "enterprise": false
 }
diff --git a/src/lib/modules/catalog/test/test.yml b/src/lib/modules/catalog/test/test.yaml
similarity index 85%
rename from src/lib/modules/catalog/test/test.yml
rename to src/lib/modules/catalog/test/test.yaml
index e1ffe71e..8d062bac 100644
--- a/src/lib/modules/catalog/test/test.yml
+++ b/src/lib/modules/catalog/test/test.yaml
@@ -1,9 +1,10 @@
+---
 services:
 
   trino:
     environment:
       MINITRINO_BOOTSTRAP: bootstrap-trino.sh
-      CONFIG_PROPERTIES: |- 
+      CONFIG_PROPERTIES: |-
         query.max-stage-count=85
         query.max-execution-time=1h
       JVM_CONFIG: |-
@@ -11,19 +12,19 @@ services:
         -Xms1G
 
   test:
-    build: 
+    build:
       context: ./modules/catalog/test/
-      labels: 
+      labels:
         - com.starburst.tests=minitrino
         - com.starburst.tests.module.test=catalog-test
     environment:
       MINITRINO_BOOTSTRAP: bootstrap-test.sh
-    command: [ tail, -f, /dev/null ] # Keep alive 
+    command: [tail, -f, /dev/null]  # Keep alive
     image: minitrino/test:latest
     container_name: test
     volumes:
       - test-data:/etc/test/
-    labels: 
+    labels:
       - com.starburst.tests=minitrino
       - com.starburst.tests.module.test=catalog-test
 
diff --git a/src/test/src/cli/runner.py b/src/test/src/cli/runner.py
index a433a438..169f3584 100644
--- a/src/test/src/cli/runner.py
+++ b/src/test/src/cli/runner.py
@@ -2,7 +2,6 @@
 # -*- coding: utf-8 -*-
 
 import src.cli.test_misc
-
 import src.cli.test_cmd_config
 import src.cli.test_cmd_down
 import src.cli.test_cmd_provision
diff --git a/src/test/src/cli/test_cmd_config.py b/src/test/src/cli/test_cmd_config.py
index 348cf198..23bab153 100644
--- a/src/test/src/cli/test_cmd_config.py
+++ b/src/test/src/cli/test_cmd_config.py
@@ -2,10 +2,9 @@
 # -*- coding: utf-8 -*-
 
 import os
-import subprocess
 
 import src.common as common
-import src.cli.helpers as helpers
+import src.cli.utils as utils
 
 from inspect import currentframe
 from types import FrameType
@@ -29,8 +28,8 @@ def test_no_directory():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    subprocess.call(f"rm -rf {common.MINITRINO_USER_DIR}", shell=True)
-    result = helpers.execute_command(CMD)
+    common.execute_command(f"rm -rf {common.MINITRINO_USER_DIR}")
+    result = utils.execute_cli_cmd(CMD)
 
     assert result.exit_code == 0
     assert os.path.isdir(common.MINITRINO_USER_DIR)
@@ -45,8 +44,8 @@ def test_no_config_file():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    subprocess.call(f"rm {common.CONFIG_FILE}", shell=True)
-    result = helpers.execute_command(CMD)
+    common.execute_command(f"rm {common.CONFIG_FILE}")
+    result = utils.execute_cli_cmd(CMD)
 
     assert result.exit_code == 0
     assert os.path.isfile(common.CONFIG_FILE)
@@ -60,23 +59,16 @@ def test_reset():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    subprocess.call(f"echo 'hello world' > {common.CONFIG_FILE}", shell=True)
+    common.execute_command(f"echo 'hello world' > {common.CONFIG_FILE}")
 
     # Run the command and feed 'y' as input
-    result = helpers.execute_command(CMD + ["--reset"], command_input="y\n")
+    result = utils.execute_cli_cmd(CMD + ["--reset"], command_input="y\n")
 
     assert result.exit_code == 0
     assert os.path.isfile(common.CONFIG_FILE)
 
-    config_contents = subprocess.Popen(
-        f"cat {common.CONFIG_FILE}",
-        shell=True,
-        stdout=subprocess.PIPE,
-        universal_newlines=True,
-    )
-
-    config_contents, _ = config_contents.communicate()
-    assert "hello world" not in config_contents
+    config_contents = common.execute_command(f"cat {common.CONFIG_FILE}")
+    assert "hello world" not in config_contents.get("output", "")
 
     common.log_success(cast(FrameType, currentframe()).f_code.co_name)
 
@@ -88,8 +80,8 @@ def test_edit_invalid_config():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    subprocess.call(f"echo 'hello world' > {common.CONFIG_FILE}", shell=True)
-    result = helpers.execute_command(CMD)
+    common.execute_command(f"echo 'hello world' > {common.CONFIG_FILE}")
+    result = utils.execute_cli_cmd(CMD)
 
     assert result.exit_code == 0
     assert "Failed to parse config file" in result.output
@@ -102,8 +94,8 @@ def test_edit_valid_config():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    helpers.make_sample_config()
-    result = helpers.execute_command(CMD)
+    utils.make_sample_config()
+    result = utils.execute_cli_cmd(CMD)
     assert result.exit_code == 0
 
     common.log_success(cast(FrameType, currentframe()).f_code.co_name)
diff --git a/src/test/src/cli/test_cmd_down.py b/src/test/src/cli/test_cmd_down.py
index 1405db77..5bcc5a1e 100644
--- a/src/test/src/cli/test_cmd_down.py
+++ b/src/test/src/cli/test_cmd_down.py
@@ -4,7 +4,7 @@
 import docker
 
 import src.common as common
-import src.cli.helpers as helpers
+import src.cli.utils as utils
 from minitrino.settings import RESOURCE_LABEL
 
 from inspect import currentframe
@@ -27,7 +27,7 @@ def test_no_containers():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(["-v", "down"])
+    result = utils.execute_cli_cmd(["-v", "down"])
 
     assert result.exit_code == 0
     assert "No containers to bring down" in result.output
@@ -46,8 +46,8 @@ def test_running_containers():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    helpers.execute_command(["-v", "provision", "--module", "test"])
-    result = helpers.execute_command(["-v", "down", "--sig-kill"])
+    utils.execute_cli_cmd(["-v", "provision", "--module", "test"])
+    result = utils.execute_cli_cmd(["-v", "down", "--sig-kill"])
 
     assert result.exit_code == 0
     assert all(
@@ -72,8 +72,8 @@ def test_keep():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    helpers.execute_command(["-v", "provision", "--module", "test"])
-    result = helpers.execute_command(["-v", "down", "--keep"])
+    utils.execute_cli_cmd(["-v", "provision", "--module", "test"])
+    result = utils.execute_cli_cmd(["-v", "down", "--keep"])
 
     assert "Stopped container" in result.output
     assert "Removed container" not in result.output
@@ -93,7 +93,7 @@ def test_keep():
 def cleanup():
     """Stops/removes containers."""
 
-    helpers.execute_command(["-v", "down", "--sig-kill"])
+    utils.execute_cli_cmd(["-v", "down", "--sig-kill"])
 
 
 if __name__ == "__main__":
diff --git a/src/test/src/cli/test_cmd_lib_install.py b/src/test/src/cli/test_cmd_lib_install.py
index e03fbf6f..aa574a67 100644
--- a/src/test/src/cli/test_cmd_lib_install.py
+++ b/src/test/src/cli/test_cmd_lib_install.py
@@ -2,10 +2,9 @@
 # -*- coding: utf-8 -*-
 
 import os
-import subprocess
 
 import src.common as common
-import src.cli.helpers as helpers
+import src.cli.utils as utils
 
 from inspect import currentframe
 from types import FrameType
@@ -26,7 +25,7 @@ def test_install():
     cleanup()
 
     # Install 0.0.0 since it's always around as a test release
-    result = helpers.execute_command(["-v", "lib-install", "--version", "0.0.0"])
+    result = utils.execute_cli_cmd(["-v", "lib-install", "--version", "0.0.0"])
 
     assert result.exit_code == 0
     assert os.path.isdir(os.path.join(common.MINITRINO_USER_DIR, "lib"))
@@ -40,7 +39,7 @@ def test_install_overwrite():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         ["-v", "lib-install", "--version", "0.0.0"], command_input="y\n"
     )
 
@@ -59,7 +58,7 @@ def test_invalid_ver():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(["-v", "lib-install", "--version", "YEE-TRINO"])
+    result = utils.execute_cli_cmd(["-v", "lib-install", "--version", "YEE-TRINO"])
 
     assert result.exit_code == 1
     assert not os.path.isdir(os.path.join(common.MINITRINO_USER_DIR, "lib"))
@@ -70,7 +69,7 @@ def test_invalid_ver():
 
 
 def cleanup():
-    subprocess.call("rm -rf ~/.minitrino/", shell=True)
+    common.execute_command("rm -rf ~/.minitrino/")
 
 
 if __name__ == "__main__":
diff --git a/src/test/src/cli/test_cmd_modules.py b/src/test/src/cli/test_cmd_modules.py
index 84ce375c..c0fd0f9a 100644
--- a/src/test/src/cli/test_cmd_modules.py
+++ b/src/test/src/cli/test_cmd_modules.py
@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 
 import src.common as common
-import src.cli.helpers as helpers
+import src.cli.utils as utils
 
 from inspect import currentframe
 from types import FrameType
@@ -24,7 +24,7 @@ def test_invalid_module():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(["-v", "modules", "--module", "not-a-real-module"])
+    result = utils.execute_cli_cmd(["-v", "modules", "--module", "not-a-real-module"])
 
     assert result.exit_code == 2
     assert "Invalid module" in result.output
@@ -37,7 +37,7 @@ def test_valid_module():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(["-v", "modules", "--module", "test"])
+    result = utils.execute_cli_cmd(["-v", "modules", "--module", "test"])
 
     assert result.exit_code == 0
     assert all(("Module: test" in result.output, "Test module" in result.output))
@@ -51,7 +51,7 @@ def test_all_modules():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(["-v", "modules"])
+    result = utils.execute_cli_cmd(["-v", "modules"])
 
     assert result.exit_code == 0
     assert all(
@@ -60,6 +60,8 @@ def test_all_modules():
             "Description:" in result.output,
             "IncompatibleModules:" in result.output,
             "DependentModules:" in result.output,
+            "Versions:" in result.output,
+            "Enterprise:" in result.output,
         )
     )
 
@@ -72,7 +74,7 @@ def test_json():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(["-v", "modules", "--module", "test", "--json"])
+    result = utils.execute_cli_cmd(["-v", "modules", "--module", "test", "--json"])
 
     assert result.exit_code == 0
     assert all(('"type": "catalog"' in result.output, '"test":' in result.output))
@@ -85,8 +87,8 @@ def test_running():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    helpers.execute_command(["-v", "provision", "--module", "test"])
-    result = helpers.execute_command(["-v", "modules", "--json", "--running"])
+    utils.execute_cli_cmd(["-v", "provision", "--module", "test"])
+    result = utils.execute_cli_cmd(["-v", "modules", "--json", "--running"])
 
     assert result.exit_code == 0
     assert all(
@@ -97,7 +99,7 @@ def test_running():
         )
     )
 
-    helpers.execute_command(["-v", "down", "--sig-kill"])
+    utils.execute_cli_cmd(["-v", "down", "--sig-kill"])
     common.log_success(cast(FrameType, currentframe()).f_code.co_name)
 
 
diff --git a/src/test/src/cli/test_cmd_provision.py b/src/test/src/cli/test_cmd_provision.py
index 24a3213e..b0925b9c 100644
--- a/src/test/src/cli/test_cmd_provision.py
+++ b/src/test/src/cli/test_cmd_provision.py
@@ -5,11 +5,11 @@
 # TODO: Test invalid user config (Trino/JVM)
 
 import docker
-import subprocess
 
 import src.common as common
-import src.cli.helpers as helpers
+import src.cli.utils as utils
 from minitrino.settings import RESOURCE_LABEL
+from minitrino.settings import MIN_SEP_VER
 
 from inspect import currentframe
 from types import FrameType
@@ -25,6 +25,7 @@ def main():
     test_invalid_module()
     test_docker_native()
     test_enterprise()
+    test_version_requirements()
     test_valid_user_config()
     test_duplicate_config_props()
     test_incompatible_modules()
@@ -39,7 +40,7 @@ def test_standalone():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(["-v", "provision"])
+    result = utils.execute_cli_cmd(["-v", "provision"])
 
     assert result.exit_code == 0
     assert "Provisioning standalone" in result.output
@@ -60,9 +61,7 @@ def test_bad_sep_version():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(
-        ["-v", "--env", "STARBURST_VER=332-e", "provision"]
-    )
+    result = utils.execute_cli_cmd(["-v", "--env", "STARBURST_VER=332-e", "provision"])
 
     assert result.exit_code == 2
     assert "Provided Starburst version" in result.output
@@ -80,7 +79,7 @@ def test_invalid_module():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         ["-v", "provision", "--module", "hive", "--module", "not-a-real-module"]
     )
 
@@ -102,7 +101,7 @@ def test_docker_native():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         ["-v", "provision", "--module", "test", "--docker-native", "--build"]
     )
 
@@ -121,20 +120,17 @@ def test_enterprise():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(["-v", "provision", "--module", "test-enterprise"])
+    utils.update_metadata_json([{"enterprise": True}])
+    result = utils.execute_cli_cmd(["-v", "provision", "--module", "test"])
 
     assert result.exit_code == 2
     assert "You must provide a path to a Starburst license" in result.output
     cleanup()
 
     # Create dummy license
-    process = subprocess.Popen(
-        "touch /tmp/dummy.license",
-        shell=True,
-    )
-    process.communicate()
+    common.execute_command("touch /tmp/dummy.license")
 
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         [
             "-v",
             "--env",
@@ -142,13 +138,16 @@ def test_enterprise():
             "provision",
             "--module",
             "test",
+            "--no-rollback",
         ]
     )
 
     assert "LIC_PATH" and "/tmp/dummy.license" in result.output
+    utils.reset_metadata_json()
     cleanup()
 
-    result = helpers.execute_command(["-v", "provision", "--module", "test"])
+    # Ensure default dummy license satisfies Compose env vars
+    result = utils.execute_cli_cmd(["-v", "provision", "--module", "test"])
 
     assert result.exit_code == 0
     assert "LIC_PATH" and "./modules/resources/dummy.license" in result.output
@@ -158,6 +157,42 @@ def test_enterprise():
     cleanup()
 
 
+def test_version_requirements():
+    """Ensures that module version requirements are properly enforced."""
+
+    common.log_status(cast(FrameType, currentframe()).f_code.co_name)
+
+    utils.update_metadata_json([{"versions": [MIN_SEP_VER + 1, 998]}])
+
+    # Should fail - lower bound
+    result = utils.execute_cli_cmd(
+        [
+            "-v",
+            "--env",
+            f"STARBURST_VER={MIN_SEP_VER}-e",
+            "provision",
+            "--module",
+            "test",
+        ]
+    )
+
+    assert result.exit_code == 2
+    assert "minimum required" in result.output
+    cleanup()
+
+    # Should fail - upper bound
+    result = utils.execute_cli_cmd(
+        ["-v", "--env", "STARBURST_VER=999-e", "provision", "--module", "test"]
+    )
+
+    assert result.exit_code == 2
+    assert "maximum required" in result.output
+
+    common.log_success(cast(FrameType, currentframe()).f_code.co_name)
+    utils.reset_metadata_json()
+    cleanup()
+
+
 def test_bootstrap_script(result):
     """Ensures that bootstrap scripts properly execute in containers."""
 
@@ -170,24 +205,15 @@ def test_bootstrap_script(result):
         )
     )
 
-    trino_bootstrap_check = subprocess.Popen(
-        f"docker exec -i trino ls /etc/starburst/",
-        shell=True,
-        stdout=subprocess.PIPE,
-        universal_newlines=True,
+    trino_bootstrap_check = common.execute_command(
+        f"docker exec -i trino ls /etc/starburst/"
     )
-    test_bootstrap_check = subprocess.Popen(
-        f"docker exec -i test cat /root/test_bootstrap.txt",
-        shell=True,
-        stdout=subprocess.PIPE,
-        universal_newlines=True,
+    test_bootstrap_check = common.execute_command(
+        f"docker exec -i test cat /root/test_bootstrap.txt"
     )
 
-    trino_bootstrap_check, _ = trino_bootstrap_check.communicate()
-    test_bootstrap_check, _ = test_bootstrap_check.communicate()
-
-    assert "test_bootstrap.txt" in trino_bootstrap_check
-    assert "hello world" in test_bootstrap_check
+    assert "test_bootstrap.txt" in trino_bootstrap_check.get("output", "")
+    assert "hello world" in test_bootstrap_check.get("output", "")
 
     common.log_success(cast(FrameType, currentframe()).f_code.co_name)
 
@@ -198,7 +224,7 @@ def test_valid_user_config():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         [
             "-v",
             "provision",
@@ -212,27 +238,18 @@ def test_valid_user_config():
         "Appending user-defined Trino config to Trino container config" in result.output
     )
 
-    jvm_config = subprocess.Popen(
-        f"docker exec -i trino cat /etc/starburst/jvm.config",
-        shell=True,
-        stdout=subprocess.PIPE,
-        universal_newlines=True,
+    jvm_config = common.execute_command(
+        f"docker exec -i trino cat /etc/starburst/jvm.config"
     )
-    trino_config = subprocess.Popen(
-        f"docker exec -i trino cat /etc/starburst/config.properties",
-        shell=True,
-        stdout=subprocess.PIPE,
-        universal_newlines=True,
+    trino_config = common.execute_command(
+        f"docker exec -i trino cat /etc/starburst/config.properties"
     )
 
-    jvm_config, _ = jvm_config.communicate()
-    trino_config, _ = trino_config.communicate()
-
-    assert all(("-Xmx2G" in jvm_config, "-Xms1G" in jvm_config))
+    assert all(("-Xmx2G" in jvm_config["output"], "-Xms1G" in jvm_config["output"]))
     assert all(
         (
-            "query.max-stage-count=85" in trino_config,
-            "query.max-execution-time=1h" in trino_config,
+            "query.max-stage-count=85" in trino_config["output"],
+            "query.max-execution-time=1h" in trino_config["output"],
         )
     )
 
@@ -246,7 +263,7 @@ def test_duplicate_config_props():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         [
             "-v",
             "--env",
@@ -278,7 +295,7 @@ def test_incompatible_modules():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         ["-v", "provision", "--module", "ldap", "--module", "test"]
     )
 
@@ -300,8 +317,8 @@ def test_provision_append():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    helpers.execute_command(["-v", "provision", "--module", "test"])
-    result = helpers.execute_command(["-v", "provision", "--module", "postgres"])
+    utils.execute_cli_cmd(["-v", "provision", "--module", "test"])
+    result = utils.execute_cli_cmd(["-v", "provision", "--module", "postgres"])
     containers = get_containers()
 
     assert result.exit_code == 0
@@ -310,25 +327,13 @@ def test_provision_append():
 
     # Ensure new volume mount was activated; indicates Trino container was
     # properly recreated
-    etc_ls = subprocess.Popen(
-        f"docker exec -i trino ls /etc/starburst/catalog/",
-        shell=True,
-        stdout=subprocess.PIPE,
-        universal_newlines=True,
-    )
-    etc_ls, _ = etc_ls.communicate()
-    assert "postgres.properties" in etc_ls
+    etc_ls = common.execute_command(f"docker exec -i trino ls /etc/starburst/catalog/")
+    assert "postgres.properties" in etc_ls["output"]
 
     # Add one more module
-    result = helpers.execute_command(["-v", "provision", "--module", "mysql"])
-    etc_ls = subprocess.Popen(
-        f"docker exec -i trino ls /etc/starburst/catalog/",
-        shell=True,
-        stdout=subprocess.PIPE,
-        universal_newlines=True,
-    )
-    etc_ls, _ = etc_ls.communicate()
-    assert "mysql.properties" and "postgres.properties" in etc_ls
+    result = utils.execute_cli_cmd(["-v", "provision", "--module", "mysql"])
+    etc_ls = common.execute_command(f"docker exec -i trino ls /etc/starburst/catalog/")
+    assert "mysql.properties" and "postgres.properties" in etc_ls["output"]
 
     containers = get_containers()
 
@@ -345,7 +350,7 @@ def test_workers():
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
     # Provision 1 worker
-    result = helpers.execute_command(["-v", "provision", "--workers", "1"])
+    result = utils.execute_cli_cmd(["-v", "provision", "--workers", "1"])
     containers = get_containers()
 
     assert result.exit_code == 0
@@ -353,7 +358,7 @@ def test_workers():
     assert len(containers) == 2  # trino, trino worker
 
     # Provision a second worker
-    result = helpers.execute_command(["-v", "provision", "--workers", "2"])
+    result = utils.execute_cli_cmd(["-v", "provision", "--workers", "2"])
     containers = get_containers()
 
     assert result.exit_code == 0
@@ -361,7 +366,7 @@ def test_workers():
     assert len(containers) == 3  # trino, (2) trino worker
 
     # Downsize workers (remove worker 2)
-    result = helpers.execute_command(["-v", "provision", "--workers", "1"])
+    result = utils.execute_cli_cmd(["-v", "provision", "--workers", "1"])
     containers = get_containers()
 
     assert result.exit_code == 0
@@ -370,7 +375,7 @@ def test_workers():
 
     # Provision a module in a running environment with a worker already present,
     # but don't specify any workers
-    result = helpers.execute_command(["-v", "provision", "--module", "test"])
+    result = utils.execute_cli_cmd(["-v", "provision", "--module", "test"])
     containers = get_containers()
 
     assert result.exit_code == 0
@@ -386,44 +391,34 @@ def test_catalogs_volume():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(["-v", "provision", "--module", "hive"])
+    result = utils.execute_cli_cmd(["-v", "provision", "--module", "hive"])
     assert result.exit_code == 0
 
-    hive = subprocess.Popen(
-        f"docker exec -i trino cat /etc/starburst/catalog/hive.properties",
-        shell=True,
-        stdout=subprocess.PIPE,
-        universal_newlines=True,
+    hive = common.execute_command(
+        "docker exec -i trino cat /etc/starburst/catalog/hive.properties"
     )
 
-    hive, _ = hive.communicate()
-    assert "connector.name=hive" in hive
+    assert "connector.name=hive" in hive["output"]
 
     cleanup()
 
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         ["-v", "provision", "--module", "hive", "--module", "delta-lake"]
     )
     assert result.exit_code == 0
     assert "Removed 'minitrino_catalogs' volume" in result.output
 
-    hive = subprocess.Popen(
-        f"docker exec -i trino cat /etc/starburst/catalog/hive.properties",
-        shell=True,
-        stdout=subprocess.PIPE,
-        universal_newlines=True,
+    hive = common.execute_command(
+        "docker exec -i trino cat /etc/starburst/catalog/hive.properties"
     )
-
-    delta = subprocess.Popen(
-        f"docker exec -i trino cat /etc/starburst/catalog/delta.properties",
-        shell=True,
-        stdout=subprocess.PIPE,
-        universal_newlines=True,
+    delta = common.execute_command(
+        "docker exec -i trino cat /etc/starburst/catalog/delta.properties"
     )
 
-    hive, _ = hive.communicate()
-    delta, _ = delta.communicate()
-    assert "connector.name=hive" in hive and "connector.name=delta-lake" in delta
+    assert (
+        "connector.name=hive" in hive["output"]
+        and "connector.name=delta-lake" in delta["output"]
+    )
 
     common.log_success(cast(FrameType, currentframe()).f_code.co_name)
     cleanup()
@@ -439,7 +434,7 @@ def get_containers():
 def cleanup():
     """Brings down containers and removes resources."""
 
-    helpers.execute_command(["down", "--sig-kill"])
+    utils.execute_cli_cmd(["down", "--sig-kill"])
 
 
 if __name__ == "__main__":
diff --git a/src/test/src/cli/test_cmd_remove.py b/src/test/src/cli/test_cmd_remove.py
index 88e4548c..f29b1ab1 100644
--- a/src/test/src/cli/test_cmd_remove.py
+++ b/src/test/src/cli/test_cmd_remove.py
@@ -2,10 +2,9 @@
 # -*- coding: utf-8 -*-
 
 import docker
-import subprocess
 
 import src.common as common
-import src.cli.helpers as helpers
+import src.cli.utils as utils
 
 from inspect import currentframe
 from types import FrameType
@@ -36,9 +35,9 @@ def test_images(docker_client):
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    helpers.execute_command(["provision", "--module", "test"])
-    helpers.execute_command(["down", "--sig-kill"])
-    result = helpers.execute_command(["-v", "remove", "--images"])
+    utils.execute_cli_cmd(["provision", "--module", "test"])
+    utils.execute_cli_cmd(["down", "--sig-kill"])
+    result = utils.execute_cli_cmd(["-v", "remove", "--images"])
 
     assert result.exit_code == 0
     assert "Image removed:" in result.output
@@ -61,9 +60,9 @@ def test_volumes(docker_client):
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    helpers.execute_command(["provision", "--module", "test"])
-    helpers.execute_command(["down", "--sig-kill"])
-    result = helpers.execute_command(["-v", "remove", "--volumes"])
+    utils.execute_cli_cmd(["provision", "--module", "test"])
+    utils.execute_cli_cmd(["down", "--sig-kill"])
+    result = utils.execute_cli_cmd(["-v", "remove", "--volumes"])
 
     assert result.exit_code == 0
     assert "Volume removed:" in result.output
@@ -85,9 +84,9 @@ def test_label(docker_client):
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    helpers.execute_command(["provision", "--module", "test"])
-    helpers.execute_command(["down", "--sig-kill"])
-    result = helpers.execute_command(
+    utils.execute_cli_cmd(["provision", "--module", "test"])
+    utils.execute_cli_cmd(["down", "--sig-kill"])
+    result = utils.execute_cli_cmd(
         [
             "-v",
             "remove",
@@ -122,9 +121,9 @@ def test_multiple_labels(docker_client):
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    helpers.execute_command(["provision", "--module", "test"])
-    helpers.execute_command(["down", "--sig-kill"])
-    result = helpers.execute_command(
+    utils.execute_cli_cmd(["provision", "--module", "test"])
+    utils.execute_cli_cmd(["down", "--sig-kill"])
+    result = utils.execute_cli_cmd(
         [
             "-v",
             "remove",
@@ -162,9 +161,9 @@ def test_invalid_label(docker_client):
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    helpers.execute_command(["provision", "--module", "test"])
-    helpers.execute_command(["down", "--sig-kill"])
-    result = helpers.execute_command(
+    utils.execute_cli_cmd(["provision", "--module", "test"])
+    utils.execute_cli_cmd(["down", "--sig-kill"])
+    result = utils.execute_cli_cmd(
         ["-v", "remove", "--images", "--label", "not-real-label=not-real"]
     )
 
@@ -188,9 +187,9 @@ def test_all(docker_client):
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    helpers.execute_command(["provision", "--module", "test"])
-    helpers.execute_command(["down", "--sig-kill"])
-    result = helpers.execute_command(
+    utils.execute_cli_cmd(["provision", "--module", "test"])
+    utils.execute_cli_cmd(["down", "--sig-kill"])
+    result = utils.execute_cli_cmd(
         ["-v", "remove", "--images", "--volumes"], command_input="y\n"
     )
 
@@ -220,8 +219,8 @@ def test_remove_dependent_resources_running(docker_client):
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    helpers.execute_command(["provision", "--module", "test"])
-    result = helpers.execute_command(
+    utils.execute_cli_cmd(["provision", "--module", "test"])
+    result = utils.execute_cli_cmd(
         ["-v", "remove", "--images", "--volumes"], command_input="y\n"
     )
 
@@ -256,9 +255,9 @@ def test_remove_dependent_resources_stopped(docker_client):
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    helpers.execute_command(["provision", "--module", "test"])
-    subprocess.call("docker stop test", shell=True)
-    result = helpers.execute_command(
+    utils.execute_cli_cmd(["provision", "--module", "test"])
+    common.execute_command("docker stop test")
+    result = utils.execute_cli_cmd(
         [
             "-v",
             "remove",
@@ -305,9 +304,9 @@ def test_remove_dependent_resources_force(docker_client):
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    helpers.execute_command(["provision", "--module", "test"])
-    subprocess.call("docker stop test", shell=True)
-    result = helpers.execute_command(
+    utils.execute_cli_cmd(["provision", "--module", "test"])
+    common.execute_command("docker stop test")
+    result = utils.execute_cli_cmd(
         [
             "-v",
             "remove",
@@ -358,7 +357,7 @@ def assert_docker_resource_count(*args):
 def cleanup():
     """Brings down containers and removes resources."""
 
-    helpers.execute_command(
+    utils.execute_cli_cmd(
         [
             "remove",
             "--images",
diff --git a/src/test/src/cli/test_cmd_snapshot.py b/src/test/src/cli/test_cmd_snapshot.py
index 714944f2..e353d692 100644
--- a/src/test/src/cli/test_cmd_snapshot.py
+++ b/src/test/src/cli/test_cmd_snapshot.py
@@ -2,10 +2,9 @@
 # -*- coding: utf-8 -*-
 
 import os
-import subprocess
 
 import src.common as common
-import src.cli.helpers as helpers
+import src.cli.utils as utils
 
 from inspect import currentframe
 from types import FrameType
@@ -20,7 +19,7 @@ def snapshot_test_yaml_file(snapshot_name="test"):
         "modules",
         "catalog",
         "test",
-        "test.yml",
+        "test.yaml",
     )
 
 
@@ -54,8 +53,8 @@ def test_snapshot_no_directory():
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
     cleanup()
-    subprocess.call(f"rm -rf {common.MINITRINO_USER_SNAPSHOTS_DIR}", shell=True)
-    result = helpers.execute_command(
+    common.execute_command(f"rm -rf {common.MINITRINO_USER_SNAPSHOTS_DIR}")
+    result = utils.execute_cli_cmd(
         ["-v", "snapshot", "--name", "test", "--module", "test"],
         command_input="y\n",
     )
@@ -72,8 +71,8 @@ def test_snapshot_active_env():
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
     cleanup()
-    helpers.execute_command(["-v", "provision", "--module", "test"])
-    result = helpers.execute_command(
+    utils.execute_cli_cmd(["-v", "provision", "--module", "test"])
+    result = utils.execute_cli_cmd(
         ["-v", "snapshot", "--name", "test"],
         command_input="y\n",
     )
@@ -95,12 +94,12 @@ def test_snapshot_active_env():
 
 
 def test_snapshot_standalone():
-    """Verifies that a the standlone Trino module can be snapshotted."""
+    """Verifies that a the standalone Trino module can be snapshotted."""
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
     cleanup()
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         ["-v", "snapshot", "--name", "test"],
         command_input="y\n",
     )
@@ -118,7 +117,7 @@ def test_snapshot_inactive_env():
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
     cleanup()
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         ["-v", "snapshot", "--name", "test", "--module", "test"],
         command_input="y\n",
     )
@@ -136,7 +135,7 @@ def test_valid_name():
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
     cleanup()
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         ["-v", "snapshot", "--name", "my-test_123", "--module", "test"],
         command_input="y\n",
     )
@@ -156,7 +155,7 @@ def test_invalid_name():
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
     cleanup()
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         ["-v", "snapshot", "--name", "##.my-test?", "--module", "test"],
         command_input="y\n",
     )
@@ -174,7 +173,7 @@ def test_specific_directory():
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
     cleanup()
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         [
             "-v",
             "snapshot",
@@ -191,7 +190,7 @@ def test_specific_directory():
     run_assertions(result, True, check_path=os.path.join(os.sep, "tmp"))
     assert "Creating snapshot of specified modules" in result.output
 
-    subprocess.call("rm -rf /tmp/test.tar.gz", shell=True)
+    common.execute_command("rm -rf /tmp/test.tar.gz")
 
     common.log_success(cast(FrameType, currentframe()).f_code.co_name)
 
@@ -202,7 +201,7 @@ def test_specific_directory_invalid():
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
     cleanup()
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         [
             "-v",
             "snapshot",
@@ -228,27 +227,20 @@ def test_command_snapshot_file():
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
     cleanup()
-    helpers.execute_command(["-v", "provision", "--module", "test"])
-    helpers.execute_command(
+    utils.execute_cli_cmd(["-v", "provision", "--module", "test"])
+    utils.execute_cli_cmd(
         ["-v", "snapshot", "--name", "test"],
         command_input="y\n",
     )
-    helpers.execute_command(["down", "--sig-kill"])
+    utils.execute_cli_cmd(["down", "--sig-kill"])
 
     command_snapshot_file = os.path.join(
         common.MINITRINO_USER_SNAPSHOTS_DIR, "test", "provision-snapshot.sh"
     )
-    process = subprocess.Popen(
-        command_snapshot_file,
-        shell=True,
-        stdout=subprocess.PIPE,
-        stderr=subprocess.STDOUT,
-        universal_newlines=True,
-    )
-    stdout, _ = process.communicate()
+    output = common.execute_command(command_snapshot_file)
 
-    assert process.returncode == 0
-    assert "Environment provisioning complete" in stdout
+    assert output.get("exit_code", None) == 0
+    assert "Environment provisioning complete" in output.get("output", "")
 
     cleanup()
     common.log_success(cast(FrameType, currentframe()).f_code.co_name)
@@ -260,7 +252,7 @@ def test_force():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         ["-v", "snapshot", "--name", "test", "--module", "test", "--force"],
         command_input="y\n",
     )
@@ -278,8 +270,8 @@ def test_no_scrub():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    helpers.make_sample_config()
-    result = helpers.execute_command(
+    utils.make_sample_config()
+    result = utils.execute_cli_cmd(
         ["-v", "snapshot", "--name", "test", "--module", "test", "--no-scrub"],
         True,
         command_input="y\n",
@@ -299,8 +291,8 @@ def test_scrub():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    helpers.make_sample_config()
-    result = helpers.execute_command(
+    utils.make_sample_config()
+    result = utils.execute_cli_cmd(
         ["-v", "snapshot", "--name", "test", "--module", "test"], command_input="y\n"
     )
 
@@ -343,14 +335,13 @@ def cleanup(snapshot_name="test"):
     """Removes test snapshot tarball and turns off running resources."""
 
     if not snapshot_name == "test":
-        subprocess.call(
-            f"rm -rf {os.path.join(common.MINITRINO_USER_SNAPSHOTS_DIR, snapshot_name)}.tar.gz",
-            shell=True,
+        common.execute_command(
+            f"rm -rf {os.path.join(common.MINITRINO_USER_SNAPSHOTS_DIR, snapshot_name)}.tar.gz"
         )
     else:
-        subprocess.call(f"rm -rf {common.SNAPSHOT_FILE}", shell=True)
+        common.execute_command(f"rm -rf {common.SNAPSHOT_FILE}")
 
-    helpers.execute_command(["down", "--sig-kill"])
+    utils.execute_cli_cmd(["down", "--sig-kill"])
 
 
 if __name__ == "__main__":
diff --git a/src/test/src/cli/test_cmd_version.py b/src/test/src/cli/test_cmd_version.py
index add3368f..a669d249 100644
--- a/src/test/src/cli/test_cmd_version.py
+++ b/src/test/src/cli/test_cmd_version.py
@@ -5,7 +5,7 @@
 import pkg_resources
 
 import src.common as common
-import src.cli.helpers as helpers
+import src.cli.utils as utils
 
 from inspect import currentframe
 from types import FrameType
@@ -22,7 +22,7 @@ def test_version():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(["version"])
+    result = utils.execute_cli_cmd(["version"])
     assert pkg_resources.require("Minitrino")[0].version in result.output
 
     common.log_success(cast(FrameType, currentframe()).f_code.co_name)
diff --git a/src/test/src/cli/test_misc.py b/src/test/src/cli/test_misc.py
index b14c092d..73fd4981 100644
--- a/src/test/src/cli/test_misc.py
+++ b/src/test/src/cli/test_misc.py
@@ -1,13 +1,8 @@
 #!usr/bin/env/python3
 # -*- coding: utf-8 -*-
 
-# TODO: Test docker host
-
-import os
-import subprocess
-
 import src.common as common
-import src.cli.helpers as helpers
+import src.cli.utils as utils
 
 from inspect import currentframe
 from types import FrameType
@@ -53,9 +48,9 @@ def run_daemon_assertions(result):
 
     for arg in args:
         if "snapshot" in arg:
-            result = helpers.execute_command(arg, command_input="y\n")
+            result = utils.execute_cli_cmd(arg, command_input="y\n")
         else:
-            result = helpers.execute_command(arg)
+            result = utils.execute_cli_cmd(arg)
         run_daemon_assertions(result)
 
     common.log_success(cast(FrameType, currentframe()).f_code.co_name)
@@ -67,7 +62,7 @@ def test_env():
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
     # User environment variable
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         ["-v", "--env", "COMPOSE_PROJECT_NAME=test", "version"]
     )
 
@@ -75,7 +70,7 @@ def test_env():
     assert "COMPOSE_PROJECT_NAME" and "test" in result.output
 
     # Shell environment variable
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         ["-v", "version"],
         env={"COMPOSE_PROJECT_NAME": "test"},
     )
@@ -84,32 +79,24 @@ def test_env():
     assert "COMPOSE_PROJECT_NAME" and "test" in result.output
 
     # Config environment variable
-    helpers.make_sample_config()
-    subprocess.call(
+    utils.make_sample_config()
+    cmd = (
         f'bash -c "cat << EOF >> {common.CONFIG_FILE}\n'
         f"COMPOSE_PROJECT_NAME=test\n"
-        f'EOF"',
-        shell=True,
-        stdout=subprocess.DEVNULL,
-        stderr=subprocess.DEVNULL,
+        f'EOF"'
     )
+    common.execute_command(cmd)
 
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         ["-v", "version"],
     )
 
     assert result.exit_code == 0
     assert "COMPOSE_PROJECT_NAME" and "test" in result.output
 
-    subprocess.call(
-        f'bash -c "rm {common.CONFIG_FILE}\n"',
-        shell=True,
-        stdout=subprocess.DEVNULL,
-        stderr=subprocess.DEVNULL,
-    )
+    common.execute_command(f'bash -c "rm {common.CONFIG_FILE}\n"')
 
-    # mintirino.env environment variable
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         ["-v", "version"],
     )
 
@@ -125,7 +112,7 @@ def test_multiple_env():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         [
             "-v",
             "--env",
@@ -156,14 +143,14 @@ def test_invalid_env():
 
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         ["-v", "--env", "COMPOSE_PROJECT_NAMEtest", "version"]
     )
 
     assert result.exit_code == 2
     assert "Invalid key-value pair" in result.output
 
-    result = helpers.execute_command(["-v", "--env", "=", "version"])
+    result = utils.execute_cli_cmd(["-v", "--env", "=", "version"])
 
     assert result.exit_code == 2
     assert "Invalid key-value pair" in result.output
@@ -178,13 +165,13 @@ def test_invalid_lib():
     common.log_status(cast(FrameType, currentframe()).f_code.co_name)
 
     # Real directory, but ain't a real library
-    result = helpers.execute_command(["-v", "--env", "LIB_PATH=/tmp/", "modules"])
+    result = utils.execute_cli_cmd(["-v", "--env", "LIB_PATH=/tmp/", "modules"])
 
     assert result.exit_code == 2
     assert "This operation requires a library to be installed" in result.output
 
     # Fake directory
-    result = helpers.execute_command(
+    result = utils.execute_cli_cmd(
         ["-v", "--env", "LIB_PATH=/gucci-is-overrated/", "modules"]
     )
 
diff --git a/src/test/src/cli/helpers.py b/src/test/src/cli/utils.py
similarity index 52%
rename from src/test/src/cli/helpers.py
rename to src/test/src/cli/utils.py
index 198e4672..9ea2ac76 100644
--- a/src/test/src/cli/helpers.py
+++ b/src/test/src/cli/utils.py
@@ -1,9 +1,11 @@
 #!usr/bin/env/python3
 # -*- coding: utf-8 -*-
 
+import os
 import re
-import subprocess
+import json
 
+import src.common as common
 from minitrino.cli import cli
 from src.common import CONFIG_FILE
 
@@ -26,7 +28,7 @@ def __init__(self, click_result, output, exit_code):
         self.exit_code = exit_code
 
 
-def execute_command(command=[], print_output=True, command_input="", env={}):
+def execute_cli_cmd(command=[], print_output=True, command_input="", env={}):
     """Executes a command through the Click CliRunner."""
 
     runner = CliRunner()
@@ -46,15 +48,59 @@ def execute_command(command=[], print_output=True, command_input="", env={}):
 def make_sample_config():
     """Creates a sample config file."""
 
-    subprocess.call(
+    cmd = (
         f'bash -c "cat << EOF > {CONFIG_FILE}\n'
         f"[config]\n"
         f"LIB_PATH=\n"
         f"STARBURST_VER=\n"
         f"TEXT_EDITOR=\n"
         f"LIC_PATH=\n"
-        f'SECRET_KEY=abc123\n"',
-        shell=True,
-        stdout=subprocess.DEVNULL,
-        stderr=subprocess.DEVNULL,
+        f'SECRET_KEY=abc123\n"'
     )
+
+    common.execute_command(cmd)
+
+
+def update_metadata_json(updates=[]):
+    """Updates the test module's metadata.json file with the provided
+    list of dicts."""
+
+    path = get_metadata_json("test")
+
+    with open(path, "r") as f:
+        data = json.load(f)
+
+    for update in updates:
+        for k, v in update.items():
+            data[k] = v
+
+    with open(path, "w") as f:
+        json.dump(data, f, indent=4)
+
+
+def reset_metadata_json():
+    """Resets the test module's metadata.json file to default values."""
+
+    default = {
+        "description": "Test module.",
+        "incompatibleModules": ["ldap"],
+        "dependentModules": ["file-access-control"],
+        "versions": [],
+        "enterprise": False,
+    }
+    path = get_metadata_json("test")
+
+    with open(path, "w") as f:
+        json.dump(default, f, indent=4)
+
+
+def get_metadata_json(module=""):
+    """Fetches the metadata.json file path for a given module."""
+
+    result = execute_cli_cmd(["modules", "-m", module, "--json"])
+
+    # Remove everything up to the first `{`
+    output = "{" + result.output.split("{", 1)[1]
+    output = json.loads(output)
+
+    return os.path.join(output[module]["module_dir"], "metadata.json")
diff --git a/src/test/src/common.py b/src/test/src/common.py
index e89a6129..639ec89d 100644
--- a/src/test/src/common.py
+++ b/src/test/src/common.py
@@ -10,6 +10,8 @@
 from time import sleep, gmtime, strftime
 from pathlib import Path
 
+from minitrino.settings import RESOURCE_LABEL
+
 # Path references
 # -----------------------------------------------------------------------------------
 USER_HOME_DIR = os.path.expanduser("~")
@@ -56,12 +58,12 @@ def start_docker_daemon():
     """Starts the Docker daemon (works on MacOS/Ubuntu)."""
 
     if sys.platform.lower() == "darwin":
-        return_code = subprocess.call("open --background -a Docker", shell=True)
+        exit_code = subprocess.call("open --background -a Docker", shell=True)
     elif "linux" in sys.platform.lower():
-        return_code = subprocess.call("sudo service docker start", shell=True)
+        exit_code = subprocess.call("sudo service docker start", shell=True)
     else:
         raise RuntimeError(f"Incompatible testing platform: {sys.platform}")
-    if return_code != 0:
+    if exit_code != 0:
         raise RuntimeError("Failed to start Docker daemon.")
 
     counter = 0
@@ -81,15 +83,119 @@ def stop_docker_daemon():
     """Stops the Docker daemon (works on MacOS/Ubuntu)."""
 
     if sys.platform.lower() == "darwin":
-        return_code = subprocess.call("osascript -e 'quit app \"Docker\"'", shell=True)
+        exit_code = subprocess.call("osascript -e 'quit app \"Docker\"'", shell=True)
     elif "linux" in sys.platform.lower():
-        return_code = subprocess.call(
+        exit_code = subprocess.call(
             "sudo service docker stop; sudo systemctl stop docker.socket", shell=True
         )
     else:
         raise RuntimeError(f"Incompatible testing platform: {sys.platform}")
-    if return_code != 0:
+    if exit_code != 0:
         raise RuntimeError("Failed to stop Docker daemon.")
 
     # Hard wait for daemon to stop
     sleep(3)
+
+
+def execute_command(cmd="", container=None, env={}):
+    """Executes a command in the user's shell or inside of a container.
+
+    - `cmd`: The command to execute.
+    - `container`: Container name to execute command inside of.
+    - `env`: Environment variables to pass to the container.
+
+    Returns:
+
+    ```python
+    {
+        "command": "str",
+        "output": "str",
+        "exit_code": int
+    }
+    ```"""
+
+    if container:
+        return _execute_in_container(cmd, container, env)
+    else:
+        return _execute_in_shell(cmd, env)
+
+
+def _execute_in_shell(cmd="", env={}):
+    """Executes a command in the host shell."""
+
+    print(f"Executing command on host shell: {cmd}")
+
+    process = subprocess.Popen(
+        cmd,
+        shell=True,
+        bufsize=1,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+        universal_newlines=True,
+        env={**os.environ, **env},
+    )
+
+    output = ""
+    print("Command output:")
+    with process as p:
+        for line in p.stdout:
+            output += line
+            print(line, end="")  # process line here
+
+    return {
+        "command": cmd,
+        "output": output,
+        "exit_code": process.returncode,
+    }
+
+
+def _execute_in_container(cmd="", container_name="", env={}):
+    """Executes a command inside of a container through the Docker SDK
+    (similar to `docker exec`)."""
+
+    docker_url = os.environ.get("DOCKER_HOST", "")
+    api_client = docker.APIClient(base_url=docker_url)
+
+    container = get_container(container_name)
+
+    print(f"Executing command in container '{container.name}': {cmd}")
+
+    exec_handler = api_client.exec_create(
+        container.name, cmd=cmd, privileged=True, tty=True, environment=env
+    )
+
+    output_generator = api_client.exec_start(exec_handler, stream=True)
+
+    output = ""
+    full_line = ""
+    print("Command output:")
+    for chunk in output_generator:
+        chunk = chunk.decode()
+        output += chunk
+        chunk = chunk.split("\n", 1)
+        if len(chunk) > 1:  # Indicates newline present
+            full_line += chunk[0]
+            print(full_line, end="")
+            full_line = ""
+            if chunk[1]:
+                full_line = chunk[1]
+        else:
+            full_line += chunk[0]
+
+    if full_line:
+        print(full_line, end="")
+
+    exit_code = api_client.exec_inspect(exec_handler["Id"]).get("ExitCode")
+
+    return {"command": cmd, "output": output, "exit_code": exit_code}
+
+
+def get_container(container_name=""):
+    """Fetches running container by container name."""
+
+    docker_url = os.environ.get("DOCKER_HOST", "")
+    docker_client = docker.DockerClient(base_url=docker_url)
+    containers = docker_client.containers.list(filters={"label": RESOURCE_LABEL})
+    for c in containers:
+        if c.name == container_name:
+            return c
diff --git a/src/test/src/lib/helpers.py b/src/test/src/lib/helpers.py
deleted file mode 100644
index ab5bd9b5..00000000
--- a/src/test/src/lib/helpers.py
+++ /dev/null
@@ -1,137 +0,0 @@
-#!usr/bin/env/python3
-# -*- coding: utf-8 -*-
-
-import os
-import docker
-import subprocess
-
-from minitrino.settings import RESOURCE_LABEL
-
-
-def execute_command(cmd="", container=None, env={}):
-    """Executes a command in the user's shell or inside of a container.
-
-    - `cmd`: The command to execute.
-    - `container`: Container name to execute command inside of.
-    - `env`: Environment variables to pass to the container.
-
-    Returns command, output, and return code in a dict."""
-
-    if container:
-        return execute_in_container(cmd, container, env)
-    else:
-        return execute_in_shell(cmd, env)
-
-
-def execute_in_shell(cmd="", env={}):
-    """Executes a command in the host shell."""
-
-    print(f"Executing command on host shell: {cmd}")
-
-    process = subprocess.Popen(
-        cmd,
-        shell=True,
-        bufsize=1,
-        stdout=subprocess.PIPE,
-        stderr=subprocess.STDOUT,
-        universal_newlines=True,
-        env={**os.environ, **env},
-    )
-
-    output = ""
-    print("Command output:")
-    with process as p:
-        for line in p.stdout:
-            output += line
-            print(line, end="")  # process line here
-
-    return {
-        "command": cmd,
-        "output": output,
-        "return_code": process.returncode,
-    }
-
-
-def execute_in_container(cmd="", container_name="", env={}):
-    """Executes a command inside of a container through the Docker SDK
-    (similar to `docker exec`)."""
-
-    docker_url = os.environ.get("DOCKER_HOST", "")
-    api_client = docker.APIClient(base_url=docker_url)
-
-    container = get_container(container_name)
-
-    print(f"Executing command in container '{container.name}': {cmd}")
-
-    exec_handler = api_client.exec_create(
-        container.name, cmd=cmd, privileged=True, tty=True, environment=env
-    )
-
-    output_generator = api_client.exec_start(exec_handler, stream=True)
-
-    output = ""
-    full_line = ""
-    print("Command output:")
-    for chunk in output_generator:
-        chunk = chunk.decode()
-        output += chunk
-        chunk = chunk.split("\n", 1)
-        if len(chunk) > 1:  # Indicates newline present
-            full_line += chunk[0]
-            print(full_line, end="")
-            full_line = ""
-            if chunk[1]:
-                full_line = chunk[1]
-        else:
-            full_line += chunk[0]
-
-    if full_line:
-        print(full_line, end="")
-
-    return_code = api_client.exec_inspect(exec_handler["Id"]).get("ExitCode")
-
-    return {"command": cmd, "output": output, "return_code": return_code}
-
-
-def get_container(container_name=""):
-    """Fetches running container by container name."""
-
-    docker_url = os.environ.get("DOCKER_HOST", "")
-    docker_client = docker.DockerClient(base_url=docker_url)
-    containers = docker_client.containers.list(filters={"label": RESOURCE_LABEL})
-    for c in containers:
-        if c.name == container_name:
-            return c
-
-
-def cleanup(remove_images=False):
-    """Removes running Minitrino environments and deletes volumes. If specified,
-    also removes images."""
-
-    execute_command("minitrino -v down --sig-kill")
-    execute_command("minitrino -v remove --volumes")
-
-    if remove_images:
-        print("Removing images...")
-        execute_command(
-            'docker images -q | grep -v "$(docker images minitrino/trino -q)" | xargs -r docker rmi'
-        )
-
-    print("Disk space usage:")
-    execute_command("df -h")
-
-
-def dump_container_logs():
-    """Dumps all container logs to stdout when an exception is raised."""
-
-    docker_url = os.environ.get("DOCKER_HOST", "")
-    docker_client = docker.DockerClient(base_url=docker_url)
-    containers = docker_client.containers.list(
-        all=True, filters={"label": RESOURCE_LABEL}
-    )
-
-    for container in containers:
-        print(f"Dumping logs for container {container.name}:")
-        logs = container.logs().decode("utf-8")  # Decode binary logs to string
-        print(logs)
-        print("\n")
diff --git a/src/test/src/lib/json/biac.json b/src/test/src/lib/json/biac.json
index 087ad141..2f53c8ff 100644
--- a/src/test/src/lib/json/biac.json
+++ b/src/test/src/lib/json/biac.json
@@ -50,4 +50,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/cache-service.json b/src/test/src/lib/json/cache-service.json
index 5fbedc7f..f58fb699 100644
--- a/src/test/src/lib/json/cache-service.json
+++ b/src/test/src/lib/json/cache-service.json
@@ -61,4 +61,4 @@
       "timeout": 180
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/clickhouse.json b/src/test/src/lib/json/clickhouse.json
new file mode 100644
index 00000000..7f6ac2a8
--- /dev/null
+++ b/src/test/src/lib/json/clickhouse.json
@@ -0,0 +1,70 @@
+{
+  "tests": [
+    {
+      "type": "query",
+      "name": "Test create schema",
+      "sql": "CREATE SCHEMA clickhouse.test_schema",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user admin"
+      ],
+      "contains": [
+        "CREATE SCHEMA"
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Test schema list",
+      "sql": "SHOW SCHEMAS IN clickhouse",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user admin"
+      ],
+      "contains": [
+        "information_schema",
+        "test_schema",
+        "minitrino"
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Test table list",
+      "sql": "SHOW TABLES IN clickhouse.minitrino",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user admin"
+      ],
+      "contains": [
+        "table1",
+        "table2",
+        "table3"
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Test CTAS",
+      "sql": "CREATE TABLE clickhouse.test_schema.test_table AS SELECT * FROM tpch.tiny.customer LIMIT 10",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user admin"
+      ],
+      "contains": [
+        "CREATE TABLE: 10 rows"
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Test table select",
+      "sql": "SELECT * FROM clickhouse.test_schema.test_table LIMIT 10",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user admin"
+      ],
+      "contains": [
+        "custkey",
+        "name",
+        "address"
+      ]
+    }
+  ]
+}
diff --git a/src/test/src/lib/json/data-products.json b/src/test/src/lib/json/data-products.json
index ced2b86f..d69e1bf3 100644
--- a/src/test/src/lib/json/data-products.json
+++ b/src/test/src/lib/json/data-products.json
@@ -41,4 +41,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/delta-lake.json b/src/test/src/lib/json/delta-lake.json
index 21e2a620..502b9052 100644
--- a/src/test/src/lib/json/delta-lake.json
+++ b/src/test/src/lib/json/delta-lake.json
@@ -62,4 +62,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/elasticsearch.json b/src/test/src/lib/json/elasticsearch.json
index aa47b26a..acbcf6ee 100644
--- a/src/test/src/lib/json/elasticsearch.json
+++ b/src/test/src/lib/json/elasticsearch.json
@@ -46,4 +46,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/file-access-control.json b/src/test/src/lib/json/file-access-control.json
index 0b94ea1e..910f3a25 100644
--- a/src/test/src/lib/json/file-access-control.json
+++ b/src/test/src/lib/json/file-access-control.json
@@ -20,7 +20,7 @@
       "name": "Test platform user query (access denied)",
       "trinoCliArgs": [
         "--server 'http://trino:8080'",
-        "--user platform"
+        "--user platform-user"
       ],
       "contains": [
         "Access Denied"
@@ -32,7 +32,7 @@
       "sql": "SELECT * FROM tpch.tiny.customer LIMIT 1",
       "trinoCliArgs": [
         "--server 'http://trino:8080'",
-        "--user metadata"
+        "--user metadata-user"
       ],
       "contains": [
         "Access Denied"
@@ -44,7 +44,7 @@
       "sql": "SHOW TABLES IN system.metadata",
       "trinoCliArgs": [
         "--server 'http://trino:8080'",
-        "--user metadata"
+        "--user metadata-user"
       ],
       "contains": [
         "catalogs"
@@ -56,7 +56,7 @@
       "sql": "SHOW TABLES IN system.runtime",
       "trinoCliArgs": [
         "--server 'http://trino:8080'",
-        "--user platform"
+        "--user platform-user"
       ],
       "contains": [
         "nodes",
@@ -74,4 +74,4 @@
       "rowCount": 0
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/file-group-provider.json b/src/test/src/lib/json/file-group-provider.json
index a6c53141..85357cc5 100644
--- a/src/test/src/lib/json/file-group-provider.json
+++ b/src/test/src/lib/json/file-group-provider.json
@@ -18,7 +18,7 @@
       "sql": "SELECT current_groups()",
       "trinoCliArgs": [
         "--server 'http://trino:8080'",
-        "--user metadata"
+        "--user metadata-user"
       ],
       "contains": [
         "metadata-users"
@@ -30,7 +30,7 @@
       "sql": "SELECT current_groups()",
       "trinoCliArgs": [
         "--server 'http://trino:8080'",
-        "--user platform"
+        "--user platform-user"
       ],
       "contains": [
         "platform-users"
@@ -48,4 +48,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/hive.json b/src/test/src/lib/json/hive.json
index e8068b94..051efeb0 100644
--- a/src/test/src/lib/json/hive.json
+++ b/src/test/src/lib/json/hive.json
@@ -62,4 +62,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/iceberg.json b/src/test/src/lib/json/iceberg.json
index 0433d68c..1b850dfd 100644
--- a/src/test/src/lib/json/iceberg.json
+++ b/src/test/src/lib/json/iceberg.json
@@ -73,4 +73,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/insights.json b/src/test/src/lib/json/insights.json
index fd15f677..d1f76e27 100644
--- a/src/test/src/lib/json/insights.json
+++ b/src/test/src/lib/json/insights.json
@@ -24,4 +24,4 @@
       "timeout": 60
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/ldap.json b/src/test/src/lib/json/ldap.json
index 596d5372..39918698 100644
--- a/src/test/src/lib/json/ldap.json
+++ b/src/test/src/lib/json/ldap.json
@@ -28,4 +28,4 @@
       "timeout": 60
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/mariadb.json b/src/test/src/lib/json/mariadb.json
index b4d3655a..86e41db8 100644
--- a/src/test/src/lib/json/mariadb.json
+++ b/src/test/src/lib/json/mariadb.json
@@ -53,4 +53,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/mysql-event-listener.json b/src/test/src/lib/json/mysql-event-listener.json
new file mode 100644
index 00000000..3f921025
--- /dev/null
+++ b/src/test/src/lib/json/mysql-event-listener.json
@@ -0,0 +1,29 @@
+{
+  "tests": [
+    {
+      "type": "query",
+      "name": "Prime event listener database",
+      "sql": "SELECT 123",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user admin"
+      ],
+      "contains": [
+        "123"
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Query event listener database",
+      "sql": "SELECT query, query_state FROM mysql_event_listener.event_listener.trino_queries WHERE query = 'SELECT 123'",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user admin"
+      ],
+      "contains": [
+        "SELECT 1",
+        "FINISHED"
+      ]
+    }
+  ]
+}
diff --git a/src/test/src/lib/json/mysql.json b/src/test/src/lib/json/mysql.json
index 45dcc8f1..24b385be 100644
--- a/src/test/src/lib/json/mysql.json
+++ b/src/test/src/lib/json/mysql.json
@@ -53,4 +53,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/oauth2.json b/src/test/src/lib/json/oauth2.json
index 4d302ecd..3d453808 100644
--- a/src/test/src/lib/json/oauth2.json
+++ b/src/test/src/lib/json/oauth2.json
@@ -28,4 +28,4 @@
       "timeout": 60
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/password-file.json b/src/test/src/lib/json/password-file.json
index 596d5372..39918698 100644
--- a/src/test/src/lib/json/password-file.json
+++ b/src/test/src/lib/json/password-file.json
@@ -28,4 +28,4 @@
       "timeout": 60
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/pinot.json b/src/test/src/lib/json/pinot.json
index d4fd0a84..5e2c2478 100644
--- a/src/test/src/lib/json/pinot.json
+++ b/src/test/src/lib/json/pinot.json
@@ -72,4 +72,4 @@
       "rowCount": 10
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/postgres.json b/src/test/src/lib/json/postgres.json
index 02d9da67..1c8fde78 100644
--- a/src/test/src/lib/json/postgres.json
+++ b/src/test/src/lib/json/postgres.json
@@ -53,4 +53,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/resource-groups.json b/src/test/src/lib/json/resource-groups.json
new file mode 100644
index 00000000..5089fa3a
--- /dev/null
+++ b/src/test/src/lib/json/resource-groups.json
@@ -0,0 +1,124 @@
+{
+  "tests": [
+    {
+      "type": "query",
+      "name": "Confirm admin resource group",
+      "sql": "SELECT 'admin'",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user admin"
+      ],
+      "contains": [
+        "admin"
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Confirm admin resource group from completed query",
+      "sql": "SELECT resource_group_id FROM system.runtime.queries WHERE query = 'SELECT ''admin''' AND user = 'admin'",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user admin"
+      ],
+      "contains": [
+        "[admin]"
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Confirm metadataQueriesSkipTheQueue resource group",
+      "sql": "SHOW CREATE TABLE tpch.tiny.customer",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user foo"
+      ],
+      "contains": [
+        "custkey"
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Confirm metadataQueriesSkipTheQueue resource group from completed query",
+      "sql": "SELECT resource_group_id FROM system.runtime.queries WHERE query = 'SHOW CREATE TABLE tpch.tiny.customer' AND user = 'foo'",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user admin"
+      ],
+      "contains": [
+        "[metadataQueriesSkipTheQueue]"
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Confirm global.systemMetadata resource group",
+      "sql": "SELECT * FROM system.jdbc.tables LIMIT 1",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user metadata-user"
+      ],
+      "contains": [
+        "table_cat"
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Confirm global.systemMetadata resource group from completed query",
+      "sql": "SELECT resource_group_id FROM system.runtime.queries WHERE query = 'SELECT * FROM system.jdbc.tables LIMIT 1' AND user = 'metadata-user'",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user admin"
+      ],
+      "contains": [
+        "[global, systemMetadata, metadata-user]"
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Confirm global.systemRuntime resource group",
+      "sql": "SELECT * FROM system.runtime.queries LIMIT 1",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user platform-user"
+      ],
+      "contains": [
+        "query_id"
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Confirm global.systemRuntime resource group from completed query",
+      "sql": "SELECT resource_group_id FROM system.runtime.queries WHERE query = 'SELECT * FROM system.runtime.queries LIMIT 1' AND user = 'platform-user'",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user admin"
+      ],
+      "contains": [
+        "[global, systemRuntime, platform-user]"
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Confirm global.other resource group",
+      "sql": "SELECT 'foo'",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user foo"
+      ],
+      "contains": [
+        "foo"
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Confirm global.other resource group from completed query",
+      "sql": "SELECT resource_group_id FROM system.runtime.queries WHERE query = 'SELECT ''foo''' AND user = 'foo'",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user admin"
+      ],
+      "contains": [
+        "[global, other, foo]"
+      ]
+    }
+  ]
+}
diff --git a/src/test/src/lib/json/session-property-manager.json b/src/test/src/lib/json/session-property-manager.json
new file mode 100644
index 00000000..59d1aea6
--- /dev/null
+++ b/src/test/src/lib/json/session-property-manager.json
@@ -0,0 +1,160 @@
+{
+  "tests": [
+    {
+      "type": "query",
+      "name": "Confirm session properties for admin resource group",
+      "sql": "SELECT 'admin-user'",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user admin"
+      ],
+      "contains": [
+        "admin-user"
+      ]
+    },
+    {
+      "type": "shell",
+      "name": "Fetch query ID to confirm correct session properties",
+      "container": "trino",
+      "command": "trino-cli --user admin --output-format CSV_UNQUOTED --execute \"SELECT query_id FROM system.runtime.queries WHERE query = 'SELECT ''admin-user'''\"",
+      "exitCode": 0,
+      "subCommands": [
+        {
+          "name": "Parse query JSON",
+          "command": "sh -c \"curl -s -u admin: localhost:8080/v1/query/${PREV_OUTPUT} | jq -r '.session.systemProperties'\"",
+          "container": "trino",
+          "exitCode": 0,
+          "contains": [
+            "resource_overcommit",
+            "query_max_execution_time",
+            "8h"
+          ]
+        }
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Confirm session properties for metadataQueriesSkipTheQueue resource group",
+      "sql": "SHOW CREATE TABLE tpch.tiny.customer",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user foo"
+      ],
+      "contains": [
+        "custkey"
+      ]
+    },
+    {
+      "type": "shell",
+      "name": "Fetch query ID to confirm correct session properties",
+      "container": "trino",
+      "command": "trino-cli --user admin --output-format CSV_UNQUOTED --execute \"SELECT query_id FROM system.runtime.queries WHERE query = 'SHOW CREATE TABLE tpch.tiny.customer' AND user = 'foo'\"",
+      "exitCode": 0,
+      "subCommands": [
+        {
+          "name": "Parse query JSON",
+          "command": "sh -c \"curl -s -u admin: localhost:8080/v1/query/${PREV_OUTPUT} | jq -r '.session.systemProperties'\"",
+          "container": "trino",
+          "exitCode": 0,
+          "contains": [
+            "query_max_execution_time",
+            "2h"
+          ]
+        }
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Confirm session properties for global.systemMetadata resource group",
+      "sql": "SELECT 'metadata-user'",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user metadata-user"
+      ],
+      "contains": [
+        "metadata-user"
+      ]
+    },
+    {
+      "type": "shell",
+      "name": "Fetch query ID to confirm correct session properties",
+      "container": "trino",
+      "command": "trino-cli --user admin --output-format CSV_UNQUOTED --execute \"SELECT query_id FROM system.runtime.queries WHERE query = 'SELECT ''metadata-user'''\"",
+      "exitCode": 0,
+      "subCommands": [
+        {
+          "name": "Parse query JSON",
+          "command": "sh -c \"curl -s -u admin: localhost:8080/v1/query/${PREV_OUTPUT} | jq -r '.session.systemProperties'\"",
+          "container": "trino",
+          "exitCode": 0,
+          "contains": [
+            "query_max_execution_time",
+            "30m"
+          ]
+        }
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Confirm session properties for global.systemRuntime resource group",
+      "sql": "SELECT 'platform-user'",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user platform-user"
+      ],
+      "contains": [
+        "platform-user"
+      ]
+    },
+    {
+      "type": "shell",
+      "name": "Fetch query ID to confirm correct session properties",
+      "container": "trino",
+      "command": "trino-cli --user admin --output-format CSV_UNQUOTED --execute \"SELECT query_id FROM system.runtime.queries WHERE query = 'SELECT ''platform-user'''\"",
+      "exitCode": 0,
+      "subCommands": [
+        {
+          "name": "Parse query JSON",
+          "command": "sh -c \"curl -s -u admin: localhost:8080/v1/query/${PREV_OUTPUT} | jq -r '.session.systemProperties'\"",
+          "container": "trino",
+          "exitCode": 0,
+          "contains": [
+            "query_max_execution_time",
+            "10m"
+          ]
+        }
+      ]
+    },
+    {
+      "type": "query",
+      "name": "Confirm session properties for global.other resource group",
+      "sql": "SELECT 'foo'",
+      "trinoCliArgs": [
+        "--server 'http://trino:8080'",
+        "--user foo"
+      ],
+      "contains": [
+        "foo"
+      ]
+    },
+    {
+      "type": "shell",
+      "name": "Fetch query ID to confirm correct session properties",
+      "container": "trino",
+      "command": "trino-cli --user admin --output-format CSV_UNQUOTED --execute \"SELECT query_id FROM system.runtime.queries WHERE query = 'SELECT ''foo'''\"",
+      "exitCode": 0,
+      "subCommands": [
+        {
+          "name": "Parse query JSON",
+          "command": "sh -c \"curl -s -u admin: localhost:8080/v1/query/${PREV_OUTPUT} | jq -r '.session.systemProperties'\"",
+          "container": "trino",
+          "exitCode": 0,
+          "contains": [
+            "query_max_execution_time",
+            "1h"
+          ]
+        }
+      ]
+    }
+  ]
+}
diff --git a/src/test/src/lib/json/sqlserver.json b/src/test/src/lib/json/sqlserver.json
index 827499f6..6690dbc3 100644
--- a/src/test/src/lib/json/sqlserver.json
+++ b/src/test/src/lib/json/sqlserver.json
@@ -51,4 +51,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/json/tls.json b/src/test/src/lib/json/tls.json
index 4d4b567c..d57778e3 100644
--- a/src/test/src/lib/json/tls.json
+++ b/src/test/src/lib/json/tls.json
@@ -36,4 +36,4 @@
       "timeout": 60
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/src/lib/runner.py b/src/test/src/lib/runner.py
index eae0f1f2..f428a080 100644
--- a/src/test/src/lib/runner.py
+++ b/src/test/src/lib/runner.py
@@ -10,10 +10,8 @@
 
 import src.common as common
 from src.lib.specs import SPECS
-from src.lib.helpers import cleanup
-from src.lib.helpers import get_container
-from src.lib.helpers import execute_command
-from src.lib.helpers import dump_container_logs
+from src.lib.utils import cleanup
+from src.lib.utils import dump_container_logs
 
 
 class ModuleTest:
@@ -44,10 +42,10 @@ def run_tests(self, tests=[], workers=False):
         """Runs module tests."""
 
         if not workers:
-            execute_command(f"minitrino -v provision -m {self.module} --no-rollback")
+            common.execute_command(f"minitrino -v provision -m {self.module}")
         else:
-            execute_command(
-                f"minitrino -v provision -m {self.module} --workers 1 --no-rollback"
+            common.execute_command(
+                f"minitrino -v provision -m {self.module} --workers 1"
             )
 
         for t in tests:
@@ -80,7 +78,7 @@ def test_query(self, json_data={}):
         i = 0
         cmd = "curl -X GET -H 'Accept: application/json' -H 'X-Trino-User: admin' 'localhost:8080/v1/info/'"
         while i <= 60:
-            output = execute_command(cmd, "trino")
+            output = common.execute_command(cmd, "trino")
             if '"starting":false' in output.get("output", ""):
                 time.sleep(5)  # hard stop to ensure coordinator is ready
                 break
@@ -103,7 +101,7 @@ def test_query(self, json_data={}):
                 cmd += f" {i}"
 
         # Execute query
-        output = execute_command(cmd, "trino", json_data.get("env", {}))
+        output = common.execute_command(cmd, "trino", json_data.get("env", {}))
 
         # Run assertions
         for c in contains:
@@ -135,14 +133,14 @@ def test_shell(self, json_data={}):
         # Run command
         cmd = json_data.get("command", "")
         container = json_data.get("container", None)
-        output = execute_command(cmd, container, json_data.get("env", {}))
+        output = common.execute_command(cmd, container, json_data.get("env", {}))
 
         # Run assertions
         if exit_code is not None:
-            cmd_exit_code = output.get("return_code")
+            cmd_exit_code = output.get("exit_code")
             assert (
                 exit_code == cmd_exit_code
-            ), f"Unexpected return code: {cmd_exit_code} expected: {exit_code}"
+            ), f"Unexpected exit code: {cmd_exit_code} expected: {exit_code}"
 
         for c in contains:
             assert c in output.get("output"), f"'{c}' not in command output"
@@ -159,7 +157,7 @@ def test_logs(self, json_data):
         """Checks for matching strings in container logs."""
 
         timeout = json_data.get("timeout", 30)
-        container = get_container(json_data.get("container"))
+        container = common.get_container(json_data.get("container"))
 
         i = 0
         while True:
@@ -201,17 +199,17 @@ def _execute_subcommand(self, json_data={}, healthcheck=False, prev_output=""):
 
         i = 0
         while True:
-            output = execute_command(cmd, container, json_data.get("env", {}))
+            output = common.execute_command(cmd, container, json_data.get("env", {}))
             try:
                 for c in contains:
                     assert c in output.get(
                         "output", ""
                     ), f"'{c}' not in {cmd_type} output"
-                cmd_exit_code = output.get("return_code")
+                cmd_exit_code = output.get("exit_code")
                 if isinstance(exit_code, int):
                     assert (
                         exit_code == cmd_exit_code
-                    ), f"Unexpected return code: {cmd_exit_code} expected: {exit_code}"
+                    ), f"Unexpected exit code: {cmd_exit_code} expected: {exit_code}"
                 return output
             except AssertionError as e:
                 if i < retry:
diff --git a/src/test/src/lib/utils.py b/src/test/src/lib/utils.py
new file mode 100644
index 00000000..9d4f21e5
--- /dev/null
+++ b/src/test/src/lib/utils.py
@@ -0,0 +1,41 @@
+#!usr/bin/env/python3
+# -*- coding: utf-8 -*-
+
+import os
+import docker
+
+import src.common as common
+from minitrino.settings import RESOURCE_LABEL
+
+
+def cleanup(remove_images=False):
+    """Removes running Minitrino environments and deletes volumes. If specified,
+    also removes images."""
+
+    common.execute_command("minitrino -v down --sig-kill")
+    common.execute_command("minitrino -v remove --volumes")
+
+    if remove_images:
+        print("Removing images...")
+        common.execute_command(
+            'docker images -q | grep -v "$(docker images minitrino/trino -q)" | xargs -r docker rmi'
+        )
+
+    print("Disk space usage:")
+    common.execute_command("df -h")
+
+
+def dump_container_logs():
+    """Dumps all container logs to stdout when an exception is raised."""
+
+    docker_url = os.environ.get("DOCKER_HOST", "")
+    docker_client = docker.DockerClient(base_url=docker_url)
+    containers = docker_client.containers.list(
+        all=True, filters={"label": RESOURCE_LABEL}
+    )
+
+    for container in containers:
+        print(f"Dumping logs for container {container.name}:")
+        logs = container.logs().decode("utf-8")  # Decode binary logs to string
+        print(logs)
+        print("\n")