diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..5ae47e3 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,25 @@ +FROM mcr.microsoft.com/vscode/devcontainers/base:alpine-3.12 +ENV USERNAME=vscode + + + +RUN apk add --no-cache git shellcheck vim wget curl shadow +RUN apk add --no-cache nodejs npm +RUN wget -qO- "https://github.com/hadolint/hadolint/releases/download/v1.17.3/hadolint-Linux-x86_64" > hadolint \ + && cp "hadolint" /usr/bin/ \ + && chmod +x /usr/bin/hadolint + + + +# hadolint ignore=DL3016 +RUN npm install -g bats +# hadolint ignore=DL3013 +RUN npm install -g markdownlint-cli + +USER $USERNAME + +RUN mkdir -p /home/$USERNAME/.vscode-server/extensions \ + /home/$USERNAME/.vscode-server-insiders/extensions \ + && chown -R $USERNAME \ + /home/$USERNAME/.vscode-server \ + /home/$USERNAME/.vscode-server-insiders diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..1b7290d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,20 @@ +{ + "name": "${localWorkspaceFolderBasename}", + "dockerFile": "./Dockerfile", + "remoteUser": "vscode", + "mounts": [ + "source=${localWorkspaceFolderBasename}-extensions,target=/home/vscode/.vscode-server/extensions,type=volume,consistency=cached", + // keep the trailing comma below so it can be used in tests + "source=${localWorkspaceFolderBasename}-extensions-insiders,target=/home/vscode/.vscode-server-insiders/extensions,type=volume,consistency=cached", + ], + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind", + "updateRemoteUserUID": true, + "settings": { + "terminal.integrated.shell.linux": "/bin/bash" + }, + "workspaceFolder": "/workspace", + "extensions": [ + "bpruitt-goddard.mermaid-markdown-syntax-highlighting", + "exiasr.hadolint", + ], +} \ No newline at end of file diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 0000000..13b3811 --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,11 @@ +name: 'Trigger: Push action' +on: [push] + +jobs: + shellcheck: + name: Shellcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9bcd451..8bdcbd4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.devcontainer \ No newline at end of file +# .devcontainer +.history \ No newline at end of file diff --git a/README.md b/README.md index 70d20ad..a18d7ac 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,15 @@ Example ![Example](example.gif) +## Usage + +Running the `.devcontainer.json` file in this repository can be done using the following command: +``` +.devcontainer.sh -v --docker-opts="-u root" +``` +The `--docker-opts` are necessary because by default the vscode image runs as vscode user (id 1000) + + ## How does it work? The script can be run in any workspace the contains the `.devcontainer` configuration folder. On start, it parses several different options from the `devcontainer.json` file, builds the dockerfile, and starts the container. The script mounts the current folder into the container into the `/workspaces/$currentfolder` path. Same as Visual Studio Code does. @@ -22,8 +31,13 @@ You can install the devcontainer script by either [downloading the script](https ``` sudo sh -c 'curl -s https://raw.githubusercontent.com/BorisWilhelms/devcontainer/main/devcontainer.sh > /usr/local/bin/devcontainer && chmod +x /usr/local/bin/devcontainer' ``` -### Prerequisites +## Prerequisites The script uses [jq](https://stedolan.github.io/jq/) to parse the `devcontainer.json`. Therefore it must be installed. +Also `GNU sed` needs to be installed + + + +## Development -## Known issues -- Since `jq` expects a valid JSON file, all possible JSON error (e.g. `,` without following properties) has to be corrected. The script will strip all comments (`//`) to make it more valid. \ No newline at end of file +### Run shellcheck +`git ls-files | grep '.sh' | xargs shellcheck` \ No newline at end of file diff --git a/devcontainer.sh b/devcontainer.sh index 101198e..e957cdc 100755 --- a/devcontainer.sh +++ b/devcontainer.sh @@ -4,27 +4,83 @@ if ! [ -x "$(command -v jq)" ]; then printf "\x1B[31m[ERROR] jq is not installed.\x1B[0m\n" exit 1 fi -OPTIND=1 -VERBOSE=0 -while getopts "v" opt; do - case ${opt} in - v ) VERBOSE=1 ;; - esac -done +if ! [ -x "$(command -v sed)" ]; then + printf "\x1B[31m[ERROR] sed is not installed (only GNU sed works).\x1B[0m\n" + exit 1 +fi + +if ! sed --version | grep "sed (GNU sed)" &>/dev/null; then + printf "\x1B[31m[ERROR] GNU sed is not installed.\x1B[0m\n" + exit 1 +fi + +VERBOSE=0 +DOCKER_OPTS="" debug() { if [ $VERBOSE == 1 ]; then - printf "\x1B[33m[DEBUG] ${1}\x1B[0m\n" + printf "\x1B[33m[DEBUG] %s\x1B[0m\n" "${1}" fi } -WORKSPACE=${1:-`pwd`} -CURRENT_DIR=${PWD##*/} +optspec=":v-:" +while getopts "$optspec" opt; do + case ${opt} in + v ) + val="${!OPTIND}" + VERBOSE=1 + ;; + -) + case "${OPTARG}" in + docker-opts) + val="${!OPTIND}"; OPTIND=$((OPTIND + 1)) + DOCKER_OPTS=$val + if [[ $VERBOSE = 1 ]]; then + debug "Setting DOCKER_OPTS $DOCKER_OPTS" + fi + ;; + docker-opts=*) + val=${OPTARG#*=} + opt=${OPTARG%=$val} + DOCKER_OPTS=$val + if [[ $VERBOSE = 1 ]]; then + debug "Setting DOCKER_OPTS $DOCKER_OPTS" + fi + debug "docker-opts=* optind $OPTIND" + ;; + + *) + if [ "$OPTERR" = 1 ]; then + echo "Unknown option --${OPTARG}" >&2 + exit 5 + fi + ;; + esac;; + *) + if [ "$OPTERR" = 1 ]; then + echo "Unknown option -${OPTARG}" >&2 + exit 5 + fi + ;; + esac +done + + +WORKSPACE="${*:$OPTIND:1}" +WORKSPACE="${WORKSPACE:-$PWD}" + +if [[ ! -d "$WORKSPACE" ]]; then + echo "Directory $WORKSPACE does not exist!" >&2 + exit 6 +fi + + echo "Using workspace ${WORKSPACE}" CONFIG_DIR=./.devcontainer debug "CONFIG_DIR: ${CONFIG_DIR}" + CONFIG_FILE=devcontainer.json debug "CONFIG_FILE: ${CONFIG_FILE}" if ! [ -e "$CONFIG_DIR/$CONFIG_FILE" ]; then @@ -32,38 +88,53 @@ if ! [ -e "$CONFIG_DIR/$CONFIG_FILE" ]; then exit fi -CONFIG=$(cat $CONFIG_DIR/$CONFIG_FILE | grep -v //) +CONFIG="$(cat "$CONFIG_DIR/$CONFIG_FILE")" + +# Replacing variables in the config file +localWorkspaceFolderBasename="$(basename "$(realpath "$CONFIG_DIR/..")")" +# shellcheck disable=SC2001 +CONFIG="$(echo "$CONFIG" | sed "s#\${localWorkspaceFolderBasename}#$localWorkspaceFolderBasename#g")" + +localWorkspaceFolder="$(dirname "$localWorkspaceFolderBasename")" +# shellcheck disable=SC2001 +CONFIG="$(echo "$CONFIG" | sed "s#\${localWorkspaceFolder}#$localWorkspaceFolder#g")" + +# Remove trailing comma's with sed +CONFIG=$(echo "$CONFIG" | grep -v // | sed -Ez 's#,([[:space:]]*[]}])#\1#gm') debug "CONFIG: \n${CONFIG}" -cd $CONFIG_DIR -DOCKER_FILE=$(echo $CONFIG | jq -r .dockerFile) +if [[ ! -d "$CONFIG_DIR" ]]; then + echo "Config dir '$CONFIG_DIR' does not exist!" >&2 + exit 7 +fi + +cd "$CONFIG_DIR" || return + +DOCKER_FILE=$(echo "$CONFIG" | jq -r .dockerFile) if [ "$DOCKER_FILE" == "null" ]; then - DOCKER_FILE=$(echo $CONFIG | jq -r .build.dockerfile) + DOCKER_FILE=$(echo "$CONFIG" | jq -r .build.dockerfile) fi -DOCKER_FILE=$(readlink -f $DOCKER_FILE) +DOCKER_FILE="$(readlink -f "$DOCKER_FILE")" debug "DOCKER_FILE: ${DOCKER_FILE}" -if ! [ -e $DOCKER_FILE ]; then +if ! [ -e "$DOCKER_FILE" ]; then echo "Can not find dockerfile ${DOCKER_FILE}" exit fi -REMOTE_USER=$(echo $CONFIG | jq -r .remoteUser) +REMOTE_USER="$(echo "$CONFIG" | jq -r .remoteUser)" debug "REMOTE_USER: ${REMOTE_USER}" -if ! [ "$REMOTE_USER" == "null" ]; then - REMOTE_USER="-u ${REMOTE_USER}" -fi -ARGS=$(echo $CONFIG | jq -r '.build.args | to_entries? | map("--build-arg \(.key)=\"\(.value)\"")? | join(" ")') +ARGS=$(echo "$CONFIG" | jq -r '.build.args | to_entries? | map("--build-arg \(.key)=\"\(.value)\"")? | join(" ")') debug "ARGS: ${ARGS}" -SHELL=$(echo $CONFIG | jq -r '.settings."terminal.integrated.shell.linux"') +SHELL=$(echo "$CONFIG" | jq -r '.settings."terminal.integrated.shell.linux"') debug "SHELL: ${SHELL}" -PORTS=$(echo $CONFIG | jq -r '.forwardPorts | map("-p \(.):\(.)")? | join(" ")') +PORTS=$(echo "$CONFIG" | jq -r '.forwardPorts | map("-p \(.):\(.)")? | join(" ")') debug "PORTS: ${PORTS}" -ENVS=$(echo $CONFIG | jq -r '.remoteEnv | to_entries? | map("-e \(.key)=\(.value)")? | join(" ")') +ENVS=$(echo "$CONFIG" | jq -r '.remoteEnv | to_entries? | map("-e \(.key)=\(.value)")? | join(" ")') debug "ENVS: ${ENVS}" WORK_DIR="/workspace" @@ -73,7 +144,36 @@ MOUNT="${MOUNT} --mount type=bind,source=${WORKSPACE},target=${WORK_DIR}" debug "MOUNT: ${MOUNT}" echo "Building and starting container" -DOCKER_IMAGE_HASH=$(docker build -f $DOCKER_FILE $ARGS .) -debug "DOCKER_IMAGE_HASH: ${DOCKER_IMAGE_HASH}" -docker run -it $REMOTE_USER $PORTS $ENVS $MOUNT -w $WORK_DIR $DOCKER_IMAGE_HASH $SHELL \ No newline at end of file +DOCKER_TAG=$(echo "$DOCKER_FILE" | md5sum - | awk '{ print $1 }') +# shellcheck disable=SC2086 +docker build -f "$DOCKER_FILE" -t "$DOCKER_TAG" $ARGS . +build_status=$? + +if [[ $build_status -ne 0 ]]; then + echo "Building docker image failed..." >&2 + exit 7 +fi + +debug "DOCKER_TAG: ${DOCKER_TAG}" + +set -e +PUID=$(id -u) +PGID=$(id -g) + +# shellcheck disable=SC2086 +docker run -it $DOCKER_OPTS $PORTS $ENVS $MOUNT -w "$WORK_DIR" "$DOCKER_TAG" "$SHELL" -c "\ +if [ '$REMOTE_USER' != '' ] && command -v usermod &>/dev/null; \ +then \ + sudo='' + if [ \"$(stat -f -c '%u' "$(which sudo)")\" = '0' ]; then + sudo=sudo + fi + \$sudo usermod -u $PUID $REMOTE_USER && \ + \$sudo groupmod -g $PGID $REMOTE_USER && \ + \$sudo passwd -d $REMOTE_USER && \ + \$sudo chown $REMOTE_USER:$REMOTE_USER -R ~$REMOTE_USER $WORK_DIR && \ + su $REMOTE_USER -s $SHELL; \ +else \ + $SHELL; \ +fi" \ No newline at end of file