diff --git a/dev-container/Dockerfile.development b/dev-container/Dockerfile.development new file mode 100644 index 000000000..8321d853b --- /dev/null +++ b/dev-container/Dockerfile.development @@ -0,0 +1,43 @@ +FROM public.ecr.aws/amazonlinux/amazonlinux:2023 + +RUN dnf upgrade -y && \ + dnf install -y \ + fuse \ + fuse-devel \ + cmake3 \ + clang \ + clang-devel \ + git \ + pkg-config \ + jq && \ + dnf clean all + +# Configure FUSE +RUN echo "user_allow_other" >> /etc/fuse.conf + +# Create non-root user +RUN useradd -m -s /bin/bash dev-user && \ + usermod -aG wheel dev-user + +# Install Rust as dev-user +USER dev-user +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none +ENV PATH="/home/dev-user/.cargo/bin:${PATH}" + +# Set colored prompt +RUN echo 'PS1="\[\033[1;36m\][dev-container]:\[\033[1;34m\]\w\[\033[0m\] \$ "' >> /home/dev-user/.bashrc + +WORKDIR /workspace + +RUN mkdir -p /workspace/target + +# Install cargo-nextest and Rust version based on toolchain config at build time +COPY --chown=dev-user:dev-user rust-toolchain.toml ./ +RUN rustup show && cargo install cargo-nextest --locked + +ENV RUST_BACKTRACE=1 + +COPY --chown=dev-user:dev-user dev-container/entrypoint.sh /home/dev-user/entrypoint.sh +RUN chmod +x /home/dev-user/entrypoint.sh + +ENTRYPOINT ["/home/dev-user/entrypoint.sh"] diff --git a/dev-container/README.md b/dev-container/README.md new file mode 100644 index 000000000..6ba514ad1 --- /dev/null +++ b/dev-container/README.md @@ -0,0 +1,50 @@ +# Development Container + +Docker container for running Mountpoint S3 tests on Linux. Useful for macOS users to test against a Linux kernel/OS. + +The container uses runtime builds with persistent caching: +- System dependencies are baked into the image at build time +- Rust toolchains (`~/.rustup/`), cargo dependencies, and build artifacts are cached in Docker volumes between runs +- On each container start, the entrypoint ensures the correct Rust toolchain (from `rust-toolchain.toml`) is installed +- Source code is mounted at runtime (not baked into image) + +## Quick Start + +```bash +# Build and run the container +./dev-container/run.py --build + +# Interactive shell +./dev-container/run.py + +# Run a command (use -- to separate script args from container args) +./dev-container/run.py --use-credentials-from-aws-config -- cargo nextest run --features s3_tests,fuse_tests -p mountpoint-s3-fs +``` + +## Credential Options + +You'll need to configure AWS credentials if you want to run things like the integration tests which access S3. + +As an example, you might have credentials available in your `~/.aws/` folder. +You can use the `--use-credentials-from-aws-config` option to bind mount that directory into the container. + +See `--help` for more available arguments. + +## When to Rebuild + +Rebuild the image when: +- System dependencies need updating + +You do **not** need to rebuild when: +- Rust toolchain version changes in `rust-toolchain.toml` (the entrypoint installs the correct toolchain automatically) +- Code or cargo dependencies change (cached in Docker volumes) + +## Cleanup + +To remove the cached rustup volume (e.g., when old toolchains accumulate): + +```bash +./dev-container/run.py --drop-rustup-volume +``` + +Build artifacts and cargo dependencies are cached in Docker volumes between runs, so you don't need to rebuild for code or dependency changes. diff --git a/dev-container/dev.py b/dev-container/dev.py new file mode 100755 index 000000000..83d0d46de --- /dev/null +++ b/dev-container/dev.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +import argparse +import os +import subprocess +import sys + +CONTAINER_USER = 'dev-user' +CARGO_CACHE_VOLUME = 'mountpoint-s3-cargo-cache' +CARGO_TARGET_VOLUME = 'mountpoint-s3-target-cache' +RUSTUP_VOLUME = 'mountpoint-s3-rustup-home' + + +def handle_build(args): + subprocess.run(['docker', 'build', '-f', 'dev-container/Dockerfile.development', '-t', args.image, '.'], check=True) + + +def handle_run(args, container_args): + docker_args = [ + 'docker', + 'run', + '-it', + '--rm', + '--device=/dev/fuse', + '--privileged', + f'-v={os.getcwd()}:/workspace', + f'-v={CARGO_CACHE_VOLUME}:/home/{CONTAINER_USER}/.cargo/registry', + f'-v={CARGO_TARGET_VOLUME}:/workspace/target', + f'-v={RUSTUP_VOLUME}:/home/{CONTAINER_USER}/.rustup', + ] + + dotenv_path = f"{os.getcwd()}/.env" + if os.path.exists(dotenv_path): + docker_args.extend(['--env-file', dotenv_path]) + + if args.use_credentials_from_env: + for var in ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN', 'AWS_REGION']: + if var in os.environ: + docker_args.extend(['-e', var]) + elif args.use_credentials_from_aws_config: + aws_dir = os.path.expanduser('~/.aws') + if os.path.exists(aws_dir): + docker_args.extend(['-v', f'{aws_dir}:/home/{CONTAINER_USER}/.aws:ro']) + + if container_args: + docker_args.extend([args.image, '/bin/bash', '-c', ' '.join(container_args)]) + else: + docker_args.extend([args.image, '/bin/bash']) + + subprocess.run(docker_args) + + +def handle_clean(args): + result = subprocess.run( + ['docker', 'ps', '-q', '--filter', f'ancestor={args.image}'], capture_output=True, text=True + ) + if result.stdout.strip(): + print(f"Error: a container using image '{args.image}' is still running. Stop it before cleaning.") + sys.exit(1) + print("Deleting docker volumes") + for volume in [CARGO_CACHE_VOLUME, CARGO_TARGET_VOLUME, RUSTUP_VOLUME]: + subprocess.run(['docker', 'volume', 'rm', volume], check=False) + + +def main(): + parser = argparse.ArgumentParser(description='Mountpoint S3 development container tool') + parser.add_argument('--image', default='mountpoint-s3-dev', help='Docker image name') + subparsers = parser.add_subparsers(dest='command', required=True) + + # build + subparsers.add_parser('build', help='Build the development container image') + + # run + run_parser = subparsers.add_parser('run', help='Run the development container') + creds_group = run_parser.add_mutually_exclusive_group() + creds_group.add_argument('--use-credentials-from-env', action='store_true', help='Pass through AWS env vars') + creds_group.add_argument('--use-credentials-from-aws-config', action='store_true', help='Mount ~/.aws as read-only') + + # clean + subparsers.add_parser('clean', help='Remove all dev container Docker volumes') + + args, container_args = parser.parse_known_args() + # Remove '--' separator if present + if container_args and container_args[0] == '--': + container_args = container_args[1:] + + if args.command == 'build': + handle_build(args) + elif args.command == 'run': + handle_run(args, container_args) + elif args.command == 'clean': + handle_clean(args) + + +if __name__ == '__main__': + main() diff --git a/dev-container/entrypoint.sh b/dev-container/entrypoint.sh new file mode 100755 index 000000000..b4988f314 --- /dev/null +++ b/dev-container/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +# Ensure the correct Rust toolchain is installed (reads /workspace/rust-toolchain.toml) +rustup show active-toolchain + +if [ $# -eq 0 ]; then + exec /bin/bash +else + exec "$@" +fi diff --git a/doc/DEVELOPMENT.md b/doc/DEVELOPMENT.md index 9130ccc93..2d5f4c853 100644 --- a/doc/DEVELOPMENT.md +++ b/doc/DEVELOPMENT.md @@ -110,6 +110,8 @@ The `docs/INSTALL.md` has a section on building from source which can get you st For running tests, you should [install cargo-nextest](https://nexte.st/docs/installation/pre-built-binaries/). You will need a Linux environment that has FUSE support. +If you wish to use macOS, +there is a [container available to support testing in `dev-container/`](../dev-container/README.md). You should also have AWS credentials available for testing. Short-term AWS credentials are recommended.