diff --git a/inc/command/class-command.php b/inc/command/class-command.php index 6c139ac..01f82ce 100644 --- a/inc/command/class-command.php +++ b/inc/command/class-command.php @@ -25,7 +25,7 @@ protected function configure() { $this->setName( 'dev-tools' ); $this->setDescription( 'Developer tools' ); $this->setDefinition( [ - new InputArgument( 'subcommand', InputArgument::REQUIRED, 'phpunit | codecept' ), + new InputArgument( 'subcommand', InputArgument::REQUIRED, 'phpunit | codecept | bootstrap' ), new InputOption( 'chassis', null, null, 'Run commands in the Local Chassis environment' ), new InputOption( 'path', 'p', InputArgument::OPTIONAL, 'Use a custom path for tests folder.', 'tests' ), new InputOption( 'output', 'o', InputArgument::OPTIONAL, 'Use a custom path for output folder.', '' ), @@ -50,6 +50,10 @@ protected function configure() { Use -o to specify the output folder. Use -a to continue executing all suites even if one fails. Use `--` to send arguments to Codeception. + +To Bootstrap configuration files: + bootstrap [--] [options] + is the type of config to bootstrap, eg: codespaces EOT ); } @@ -68,6 +72,8 @@ protected function execute( InputInterface $input, OutputInterface $output ) { return $this->phpunit( $input, $output ); case 'codecept': return $this->codecept( $input, $output ); + case 'bootstrap': + return $this->bootstrap( $input, $output ); default: throw new CommandNotFoundException( sprintf( 'Subcommand "%s" is not defined.', $subcommand ) ); @@ -445,6 +451,70 @@ protected function codecept( InputInterface $input, OutputInterface $output ) { return $return; } + /** + * Bootstraps configuration files. + * + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int + */ + protected function bootstrap( InputInterface $input, OutputInterface $output ) : int { + $configs = [ 'codespaces' ]; + + $options = $input->getArgument( 'options' ); + $subsubcommand = $options[0] ?? null; + + if ( empty( $subsubcommand ) ) { + $output->writeln( 'You need to select a configuration set to bootstrap/generate.' ); + $output->writeln( sprintf( 'Available configuration sets are: %s.', implode( ', ', $configs ) ) ); + $output->writeln( 'eg: composer dev-tools bootstrap codespaces' ); + + return 1; + } + + if ( ! in_array( $subsubcommand, $configs, true ) ) { + $output->writeln( sprintf( 'Could not find the target configuration set generator for "%s".', $subsubcommand ) ); + return 1; + } + + call_user_func( [ $this, 'bootstrap_' . $subsubcommand ], $input, $output ); + + return 0; + } + + /** + * Bootstraps Codespaces devcontainer configuration files. + * + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int + */ + protected function bootstrap_codespaces( InputInterface $input, OutputInterface $output ) : int { + $target_folder = getcwd() . '/.devcontainer'; + $template_folder = realpath( __DIR__ . '/../../templates/.devcontainer' ); + + if ( file_exists( $target_folder ) ) { + $output->writeln( 'Codespaces devcontainer configuration exists already at /.devcontainer, aborting.' ); + return 1; + } + + $base_command = sprintf( 'cp -r "%s" "%s" &> /dev/null', $template_folder, $target_folder ); + passthru( $base_command, $return_var ); + + if ( $return_var ) { + $output->writeln( 'Could not generate Codespaces devcontainer configuration, aborting.' ); + return 1; + } + + $output->writeln( 'Files have been copied to the root of your project in .devcontainer folder.' ); + $output->writeln( 'Feel free to edit the generated config files to install system packages or editor extensions as needed.' ); + $output->writeln( 'Once ready, visit https://github.com/codespaces/new to create a new Codespace for your project repo.' ); + + return 0; + } + /** * Run the passed command on either the local-server or local-chassis environment. * diff --git a/templates/.devcontainer/DockerFile b/templates/.devcontainer/DockerFile new file mode 100644 index 0000000..761fd5a --- /dev/null +++ b/templates/.devcontainer/DockerFile @@ -0,0 +1,23 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/main/containers/php/.devcontainer/base.Dockerfile + +# [Choice] PHP version: 8, 8.0, 7, 7.4, 7.3 +ARG PHP_VERSION="8.0" +FROM mcr.microsoft.com/vscode/devcontainers/php:${PHP_VERSION} + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 + +# Set up Docker +COPY library-scripts/docker-in-docker-debian.sh /tmp/library-scripts/ +RUN apt-get update && /bin/bash /tmp/library-scripts/docker-in-docker-debian.sh +ENTRYPOINT ["/usr/local/share/docker-init.sh"] +VOLUME [ "/var/lib/docker" ] +CMD ["sleep", "infinity"] \ No newline at end of file diff --git a/templates/.devcontainer/devcontainer.json b/templates/.devcontainer/devcontainer.json new file mode 100644 index 0000000..66747cd --- /dev/null +++ b/templates/.devcontainer/devcontainer.json @@ -0,0 +1,47 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.192.0/containers/php +{ + "name": "PHP", + "build": { + "dockerfile": "Dockerfile", + "args": { + "PHP_VERSION": "8", + "NODE_VERSION": "lts/*" + } + }, + "mounts": [ + "source=dind-var-lib-docker,target=/var/lib/docker,type=volume" + ], + "runArgs": [ + "--init", + "--privileged" + ], + "overrideCommand": false, + + // Set *default* container specific settings.json values on container create. + "settings": { + "php.validate.executablePath": "/usr/local/bin/php" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "felixfbecker.php-debug", + "bmewburn.vscode-intelephense-client", + "mrmlnc.vscode-apache" + ], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [ + 80, + 8080, + ], + "otherPortsAttributes": { + "onAutoForward": "ignore" + }, + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "sudo chmod a+x \"$(pwd)\" && sudo rm -rf /var/www/html && sudo ln -s \"$(pwd)\" /var/www/html" + + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} \ No newline at end of file diff --git a/templates/.devcontainer/library-scripts/docker-in-docker-debian.sh b/templates/.devcontainer/library-scripts/docker-in-docker-debian.sh new file mode 100644 index 0000000..5468055 --- /dev/null +++ b/templates/.devcontainer/library-scripts/docker-in-docker-debian.sh @@ -0,0 +1,231 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker-in-docker.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./docker-in-docker-debian.sh [enable non-root docker access flag] [non-root user] [use moby] + +ENABLE_NONROOT_DOCKER=${1:-"true"} +USERNAME=${2:-"automatic"} +USE_MOBY=${3:-"true"} +MICROSOFT_GPG_KEYS_URI="https://packages.microsoft.com/keys/microsoft.asc" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Install dependencies +check_packages apt-transport-https curl ca-certificates lxc pigz iptables gnupg2 + +# Swap to legacy iptables for compatibility +if type iptables-legacy > /dev/null 2>&1; then + update-alternatives --set iptables /usr/sbin/iptables-legacy + update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy +fi + +# Install Docker / Moby CLI if not already installed +if type docker > /dev/null 2>&1 && type dockerd > /dev/null 2>&1; then + echo "Docker / Moby CLI and Engine already installed." +else + # Source /etc/os-release to get OS info + . /etc/os-release + if [ "${USE_MOBY}" = "true" ]; then + # Import key safely (new 'signed-by' method rather than deprecated apt-key approach) and install + get_common_setting MICROSOFT_GPG_KEYS_URI + curl -sSL ${MICROSOFT_GPG_KEYS_URI} | gpg --dearmor > /usr/share/keyrings/microsoft-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/repos/microsoft-${ID}-${VERSION_CODENAME}-prod ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/microsoft.list + apt-get update + apt-get -y install --no-install-recommends moby-cli moby-buildx moby-compose moby-engine + else + # Import key safely (new 'signed-by' method rather than deprecated apt-key approach) and install + curl -fsSL https://download.docker.com/linux/${ID}/gpg | gpg --dearmor > /usr/share/keyrings/docker-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list + apt-get update + apt-get -y install --no-install-recommends docker-ce-cli docker-ce + fi +fi + +echo "Finished installing docker / moby" + +# Install Docker Compose if not already installed and is on a supported architecture +if type docker-compose > /dev/null 2>&1; then + echo "Docker Compose already installed." +else + TARGET_COMPOSE_ARCH="$(uname -m)" + if [ "${TARGET_COMPOSE_ARCH}" = "amd64" ]; then + TARGET_COMPOSE_ARCH="x86_64" + fi + if [ "${TARGET_COMPOSE_ARCH}" != "x86_64" ]; then + # Use pip to get a version that runns on this architecture + if ! dpkg -s python3-minimal python3-pip libffi-dev python3-venv pipx > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install python3-minimal python3-pip libffi-dev python3-venv pipx + fi + export PIPX_HOME=/usr/local/pipx + mkdir -p ${PIPX_HOME} + export PIPX_BIN_DIR=/usr/local/bin + export PIP_CACHE_DIR=/tmp/pip-tmp/cache + pipx install --system-site-packages --pip-args '--no-cache-dir --force-reinstall' docker-compose + rm -rf /tmp/pip-tmp + else + LATEST_COMPOSE_VERSION=$(basename "$(curl -fsSL -o /dev/null -w "%{url_effective}" https://github.com/docker/compose/releases/latest)") + curl -fsSL "https://github.com/docker/compose/releases/download/${LATEST_COMPOSE_VERSION}/docker-compose-$(uname -s)-${TARGET_COMPOSE_ARCH}" -o /usr/local/bin/docker-compose + chmod +x /usr/local/bin/docker-compose + fi +fi + +# If init file already exists, exit +if [ -f "/usr/local/share/docker-init.sh" ]; then + echo "/usr/local/share/docker-init.sh already exists, so exiting." + exit 0 +fi +echo "docker-init doesnt exist..." + +# Add user to the docker group +if [ "${ENABLE_NONROOT_DOCKER}" = "true" ]; then + if ! getent group docker > /dev/null 2>&1; then + groupadd docker + fi + + usermod -aG docker ${USERNAME} +fi + +tee /usr/local/share/docker-init.sh > /dev/null \ +<< 'EOF' +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- + +sudoIf() +{ + if [ "$(id -u)" -ne 0 ]; then + sudo "$@" + else + "$@" + fi +} + +# explicitly remove dockerd and containerd PID file to ensure that it can start properly if it was stopped uncleanly +# ie: docker kill +sudoIf find /run /var/run -iname 'docker*.pid' -delete || : +sudoIf find /run /var/run -iname 'container*.pid' -delete || : + +set -e + +## Dind wrapper script from docker team +# Maintained: https://github.com/moby/moby/blob/master/hack/dind + +export container=docker + +if [ -d /sys/kernel/security ] && ! sudoIf mountpoint -q /sys/kernel/security; then + sudoIf mount -t securityfs none /sys/kernel/security || { + echo >&2 'Could not mount /sys/kernel/security.' + echo >&2 'AppArmor detection and --privileged mode might break.' + } +fi + +# Mount /tmp (conditionally) +if ! sudoIf mountpoint -q /tmp; then + sudoIf mount -t tmpfs none /tmp +fi + +# cgroup v2: enable nesting +if [ -f /sys/fs/cgroup/cgroup.controllers ]; then + # move the init process (PID 1) from the root group to the /init group, + # otherwise writing subtree_control fails with EBUSY. + sudoIf mkdir -p /sys/fs/cgroup/init + sudoIf echo 1 > /sys/fs/cgroup/init/cgroup.procs + # enable controllers + sudoIf sed -e 's/ / +/g' -e 's/^/+/' < /sys/fs/cgroup/cgroup.controllers \ + > /sys/fs/cgroup/cgroup.subtree_control +fi +## Dind wrapper over. + +# Handle DNS +set +e +cat /etc/resolv.conf | grep -i 'internal.cloudapp.net' +if [ $? -eq 0 ] +then + echo "Setting dockerd Azure DNS." + CUSTOMDNS="--dns 168.63.129.16" +else + echo "Not setting dockerd DNS manually." + CUSTOMDNS="" +fi +set -e + +# Start docker/moby engine +( sudoIf dockerd $CUSTOMDNS > /tmp/dockerd.log 2>&1 ) & + +set +e + +# Execute whatever commands were passed in (if any). This allows us +# to set this script to ENTRYPOINT while still executing the default CMD. +exec "$@" +EOF + +chmod +x /usr/local/share/docker-init.sh +chown ${USERNAME}:root /usr/local/share/docker-init.sh \ No newline at end of file