diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 698298c..ede719d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -49,8 +49,8 @@ jobs: - name: Run tests # FIXME: Can this be run without elevated privileges? run: | - sudo $(command -v pytest) - coverage xml + sudo --preserve-env $(command -v pytest) + coverage xml --omit postroj/winrunner.py - name: Upload coverage results to Codecov uses: codecov/codecov-action@v3 diff --git a/CHANGES.rst b/CHANGES.rst index 5b28851..7a228d3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,22 @@ in progress - Improve platform guards and naming things - Improve central command invocation function - Improve documentation +- Make Windows runner subsystem production ready. +- Add support for Windows Server Core 2019 and friends, like + ``windows/servercore:ltsc2019``, ``windows/nanoserver:1809``, or + ``eclipse-temurin:17-jdk``. +- Add support for Windows Server Core 2016, 2022 and friends, like + ``windows/servercore:ltsc2016``, ``windows/servercore:ltsc2022``, or + ``windows/nanoserver:ltsc2022``. +- Improve documentation about the Windows backend +- Rename environment variables used to control the Windows Docker Machine + subsystem. The new names are ``RACKER_WDM_VCPUS``, ``RACKER_WDM_MEMORY``, + and ``RACKER_WDM_MACHINE``. +- Add environment variable ``RACKER_WDM_PROVIDER`` to reconfigure the + Vagrant virtualization backend differently than VirtualBox. +- Documentation: Add use case how to build a Python package within a + Windows environment, using Microsoft Visual C++ Build Tools 2015 and + Anaconda, both installed using Chocolatey, and ``cibuildwheel``. 2022-05-20 0.2.0 diff --git a/README.rst b/README.rst index 758bff3..0868172 100644 --- a/README.rst +++ b/README.rst @@ -109,6 +109,13 @@ another one for Windows. `systemd-nspawn`_. Provisioning of additional software is performed using the native package manager of the corresponding Linux distribution. +- For running Windows operating systems containers, Racker uses `Vagrant`_, + `Docker`_, and `Windows Docker Machine`_. The virtual machine base image is + acquired from `Vagrant Cloud`_, container images are acquired from the + `Microsoft Container Registry`_. For provisioning additional software, the + `Chocolatey`_ package manager is used. All of cmd, PowerShell and Bash are + pre-installed on the container images. + Operating system coverage ------------------------- @@ -132,6 +139,11 @@ Linux - SUSE SLES 15 and BCI:latest - Ubuntu LTS 20 and 22 (focal, jammy) +Windows +....... +- Windows Server Core LTSC 2016, 2019, and 2022 +- Windows Nano Server 1809 and LTSC 2022 + Prior art --------- @@ -212,6 +224,8 @@ Racker The ``racker`` program aims to resemble the semantics of Docker by providing a command line interface compatible with the ``docker`` command. +Linux +----- :: # Invoke the vanilla Docker `hello-world` image. @@ -244,6 +258,20 @@ command line interface compatible with the ``docker`` command. time echo "hello world" | racker run -it --rm fedora:37 cat /dev/stdin > hello cat hello +Windows +------- + +An example of a basic command line invocation should get you started, +especially if you are familiar with the ``docker`` command:: + + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2022 -- wmic os get caption + + Caption + Microsoft Windows Server 2022 Datacenter + +More extensive information, including many examples, can be found at the +`Racker Windows backend`_ documentation. + Postroj ======= @@ -429,6 +457,7 @@ Troubleshooting .. _Packer: https://www.packer.io/ .. _Podman: https://podman.io/ .. _Racker sandbox installation: https://github.com/cicerops/racker/blob/main/doc/sandbox.rst +.. _Racker Windows backend: https://github.com/cicerops/racker/blob/main/doc/winrunner.rst .. _skopeo: https://github.com/containers/skopeo .. _systemd: https://www.freedesktop.org/wiki/Software/systemd/ .. _systemd-nspawn: https://www.freedesktop.org/software/systemd/man/systemd-nspawn.html diff --git a/doc/cratedb.rst b/doc/cratedb.rst index 45fc533..ba8a923 100644 --- a/doc/cratedb.rst +++ b/doc/cratedb.rst @@ -1,10 +1,53 @@ -######################### -Using postroj for CrateDB -######################### +####################################### +Using Racker and Postroj for CrateDB CI +####################################### -.. note:: - This is still a work in progress. +********** +racker run +********** + +Purpose: Invoke programs in a Java/OpenJDK environment, within a +virtualized/dockerized, volatile/ephemeral Windows environment. + +Run the CrateDB test suite on OpenJDK 18 (Eclipse Temurin):: + + time racker --verbose run --rm --platform=windows/amd64 eclipse-temurin:18-jdk \ + "sh -c 'mkdir /c/src; cd /c/src; git clone https://github.com/crate/crate --depth=1; cd crate; ./gradlew --no-daemon --parallel -PtestForks=2 :server:test -Dtests.crate.run-windows-incompatible=false --stacktrace'" + +Use the same image, but select a specific operating system version:: + + export RACKER_WDM_MACHINE=2019-box + racker --verbose run --rm --platform=windows/amd64 eclipse-temurin:18-jdk -- wmic os get caption + +Invoke a Java command prompt (JShell) with OpenJDK 18:: + + racker --verbose run -it --rm --platform=windows/amd64 eclipse-temurin:18-jdk jshell + System.out.println("OS: " + System.getProperty("os.name") + ", version " + System.getProperty("os.version")) + System.out.println("Java: " + System.getProperty("java.vendor") + ", version " + System.getProperty("java.version")) + +Build CrateDB from source, extract Zip archive, and invoke available programs:: + + # Spawn a Windows environment with `cmd` shell. + # TODO: Bind-mounting not possible via command line yet, need to touch the code for this. + racker --verbose run --rm -it --platform=windows/amd64 eclipse-temurin:18-jdk -- cmd + + # Build CrateDB. + cd \crate + gradlew clean distZip + + # Extract zip archive. + mkdir \tmp + cd \tmp + unzip \crate\app\build\distributions\crate-5.3.0-SNAPSHOT-327070e3fe.zip + cd crate-5.3.0-SNAPSHOT-327070e3fe + + # Run CrateDB and tools. + bin\crate + # Submit to stop + + bin\crate-node fix-metadata + # Send y to the prompt **************** diff --git a/doc/use-cases/python-on-windows.rst b/doc/use-cases/python-on-windows.rst new file mode 100644 index 0000000..f9d2adb --- /dev/null +++ b/doc/use-cases/python-on-windows.rst @@ -0,0 +1,74 @@ +############################### +Use cases for Python on Windows +############################### + + +************************* +Build wheels for PyTables +************************* + +About +===== + +DIY, without a hosted CI provider. + +How to build a Python wheel package, here PyTables, within a Windows +environment, using Microsoft Visual C++ Build Tools 2015 and Anaconda, both +installed using Chocolatey, and ``cibuildwheel``. + +References +========== + +- https://github.com/PyTables/PyTables/pull/872#issuecomment-773535041 +- https://github.com/PyTables/PyTables/blob/master/.github/workflows/wheels.yml + +Synopsis +======== + +.. note:: + + The ``windows-pytables-wheel.sh`` program is part of this repository. You + will only find it at the designated location when running ``racker`` from + the working tree of its Git repository. + + You still can get hold of the program and invoke it, by downloading it from + `windows-pytables-wheel.sh`_. + +So, let's start by defining the download URL to that file:: + + export PAYLOAD_URL=https://raw.githubusercontent.com/cicerops/racker/windows/doc/use-cases/windows-pytables-wheel.sh + +Unattended:: + + time racker --verbose run --rm --platform=windows/amd64 python:3.9 -- \ + "sh -c 'wget ${PAYLOAD_URL}; sh windows-pytables-wheel.sh'" + +Or, interactively:: + + racker --verbose run -it --rm --platform=windows/amd64 python:3.9 -- bash + wget ${PAYLOAD_URL} + sh windows-pytables-wheel.sh + + +Future +====== + +See https://github.com/cicerops/racker/issues/8. + +When working on the code base, you can invoke the program directly from +the repository, after the ``--volume`` option got implemented:: + + # Unattended. + time racker --verbose run --rm \ + --volume=C:/Users/amo/dev/cicerops-foss/sources/postroj:C:/racker \ + --platform=windows/amd64 python:3.9 -- \ + sh /c/racker/doc/use-cases/windows-pytables-wheel.sh + + # Interactively. + racker --verbose run -it --rm \ + --volume=C:/Users/amo/dev/cicerops-foss/sources/postroj:C:/racker \ + --platform=windows/amd64 python:3.9 -- bash + /c/racker/doc/use-cases/windows-pytables-wheel.sh + + +.. _windows-pytables-wheel.sh: https://raw.githubusercontent.com/cicerops/racker/main/doc/use-cases/windows-pytables-wheel.sh diff --git a/doc/use-cases/windows-pytables-wheel.sh b/doc/use-cases/windows-pytables-wheel.sh new file mode 100755 index 0000000..9842ce3 --- /dev/null +++ b/doc/use-cases/windows-pytables-wheel.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# +# Build wheels for PyTables on Windows. DIY, without a hosted CI provider. +# https://github.com/cicerops/racker/blob/main/doc/use-cases/python-on-windows.rst +# +# Synopsis:: +# +# racker --verbose run -it --rm --platform=windows/amd64 python:3.9 -- bash +# /c/racker/doc/use-cases/windows-pytables-wheel.sh +# +set -e + +# Install prerequisites. + +# Miniconda - A minimal installer for Anaconda. +# https://conda.io/miniconda.html +scoop bucket add extras +scoop install miniconda3 +#export PATH="$PATH:/c/Users/ContainerAdministrator/scoop/apps/miniconda3/current/condabin" +#alias conda=conda.bat +#/c/Users/ContainerAdministrator/scoop/apps/miniconda3/current/condabin/conda.bat init bash +source /c/Users/ContainerAdministrator/scoop/apps/miniconda3/current/Scripts/activate +conda --version + +# /c/Users/ContainerAdministrator/scoop/apps/miniconda3/4.12.0/Scripts/conda.exe + +# TODO: `/AddToPath:1` seems to not work, so adjust `$PATH` manually. +#export PATH="$PATH:/c/Tools/miniconda3/condabin" + +# TODO: At least within Bash, just addressing `conda` does not work. +#export conda="conda.bat" + + +# Check prerequisites. +#echo $PATH +#$conda --version +# cibuildwheel --version + +# Pretend to be on a build matrix. +export MATRIX_ARCH=win_amd64 # win32 +export MATRIX_ARCH_SUBDIR=win-64 # win-32 + +# Activate and prepare Anaconda environment for building. +conda create --yes --name=build +conda activate build +conda config --env --set subdir ${MATRIX_ARCH_SUBDIR} + +# Install needed libraries. +conda install --yes blosc bzip2 hdf5 lz4 lzo snappy zstd zlib + +# Install cibuildwheel. +# Build Python wheels for all the platforms on CI with minimal configuration. +# https://cibuildwheel.readthedocs.io/ +pip install --upgrade cibuildwheel + +# Configure cibuildwheel. +#export CIBW_BUILD="cp36-${MATRIX_ARCH} cp37-${MATRIX_ARCH} cp38-${MATRIX_ARCH} cp39-${MATRIX_ARCH} cp310-${MATRIX_ARCH}" +export CIBW_BUILD="cp39-${MATRIX_ARCH}" +#export CIBW_BEFORE_ALL_WINDOWS="conda create --yes --name=build && conda activate build && conda config --env --set subdir ${MATRIX_ARCH_SUBDIR} && conda install --yes blosc bzip2 hdf5 lz4 lzo snappy zstd zlib" +#export CIBW_ENVIRONMENT_WINDOWS='CONDA_PREFIX="C:\\Miniconda\\envs\\build" PATH="$PATH;C:\\Miniconda\\envs\\build\\Library\\bin"' +export CIBW_ENVIRONMENT="PYTABLES_NO_EMBEDDED_LIBS=true DISABLE_AVX2=true" +export CIBW_BEFORE_BUILD="echo $PATH; pip install -r requirements.txt cython>=0.29.21 delvewheel" +export CIBW_REPAIR_WHEEL_COMMAND_WINDOWS="delvewheel repair -w {dest_dir} {wheel}" + +# Debugging. +# env + +# Acquire sources. +mkdir -p /c/src +cd /c/src +test ! -d PyTables && git clone https://github.com/PyTables/PyTables --recursive --depth=1 +cd PyTables + +# Build wheel. +cibuildwheel --platform=windows --output-dir=wheelhouse diff --git a/doc/winrunner.rst b/doc/winrunner.rst index de388d9..03baf6f 100644 --- a/doc/winrunner.rst +++ b/doc/winrunner.rst @@ -1,17 +1,51 @@ -################# -postroj winrunner -################# +###################### +Racker Windows backend +###################### + + +***** +About +***** + +Launch an interactive command prompt (cmd, PowerShell, or Bash) within a +Windows environment (2016, 2019, or 2022), or invoke programs +non-interactively. + +Features +======== + +- The subsystem is heavily based on the excellent `Windows Docker Machine`_. +- The `Scoop`_ package manager is pre-installed on the container images + where PowerShell is available. The `Chocolatey`_ package manager can be + installed on demand. +- Programs like ``busybox``, ``curl``, ``git``, ``nano``, and ``wget`` are + pre-installed on the container images where `Scoop`_ is available. +- The `Windows container version compatibility`_ problem is conveniently + solved by automatically selecting the right machine matching the requested + container image. + + +Use cases +========= + +The first encounter with `Windows Docker Machine`_ was when aiming to run the +build process of the PyTables Python package within a Windows environment, see +`Wheels for Windows`_. + +The second use case was to run the Java test suite of CrateDB within a Windows +environment, see `Using Racker and Postroj for CrateDB CI`_. -.. note:: - This is still a work in progress. ***** -About +Setup ***** +:: -- https://github.com/PyTables/PyTables/pull/872#issuecomment-773535041 + # Install VirtualBox, Vagrant, Docker, Python, and Racker. + brew install virtualbox vagrant docker python + pip install racker ******** @@ -19,63 +53,423 @@ Synopsis ******** :: - # Basic usage. - postroj invoke --system=windows-1809 -- cmd /C echo hello + racker --verbose run --rm \ + --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2019 -- \ + wmic os get caption -***** -Setup -***** -:: +************* +Configuration +************* - # Install VirtualBox, Vagrant and Docker. - brew install virtualbox docker vagrant +Resources +========= - # Install Windows VM with Docker environment. - git clone https://github.com/StefanScherer/windows-docker-machine - cd windows-docker-machine +When creating the `Windows Docker Machine`_ virtual machine, the program will +configure it to use 4 VCPUs and 4 GB system memory by default. - # Adjust resources. - sed -i 's/v.memory = 2048/v.memory = 8192/' Vagrantfile - sed -i 's/v.cpus = 2/v.cpus = 8/' Vagrantfile +In order to adjust those values, use these environment variables before +invoking the later commands:: - # Run the box. - vagrant up --provider virtualbox 2019-box + export RACKER_WDM_VCPUS=8 + export RACKER_WDM_MEMORY=8192 +If you want to adjust the values after the initial deployment, you will have to +reset the `Windows Docker Machine`_ installation directory. For example, it is: -***** -Usage -***** +- On Linux: ``/root/.local/state/racker/windows-docker-machine`` +- On macOS: ``/Users/amo/Library/Application Support/racker/windows-docker-machine`` + + +VM provider +=========== + +`Vagrant`_ is able to use different providers as virtualization backend. By +default, Racker selects `VirtualBox`_. In order to change the backend, +reconfigure this environment variable:: + + export RACKER_WDM_PROVIDER=vmware_workstation + +Possible values are, in alphabetical order, ``hyperv``, ``virtualbox``, +``qemu``, ``vmware_fusion``, ``vmware_workstation``. + +Please note that this has not been tested with providers other than +`VirtualBox`_, so we would welcome to receive feedback from the community +whether this also works well for them on other hypervisors. + + +VM machine +========== + +The architecture of Windows leads to container compatibility requirements that +are different than on Linux, more background about this detail can be found at +`Windows container version compatibility`_. + +In order to provide appropriate convenience, Racker's launcher subsystem +inquires the ``os.version`` attribute of the OCI image about the designated +version of Windows version before starting the container. Based on the version, +the corresponding Windows Docker Machine host is selected to run the payload +on. Currently, the supported operating systems are Windows 2016, 2019, and 2022. + +Certain container images can still be launched on mismatching operating system +versions, for example, the `eclipse-temurin`_ container images. By default, +when possible, the image will be launched on a Windows 2022 machine. If you +want to explicitly control on which runner host the container will be launched, +use another environment variable:: + + export RACKER_WDM_MACHINE=2019-box + +If you receive error messages like ``docker: no matching manifest for +windows/amd64 10.0.17763 in the manifest list entries.``, reset this setting +by typing:: + + unset RACKER_WDM_MACHINE + +Vagrant stores the boxes in this directory: + +- On Linux an macOS: ``~/.vagrant.d/boxes`` +- On Windows: ``C:/Users/USERNAME/.vagrant.d/boxes`` + +The sizes of the three Vagrant boxes are: -Which shell spawns faster? +- ``StefanScherer/windows_2016_docker``: 11.0 GB +- ``StefanScherer/windows_2019_docker``: 14.0 GB +- ``StefanScherer/windows_2022_docker``: 6.4 GB + + +******** +Examples +******** + + +Introduction +============ + +For understanding some of the acronyms used in the following section, it is +good to memorize those: + +- LTSC: Long-Term Servicing Channel +- SAC: Semi-Annual Channel + +For more details, see `Overview of System Center release options`_. + + +Choosing a base image +===================== + +Quoting from `Windows Container Base Images`_: + + How do you choose the right base image to build upon? For most users, Windows + Server Core and Nanoserver will be the most appropriate image to use. Each + base image is briefly described below: + + - ``Nano Server`` is an ultralight Windows offering for new application + development. + - ``Server Core`` is medium in size and a good option for "lifting and + shifting" Windows Server apps. + - ``Windows Server`` has full Windows API support, and allows you to use + more server features. + - ``Windows`` is the largest image and has full Windows API support for + workloads. + +The examples outlined within this section will use different Windows container +images. According to the feature set outlined above, their download sizes are +different. + +- Nano Server: 125 MB +- Server Core: 2.2 GB +- Windows Server: 4.8 GB +- Windows: 7.1 GB + +Around 2016/2019, it was like https://stefanscherer.github.io/windows-docker-workshop/#20. + + +System information +================== + +Install and run `Winfetch`_:: + + racker --verbose run --rm \ + --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2022 -- \ + cmd /C 'scoop install winfetch & winfetch' + +.. figure:: https://user-images.githubusercontent.com/453543/173195228-b75c8727-7187-4c38-ae28-f74098dfb450.png + :width: 800 + +With ``ver``, ``reg``, WMI and PowerShell:: + + # Both ``ver`` and ``reg`` will be available even on Nano Server. + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2016 -- cmd /C ver + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2016 -- 'reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v ProductName' + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2016 -- 'reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v InstallationType' + + # WMI and PowerShell are not always available. + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2016 -- wmic os get caption + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2019 -- powershell -Command Get-ComputerInfo + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2022 -- powershell -Command Get-ComputerInfo -Property WindowsProductName + +With ``busybox``:: + + racker --verbose run -it --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2022 -- cmd + + C:\>busybox nproc + 6 + + C:\>busybox free -m + total used free shared buff/cache available + Mem: 2048 1422 16774251 0 3591 0 + Swap: 1664 0 1664 + + C:\>busybox df -h + Filesystem Size Used Available Use% Mounted on + C: 19.9G 83.3M 19.8G 0% C:/ + + +Interactive command prompt ========================== + +Where possible, the operating system images offer three terminal/shell +programs: cmd, PowerShell, and Bash. To get an interactive shell, run:: + + racker --verbose run -it --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2016 cmd + racker --verbose run -it --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2019 powershell + racker --verbose run -it --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2022 bash + + +Invoke single command +===================== :: - time docker --context=2019-box run -it --rm openjdk:17-windowsservercore-1809 cmd /C "echo Hello, world." + # Run a basic command with cmd, PowerShell, and Bash. + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2016 cmd /C echo "Hello, world." + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2019 -- 'powershell -Command {echo "Hello, world."}' + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2022 'sh -c "echo Hello, world."' + + # Use stdin and stdout, with time keeping. + time racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/nanoserver:1809 cmd /C echo "Hello, world." > hello + cat hello + +Nano Server +=========== :: - time docker --context=2019-box run -it --rm openjdk:17-windowsservercore-1809 powershell -Command "echo 'Hello, world.'" + # Display system version. + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/nanoserver:sac2016 cmd /C ver + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/nanoserver:1809 cmd /C ver + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/nanoserver:ltsc2022 cmd /C ver + # Interactive shell with cmd. + racker --verbose run -it --rm --platform=windows/amd64 mcr.microsoft.com/windows/nanoserver:1809 cmd + # Interactive shell with PowerShell. + racker --verbose run -it --rm --platform=windows/amd64 mcr.microsoft.com/powershell:nanoserver-ltsc2022 pwsh + + +Windows Server +============== +:: + + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/server:ltsc2022 -- cmd /C ver + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/server:ltsc2022 -- wmic os get caption + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/server:ltsc2022 -- powershell -Command Get-ComputerInfo -Property WindowsProductName -*********** -Admin guide -*********** +Windows +======= :: - docker --context=2019-box run -it --rm openjdk:17-windowsservercore-1809 cmd + # Windows 10 + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows:1809 -- cmd /C ver + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows:1809 -- wmic os get caption + racker --verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows:1809 -- powershell -Command Get-ComputerInfo -Property WindowsProductName + + # Untested. + racker --verbose run -it --rm --platform=windows/amd64 mcr.microsoft.com/windows:20H2 wmic os get caption + + +Midnight Commander +================== + +Install and run `Midnight Commander`_:: + + racker --verbose run -it --rm \ + --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2022 -- \ + cmd /C 'choco install --yes --force mc --install-arguments=/tasks=modifypath & refreshenv & mc' + +.. figure:: https://user-images.githubusercontent.com/453543/173195789-9ef87618-5526-4317-99d7-b0dee6ca3970.png + :width: 800 + + +Python +====== + +Select a Windows container image including `Python`_ and launch it. + +Display Python version, launched within containers in different environments:: + + # Server Core + racker --verbose run --rm --platform=windows/amd64 python:2.7 -- python -V + racker --verbose run --rm --platform=windows/amd64 python:3.9 -- python -V + racker run --rm --platform=windows/amd64 winamd64/python:3.9-windowsservercore-1809 -- python -V + racker run --rm --platform=windows/amd64 winamd64/python:3.10-windowsservercore-ltsc2022 -- python -V + racker run --rm --platform=windows/amd64 winamd64/python:3.11-rc -- python -V + + # Explicitly select `2019-box` as different host OS. + # The default would be to automatically select `2022-box`. + RACKER_WDM_MACHINE=2019-box racker --verbose run --rm --platform=windows/amd64 winamd64/python:3.11-rc -- python -V + + # Nano Server + racker --verbose run --rm --platform=windows/amd64 stefanscherer/python-windows:nano -- python -V + +Display the Zen of Python:: + + racker --verbose run --rm --platform=windows/amd64 python:3.9 -- 'python -c "import this"' + +Install NumPy and display its configuration:: + + racker --verbose run --rm --platform=windows/amd64 python:3.10 -- 'sh -c "pip install numpy; python -c \"import numpy; numpy.show_config()\""' + + +Java +==== + +Display Java version, launched within containers in different environments:: + + # Eclipse Temurin. + racker --verbose run --rm --platform=windows/amd64 eclipse-temurin:16-jdk -- java --version + racker --verbose run --rm --platform=windows/amd64 eclipse-temurin:18-jdk -- java --version + + # Oracle OpenJDK. + racker --verbose run --rm --platform=windows/amd64 openjdk:8 -- java -version + racker --verbose run --rm --platform=windows/amd64 openjdk:8-windowsservercore-ltsc2016 -- java -version + racker --verbose run --rm --platform=windows/amd64 openjdk:8-windowsservercore-1809 -- java -version + racker --verbose run --rm --platform=windows/amd64 openjdk:19 -- java --version + + # Explicitly select `2019-box` as different host OS. + # The default would be to automatically select `2022-box`. + RACKER_WDM_MACHINE=2019-box racker --verbose run --rm --platform=windows/amd64 openjdk:19 -- java --version + + # Nano Server + racker --verbose run --rm --platform=windows/amd64 openjdk:19-nanoserver -- java --version + + +Invoke a Java command prompt (JShell) with different Java and OS versions:: + + racker --verbose run -it --rm --platform=windows/amd64 eclipse-temurin:18-jdk jshell + racker --verbose run -it --rm --platform=windows/amd64 openjdk:8-windowsservercore-ltsc2016 jshell + racker --verbose run -it --rm --platform=windows/amd64 openjdk:8-windowsservercore-1809 jshell + racker --verbose run -it --rm --platform=windows/amd64 openjdk:19-windowsservercore-ltsc2022 jshell + System.out.println("OS: " + System.getProperty("os.name") + ", version " + System.getProperty("os.version")) + System.out.println("Java: " + System.getProperty("java.vendor") + ", version " + System.getProperty("java.version")) + /exit + + + +****************** +Container handbook +****************** + +Inquire system information +========================== + +On systems where ``wmic`` is installed:: + + docker --context=2019-box run -it --rm mcr.microsoft.com/windows/servercore:ltsc2019 cmd wmic cpu get NumberOfCores wmic computersystem get TotalPhysicalMemory -:: +On systems where PowerShell is installed:: - docker --context=2019-box run -it --rm openjdk:17-windowsservercore-1809 powershell + docker --context=2019-box run -it --rm mcr.microsoft.com/windows/servercore:ltsc2019 powershell Get-ComputerInfo -Terminate an environment:: - docker --context=2019-box ps - docker --context=2019-box stop 5e2fe406ccbc +Manipulating ``PATH`` +===================== + +Display the content of the ``PATH`` environment variable:: + + echo %PATH% + (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -Name Path).Path + +Set the content of the ``PATH`` environment variable:: + + # Using `setx`. + setx PATH "$env:path;$($env:SystemDrive)\Program Files\Git\bin" -m + + # Using PowerShell. + [Environment]::SetEnvironmentVariable('Path', $env:Path + ';' + $($env:SystemDrive) + '\Program Files\Git\bin', 'Machine') + + + +*********** +Admin guide +*********** + + +Terminate a container +===================== + +You will experience situations where the invocation of programs will block your +terminal and you can't terminate the process using ``CTRL+C``. For example, try +to run ``wish.exe``. + +In such situations, you might want to kill the container. It works like this:: + + # Find the container id. + docker --context=2022-box ps + + # Terminate or stop the container. + docker --context=2022-box kill 08df5fc812f9 + docker --context=2022-box stop 08df5fc812f9 + + +The Docker contexts +=================== + +Communication from the Docker CLI to the Docker daemons running on the WDM +machines is established through Docker contexts. + +To list all active contexts, type:: + + docker context list + +To remove the contexts automatically established by WDM, type:: + + docker context rm 2016-box 2019-box 2022-box + + +Installing and using Chocolatey +=============================== + +The `Chocolatey`_ package manager can be used to install additional software like +``git`` and ``bash``:: + + racker run -it --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2019 powershell + Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) + + choco install --yes git --package-parameters="/GitAndUnixToolsOnPath /Editor:Nano" + iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/badrelmers/RefrEnv/main/refrenv.ps1')) + + $ bash --version + $ git --version + +The whole software catalog can be inquired at `Chocolatey community packages`_. + +.. _Chocolatey: https://chocolatey.org/ +.. _Chocolatey community packages: https://community.chocolatey.org/packages +.. _eclipse-temurin: https://hub.docker.com/_/eclipse-temurin +.. _Midnight Commander: https://en.wikipedia.org/wiki/Midnight_Commander +.. _Overview of System Center release options: https://docs.microsoft.com/en-us/system-center/ltsc-and-sac-overview +.. _Python: https://www.python.org/ +.. _Scoop: https://scoop.sh/ +.. _Using Racker and Postroj for CrateDB CI: https://github.com/cicerops/racker/blob/main/doc/cratedb.rst +.. _Vagrant: https://www.vagrantup.com/ +.. _VirtualBox: https://www.virtualbox.org/ +.. _Windows Container Base Images: https://docs.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/container-base-images +.. _Windows container version compatibility: https://docs.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/version-compatibility +.. _Windows Docker Machine: https://github.com/StefanScherer/windows-docker-machine +.. _Winfetch: https://github.com/kiedtl/winfetch +.. _Wheels for Windows: https://github.com/PyTables/PyTables/pull/872#issuecomment-773535041 diff --git a/postroj/cli.py b/postroj/cli.py index d6583c4..2558dfe 100644 --- a/postroj/cli.py +++ b/postroj/cli.py @@ -4,7 +4,7 @@ import click -from postroj import pkgprobe, runner, selftest, winrunner +from postroj import pkgprobe, selftest from postroj.api import pull_multiple_images, pull_single_image from postroj.registry import list_images from postroj.util import boot @@ -48,6 +48,5 @@ def cli_pull(ctx: click.Context, name: str, pull_all: bool = False): cli.add_command(cmd=cli_list_images, name="list-images") cli.add_command(cmd=cli_pull, name="pull") -cli.add_command(cmd=runner.invoke, name="invoke") cli.add_command(cmd=pkgprobe.main, name="pkgprobe") cli.add_command(cmd=selftest.selftest_main, name="selftest") diff --git a/postroj/exceptions.py b/postroj/exceptions.py index 20a7a2b..bfbd69b 100644 --- a/postroj/exceptions.py +++ b/postroj/exceptions.py @@ -11,7 +11,7 @@ class ProvisioningError(Exception): class InvalidImageReference(Exception): - pass + returncode = 1 class InvalidPhysicalImage(Exception): diff --git a/postroj/runner.py b/postroj/runner.py deleted file mode 100644 index aacb60c..0000000 --- a/postroj/runner.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# (c) 2022 Andreas Motl -import logging -import sys - -import click - -from postroj.winrunner import WinRunner -from racker.cli import racker_run - -logger = logging.getLogger(__name__) - - -@click.command() -@click.option("--system", type=str) -@click.option("--cpus", type=int) -@click.option("--memory", type=str) -@click.option("--mount", type=str) -@click.argument("command", nargs=-1, type=str) -@click.pass_context -def invoke(ctx, system, cpus, memory, mount, command): - """ - Run a command within a designated system environment - """ - # TODO: Propagate and implement `cpus`, `memory` and `mount`. See - command = " ".join(command) - if system == "windows-1809": - runner = WinRunner() - runner.setup() - runner.start() - outcome = runner.run(command) - sys.stdout.write(outcome) - else: - raise NotImplementedError(f'Runtime system "{system}" not supported yet') - - -if __name__ == "__main__": - racker_run() diff --git a/postroj/winrunner.Dockerfile b/postroj/winrunner.Dockerfile new file mode 100644 index 0000000..6468d74 --- /dev/null +++ b/postroj/winrunner.Dockerfile @@ -0,0 +1,46 @@ +# Windows runner Dockerfile for Racker +# https://github.com/cicerops/racker +# +# Provision a Windows operating system image. +# +# - Install the Scoop package manager. +# - Install additional software using Scoop. +# - https://scoop.sh/ +# + +ARG BASE_IMAGE + +FROM ${BASE_IMAGE} + +# Restore the default Windows shell for correct batch processing. +SHELL ["cmd", "/S", "/C"] + +# Install the Scoop package manager. +# https://github.com/ScoopInstaller/Install#for-admin +RUN powershell -Command irm get.scoop.sh -outfile 'scoop-install.ps1'; .\scoop-install.ps1 -RunAsAdmin + +# Install/update Aria2 and Git first, to speed up downloads and have it up-to-date. +RUN scoop install aria2 git + +# Install essential and convenience programs. +RUN scoop install msys2 zip unzip + +# Make MSYS2 programs available on the program search path. +# Note: It is not fully installed. In order to complete it, run `msys2` once. +RUN powershell $msys_path = $(scoop prefix msys2); [Environment]::SetEnvironmentVariable('Path', $env:Path + ';' + $msys_path + '\usr\bin', 'Machine') + +# Display the program search path. +#RUN powershell echo (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -Name Path).Path + +# Rename Windows-native programs in favor of Scoop-installed/FOSS/GNU ones. +# An alternative would be to manipulate `$PATH`, but that is more tedious. +RUN sh -c 'test -f /c/Windows/system32/curl && mv /c/Windows/system32/curl /c/Windows/system32/curl-win' +RUN sh -c 'test -f /c/Windows/system32/convert && mv /c/Windows/system32/convert /c/Windows/system32/convert-ntfs' + +# TODO: With PowerShell 7, it is possible to remove the corresponding aliases. +#Remove-Alias -Name ls +#Remove-Alias -Name cat +#Remove-Alias -Name mv +#Remove-Alias -Name ps +#Remove-Alias -Name pwd +#Remove-Alias -Name rm diff --git a/postroj/winrunner.py b/postroj/winrunner.py index b4eb124..99584af 100644 --- a/postroj/winrunner.py +++ b/postroj/winrunner.py @@ -1,56 +1,180 @@ # -*- coding: utf-8 -*- # (c) 2022 Andreas Motl """ -A convenience wrapper around Windows Docker Machine. +A convenience wrapper around Windows Docker Machine by Stefan Scherer. https://github.com/StefanScherer/windows-docker-machine """ import json -import shlex +import logging +import os import subprocess +import tempfile from pathlib import Path from urllib.parse import urlparse -import click +import appdirs +import pkg_resources -from postroj.util import port_is_up +from postroj.exceptions import InvalidImageReference +from postroj.util import port_is_up, cmd, fix_tty, subprocess_get_error_message + +logger = logging.getLogger(__name__) class WinRunner: - BOX = "2019-box" + VAGRANT_PROVIDER = os.environ.get("RACKER_WDM_PROVIDER", "virtualbox") + VCPUS = os.environ.get("RACKER_WDM_VCPUS", 4) + MEMORY = os.environ.get("RACKER_WDM_MEMORY", 4096) + + def __init__(self, image: str): + self.image_base = image + self.image_real = "racker-runtime/" + self.image_base.replace("/", "-") - def __init__(self): - self.workdir = Path.home() / "postroj" - self.workdir.mkdir(exist_ok=True) + self.wdm_machine = None + self.choose_wdm_machine() + logger.info(f"Using WDM host machine {self.wdm_machine} for launching container image {self.image_base}") + + self.workdir = Path(appdirs.user_state_dir(appname="racker")) + self.workdir.mkdir(exist_ok=True, parents=True) self.wdmdir = self.workdir / "windows-docker-machine" def setup(self): + """ + Prepare virtual machine and adjust system resources. + """ if not self.wdmdir.exists(): - click.echo("Installing Windows Docker Machine") + logger.info(f"Installing Windows Docker Machine into {self.wdmdir}") command = f""" - cd {self.workdir} - git clone https://github.com/StefanScherer/windows-docker-machine + cd '{self.workdir}' + git clone https://github.com/cicerops/windows-docker-machine --branch racker cd windows-docker-machine - #ls -alF - sed -i 's/v.memory = 2048/v.memory = 8192/' Vagrantfile - sed -i 's/v.cpus = 2/v.cpus = 8/' Vagrantfile + sed -i 's/v.cpus = [0-9]\+/v.cpus = {self.VCPUS}/' Vagrantfile + sed -i 's/v.memory = [0-9]\+/v.memory = {self.MEMORY}/' Vagrantfile + sed -i 's/v.maxmemory = [0-9]\+/v.maxmemory = {self.MEMORY}/' Vagrantfile """ - run(command, shell=True) + hshell(command) else: - click.echo("Windows Docker Machine already installed") + logger.info(f"Windows Docker Machine already installed into {self.wdmdir}") - def start(self): + def choose_wdm_machine(self): + """ + Choose the right virtual machine based on the container image to launch. + """ + + image = self.image_base + if not image.startswith("docker://"): + image = f"docker://{image}" + + logger.info(f"Inquiring information about OCI image '{image}'") + + if "RACKER_WDM_MACHINE" in os.environ: + self.wdm_machine = os.environ["RACKER_WDM_MACHINE"] + return + + # TODO: Cache the response from `skopeo inspect` to avoid the 3-second speed bump. + # https://github.com/cicerops/racker/issues/6 + command = f"skopeo --override-os=windows inspect --config --raw {image}" + try: + process = cmd(command, capture=True) + except subprocess.CalledProcessError as ex: + message = subprocess_get_error_message(exception=ex) + message = f"Inquiring information about OCI image '{image}' failed. {message}" + logger.error(message) + exception = InvalidImageReference(message) + exception.returncode = ex.returncode + raise exception + + image_info = json.loads(process.stdout) + image_os_name = image_info["os"] + image_os_version = image_info["os.version"] + logger.info(f"Image inquiry said os={image_os_name}, version={image_os_version}") + + if image_os_name != "windows": + raise ValueError(f"Container image {image} is not Windows, but {image_os_name} instead") + + # https://docs.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/version-compatibility + # https://stefanscherer.github.io/windows-docker-workshop/#91 + os_version_box_map = { + "10.0.14393": "2016-box", + "10.0.16299": "2019-box", # openjdk:8-windowsservercore-1709 + "10.0.17134": "2019-box", + "10.0.17763": "2019-box", + "10.0.19042": "2022-box", + "10.0.20348": "2022-box", + } + + for os_version, machine in os_version_box_map.items(): + if image_os_version.startswith(os_version): + self.wdm_machine = machine + + if self.wdm_machine is None: + raise ValueError(f"Unable to choose WDM host machine for container image {image}, matching OS version {image_os_version}. " + f"Please report this error to https://github.com/cicerops/racker/issues/new.") + + def start(self, provision=False): + """ + Start the "Windows Docker Machine" virtual machine. + + - Launch a virtual machine using Vagrant. + - Connect to Docker daemon on virtual machine. + - Provision the operating system image with additional software. + """ if self.docker_context_online(): - click.echo("Docker context is online") + logger.info("Docker context is online") + else: + logger.info("Docker context is offline, starting VirtualBox VM with Vagrant") + # TODO: The `provision` option flag is not wired in any way yet. + # https://github.com/cicerops/racker/issues/7 + provision_option = "" + if provision: + provision_option = "--provision" + cmd(f"vagrant up --provider={self.VAGRANT_PROVIDER} {provision_option} {self.wdm_machine}", cwd=self.wdmdir, use_stderr=True) + + logger.info("Pinging Docker context") + if not self.docker_context_online(): + raise IOError(f"Unable to bring up Docker context {self.wdm_machine}") + + # Attention: This can run into 60 second timeouts. + # TODO: Use ``with stopit.ThreadingTimeout(timeout) as to_ctx_mgr``. + # TODO: Make timeout values configurable. + # https://github.com/moby/moby/blob/0e04b514fb/integration-cli/docker_cli_run_test.go + cmd(f"docker --context={self.wdm_machine} ps", capture=True) + + # Skip installing software using Chocolatey for specific Windows OS versions. + # - Windows Nanoserver does not have PowerShell. + # - Windows 2016 croaks like: + # `The command 'cmd /S /C choco install --yes ...' returned a non-zero code: 3221225785` + if "nanoserver" in self.image_base or self.wdm_machine == "2016-box": + self.image_real = self.image_base else: - click.echo("Docker context is offline, starting VirtualBox VM with Vagrant") - run(f"vagrant up --provider=virtualbox {self.BOX}", cwd=self.wdmdir) + self.provision_image() - click.echo("Pinging Docker context") - run("docker --context=2019-box ps") + def provision_image(self): + """ + Provide an operating system image by building a Docker image using `winrunner.Dockerfile`. + + - Provision a Windows operating system image with additional software. + - Automatically installs the open source version of the Chocolatey package manager. + - By default, it automatically installs some programs like `busybox`, `curl`, `git`, + `nano`, and `wget`. + """ + + logger.info(f"Provisioning Docker image for Windows environment based on {self.image_base}") + dockerfile = pkg_resources.resource_filename("postroj", "winrunner.Dockerfile") + tmpdir = tempfile.mkdtemp() + command = f"docker --context={self.wdm_machine} build --platform=windows/amd64 " \ + f"--file={dockerfile} --build-arg=BASE_IMAGE={self.image_base} --tag={self.image_real} {tmpdir}" + logger.debug(f"Running command: {command}") + try: + hcmd(command) + except subprocess.CalledProcessError: + raise + finally: + os.rmdir(tmpdir) def cmd(self, command): command = f"cmd /C {command}" @@ -60,45 +184,66 @@ def powershell(self, command): command = f"powershell -Command {command}" return self.run(command) - def run(self, command, strip_armor=True, translate_newlines=True): - click.echo(f"Running command: {command}") - command = f"docker --context={self.BOX} run -it --rm openjdk:17-windowsservercore-1809 {command}" - outcome = run(command) - if strip_armor: - """ - b'\x1b[2J\x1b[?25l\x1b[m\x1b[H\r\n\r\n...\r\n\x1b[H\x1b]0;C:\\Windows\\system32\\cmd.exe\x00\x07\x1b[?25h\x1b[?25lhello \r\n\x1b[?25h' - """ - prefix = "\x1b[?25h\x1b[?25l" - suffix = "\x1b[?25h" - cutoff_left = outcome.find(prefix) + len(prefix) - cutoff_right = outcome.rfind(suffix) - outcome = outcome[cutoff_left:cutoff_right] - if translate_newlines: - outcome = outcome.replace("\r\n", "\n") - # print(outcome.encode()) - return outcome + def run(self, command, interactive: bool = False, tty: bool = False): + logger.info(f"Running guest command: {command}") + + option_interactive = "" + if interactive or tty: + option_interactive = "-it" + + # TODO: Propagate ``--rm`` option appropriately. + # TODO: Propagate ``--volume`` option. + # option_volume = "--volume=C:/Users/amo/dev/cicerops-foss/sources/postroj:C:/racker" + # https://github.com/cicerops/racker/issues/8 + option_volume = "" + #option_volume = "--volume=C:/Users/amo/dev/cicerops-foss/sources/postroj:C:/racker" + #option_volume = "--volume=C:/Users/amo/dev/panodata/sources/apprise:C:/apprise" + #option_volume = "--volume=C:/Users/amo/dev/earthobservations/wetterdienst:C:/wetterdienst" + #option_volume = "--volume=C:/Users/amo/dev/crate/sources/crate:C:/crate" + command = f"docker --context={self.wdm_machine} run {option_interactive} --rm {option_volume} {self.image_real} {command}" + + # When an interactive prompt is requested, spawn a shell without further ado. + if interactive or tty: + ccmd(command, use_pty=True) + + # Otherwise, capture stdout and mangle its output. + else: + outcome = cmd(command) + return outcome def docker_context_online(self): """ - Test if a Docker context is online. + Test if the Docker context is online. """ - response = json.loads(run(f"docker context inspect {self.BOX}")) - address = urlparse(response[0]["Endpoints"]["docker"]["Host"]) + logger.info(f"Checking connectivity to Docker daemon in Windows context '{self.wdm_machine}'") + try: + response = cmd(f"docker context inspect {self.wdm_machine}", capture=True) + except: + logger.warning(f"Docker context {self.wdm_machine} not online or not created yet") + return False + + data = json.loads(response.stdout) + address = urlparse(data[0]["Endpoints"]["docker"]["Host"]) + + logger.info(f"Checking TCP connectivity to {address.hostname}:{address.port}") return port_is_up(address.hostname, address.port) -def run(command, shell=False, cwd=None): - """ - Generic routine to run command within container. - - STDERR will be displayed, STDOUT will be captured. - """ - # print(f"Running command: {command}") - # command = f""" - # systemd-run --machine={machine} --wait --quiet --pipe {command} - # """ - if shell: - output = subprocess.check_output(command, shell=shell, cwd=cwd) - else: - output = subprocess.check_output(shlex.split(command), cwd=cwd) - return output.decode() +def hcmd(command, cwd=None, use_stderr=True, silent=False): + logger.debug(f"Running command: {command}") + return cmd(command, cwd=cwd, use_stderr=use_stderr) + + +def hshell(command, cwd=None): + logger.debug(f"Running command: {command}") + #return cmd(command, cwd=cwd, passthrough=True).stdout + return subprocess.check_output(command, shell=True, cwd=cwd).decode() + + +def ccmd(command, use_pty=False, capture=False): + logger.debug(f"Running command: {command}") + p = cmd(command=command, use_pty=use_pty, capture=capture) + stdout = p.stdout + if use_pty: + fix_tty() + return stdout diff --git a/pyproject.toml b/pyproject.toml index b9fb4e7..1404b88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,6 @@ markers = [ [tool.coverage.run] omit = [ "testing/*", - "postroj/winrunner.py", ] [tool.coverage.report] diff --git a/racker/cli.py b/racker/cli.py index 5ae0c0d..d811c84 100644 --- a/racker/cli.py +++ b/racker/cli.py @@ -10,8 +10,9 @@ from postroj.api import pull_curated_image from postroj.container import PostrojContainer -from postroj.exceptions import ProvisioningError +from postroj.exceptions import ProvisioningError, InvalidImageReference from postroj.util import boot, subprocess_get_error_message +from postroj.winrunner import WinRunner from racker.image import ImageLibrary logger = logging.getLogger(__name__) @@ -47,10 +48,11 @@ def racker_pull(ctx, name: str): @click.option("--interactive", "-i", is_flag=True) @click.option("--tty", "-t", is_flag=True) @click.option("--rm", is_flag=True) +@click.option("--platform", type=str, required=False) @click.argument("image", type=str, required=False) @click.argument("command", nargs=-1, type=str) @click.pass_context -def racker_run(ctx, interactive: bool, tty: bool, rm: bool, image: str, command: str): +def racker_run(ctx, interactive: bool, tty: bool, rm: bool, platform: str, image: str, command: str): """ Spawn a container and run a command on it. Aims to be compatible with `docker run`. @@ -84,6 +86,45 @@ def racker_run(ctx, interactive: bool, tty: bool, rm: bool, image: str, command: if interactive or tty: use_pty = True + # Use a different subsystem for running Windows containers on Linux or + # macOS. + if platform is None: + pass + + elif platform == "windows/amd64": + logger.info(f"Preparing runtime environment for platform {platform} and image {image}") + try: + runner = WinRunner(image=image) + except InvalidImageReference as ex: + raise SystemExit(ex.returncode) + try: + with redirect_stdout(sys.stderr): + with redirect_stderr(sys.stderr): + runner.setup() + runner.start() + except subprocess.CalledProcessError as ex: + message = subprocess_get_error_message(exception=ex) + logger.critical(f"Launching container failed. {message}") + # subprocess_forward_stderr_stdout(exception=ex) + raise SystemExit(ex.returncode) + + logger.info(f"Invoking command '{command}' on {image}") + try: + runner.run(command, interactive=interactive, tty=tty) + except subprocess.CalledProcessError as ex: + message = subprocess_get_error_message(exception=ex) + logger.critical(f"Running command in Windows container on image {image} failed. {message}") + raise SystemExit(ex.returncode) + return + + else: + raise NotImplementedError(f'Runtime for platform "{platform}" not supported yet') + + if sys.platform != "linux": + raise NotImplementedError(f"Unable to launch Linux systems on non-Linux machines yet, " + f"please use the Vagrant setup") + + # Acquire filesystem image. # TODO: Add more advanced image registry, maybe using `docker-py`, # resolving image names from postroj-internal images, Docker Hub, # GHCR, etc. diff --git a/setup.py b/setup.py index 64668f0..ea9e1a1 100644 --- a/setup.py +++ b/setup.py @@ -76,6 +76,7 @@ ], }, install_requires=[ + "appdirs>=1,<2", "click>=7,<9", "furl>=2,<3", "subprocess-tee>=0.3,<1", diff --git a/testing/postroj/test_winrunner.py b/testing/postroj/test_winrunner.py new file mode 100644 index 0000000..afe378b --- /dev/null +++ b/testing/postroj/test_winrunner.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# (c) 2022 Andreas Motl +import pytest + +from postroj.winrunner import ccmd, hshell, hcmd + + +def test_hcmd_echo_newline(capfd): + process = hcmd("/bin/echo foo", use_stderr=False) + assert process.stdout is None + assert process.stderr is None + + result = capfd.readouterr() + assert result.out == "foo\n" + assert result.err == "" + + +def test_hcmd_echo_no_newline(capfd): + hcmd("/bin/echo -n foo", use_stderr=False) + result = capfd.readouterr() + assert result.out == "foo" + + +def test_hshell_echo_newline(): + output = hshell("/bin/echo foo") + assert output == "foo\n" + + +def test_hshell_echo_no_newline(): + output = hshell("/bin/echo -n foo") + assert output == "foo" + + +def test_ccmd_echo_newline(): + output = ccmd("/bin/echo foo", capture=True) + assert output == "foo\n" + + +def test_ccmd_echo_newline_pty(capfd): + output = ccmd("/bin/echo foo", capture=True, use_pty=True) + assert output is None + + result = capfd.readouterr() + assert result.out == "foo\n" + assert result.err == "" + + +@pytest.mark.xfail +def test_ccmd_echo_no_newline(): + output = ccmd("/bin/echo -n foo", capture=True) + assert output == "foo" + + +def test_ccmd_echo_no_newline_pty(capfd): + output = ccmd("/bin/echo -n foo", capture=True, use_pty=True) + assert output is None + + result = capfd.readouterr() + assert result.out == "foo" + assert result.err == "" diff --git a/testing/racker/test_run.py b/testing/racker/test_run.py index 4edb12c..0af6a6c 100644 --- a/testing/racker/test_run.py +++ b/testing/racker/test_run.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- # (c) 2022 Andreas Motl +import re import shlex import subprocess import sys from pathlib import Path +from unittest import mock import pytest from click._compat import strip_ansi @@ -12,6 +14,9 @@ from racker.cli import cli +# Currently, this test module effectively runs well on Linux, +# because it invokes machinery based on `systemd-nspawn`. + if sys.platform != "linux": pytest.skip("Skipping Linux-only tests", allow_module_level=True) @@ -104,6 +109,73 @@ def test_run_stdin_stdout(monkeypatch, capsys, delay): assert process.stdout == b"foo" +@pytest.mark.xfail(reason="Not working within Linux VM on macOS. Reason: " + "Cannot enable nested VT-x/AMD-V without nested-paging and unrestricted guest execution!") +def test_run_windows_valid_image(capfd, delay): + """ + Request a valid Windows container image. + + Note: This is currently not possible, because somehow VT-x does not work + well enough to support this scenario, at least not on macOS Catalina. + """ + runner = CliRunner() + + result = runner.invoke(cli, "run --rm --platform=windows/amd64 mcr.microsoft.com/windows/nanoserver:ltsc2022 -- cmd /C ver", catch_exceptions=False) + assert result.exit_code == 0 + + captured = capfd.readouterr() + assert "Microsoft Windows [Version 10.0.20348.707]" in captured.out + + +def test_run_windows_invalid_image(caplog, delay): + """ + Request an invalid Windows container image and make sure it croaks correctly. + """ + runner = CliRunner() + + result = runner.invoke(cli, "run --rm --platform=windows/amd64 images.example.org/foo/bar:special -- cmd /C ver", catch_exceptions=False) + assert result.exit_code == 1 + + assert re.match(".*Inquiring information about OCI image .+ failed.*", caplog.text) + assert re.match(".*Reason:.*Error parsing image name .* (error )?pinging (container|docker) registry images.example.org.*", caplog.text) + + +def test_run_windows_mocked_noninteractive(): + """ + Pretend to launch a Windows container, but don't. + Reason: The `WinRunner` machinery has been mocked completely. + """ + runner = CliRunner() + + with mock.patch("racker.cli.WinRunner") as winrunner: + result = runner.invoke(cli, "run --rm --platform=windows/amd64 images.example.org/foo/bar:special -- cmd /C ver", catch_exceptions=False) + assert result.exit_code == 0 + assert winrunner.mock_calls == [ + mock.call(image='images.example.org/foo/bar:special'), + mock.call().setup(), + mock.call().start(), + mock.call().run('cmd /C ver', interactive=False, tty=False), + ] + + +def test_run_windows_mocked_interactive(): + """ + Pretend to launch a Windows container, but don't. + Reason: The `WinRunner` machinery has been mocked completely. + """ + runner = CliRunner() + + with mock.patch("racker.cli.WinRunner") as winrunner: + result = runner.invoke(cli, "run -it --rm --platform=windows/amd64 images.example.org/foo/bar:special -- cmd /C ver", catch_exceptions=False) + assert result.exit_code == 0 + assert winrunner.mock_calls == [ + mock.call(image='images.example.org/foo/bar:special'), + mock.call().setup(), + mock.call().start(), + mock.call().run('cmd /C ver', interactive=True, tty=True), + ] + + # Unfortunately, this fails. """ def test_run_stdin_stdout_original(capfd): diff --git a/testing/racker/test_run_windows.py b/testing/racker/test_run_windows.py new file mode 100644 index 0000000..370f359 --- /dev/null +++ b/testing/racker/test_run_windows.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# (c) 2022 Andreas Motl +import os +import subprocess +import sys +from pathlib import Path +import shlex +import socket +from subprocess import CalledProcessError, CompletedProcess + +import pytest + + +# Currently, this test module effectively runs well on macOS. +# The following snippet skips invocation on both VirtualBox +# and GitHub Actions. + +if "rackerhost-debian11" in socket.gethostname(): + pytest.skip("Nested virtualization with VT-x fails within " + "VirtualBox environment on developer's macOS workstation", allow_module_level=True) + +if "GITHUB_ACTIONS" in os.environ: + pytest.skip("Installing the Vagrant filesystem image for Windows " + "takes too much disk space on GitHub Actions", allow_module_level=True) + + +def run_racker(command: str) -> subprocess.CompletedProcess: + program_path = Path(sys.argv[0]).parent + racker = program_path / "racker" + command = f"{racker} {command}" + process = subprocess.run(shlex.split(command), stdout=subprocess.PIPE) + process.check_returncode() + return process + + +def test_run_windows_cmd_success(): + """ + Launch a Windows Nanoserver container and invoke a `cmd` command. + """ + process = run_racker("--verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/nanoserver:1809-amd64 cmd /C 'echo Hello, world.'") + process.check_returncode() + assert process.stdout == b"Hello, world.\r\n" + + +def test_run_windows_cmd_failure(): + """ + Check exit code propagation of a failing `cmd` command. + """ + with pytest.raises(CalledProcessError) as ex: + run_racker("--verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/nanoserver:1809-amd64 cmd /C 'exit 66'") + process: CompletedProcess = ex.value + assert process.returncode == 66 + + +def test_run_windows_powershell_success(): + """ + Launch a Windows Server Core container and invoke a PowerShell command. + """ + process = run_racker("--verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2019-amd64 -- 'powershell -Command {echo \"Hello, world.\"}'") + process.check_returncode() + # FIXME: Why does `echo` get echoed here!? + assert process.stdout == b"echo Hello, world.\r\n" + + +def test_run_windows_powershell_failure(): + """ + Check exit code propagation of a failing PowerShell command. + """ + with pytest.raises(CalledProcessError) as ex: + run_racker("--verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2019-amd64 -- 'powershell -Command exit 66'") + process: CompletedProcess = ex.value + assert process.returncode == 66 + + +def test_run_windows_bash_success(): + """ + Launch a Windows Server Core container and invoke a Bash command. + """ + process = run_racker("--verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2019-amd64 -- 'sh -c \"echo Hello, world.\"'") + process.check_returncode() + assert process.stdout == b"Hello, world.\n" + + +def test_run_windows_bash_failure(): + """ + Check exit code propagation of a failing Bash command. + """ + with pytest.raises(CalledProcessError) as ex: + run_racker("--verbose run --rm --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2019-amd64 -- 'sh -c \"exit 66\"'") + process: CompletedProcess = ex.value + assert process.returncode == 66