From 2e459ed7b56a9a609ed38741248a0cb73bf90196 Mon Sep 17 00:00:00 2001 From: Mandy Schoep Date: Sun, 31 Jan 2021 18:20:57 +0100 Subject: [PATCH 01/11] Added variable replacement --- devcontainer.sh | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/devcontainer.sh b/devcontainer.sh index 101198e..ded24a6 100755 --- a/devcontainer.sh +++ b/devcontainer.sh @@ -4,7 +4,17 @@ if ! [ -x "$(command -v jq)" ]; then printf "\x1B[31m[ERROR] jq is not installed.\x1B[0m\n" exit 1 fi -OPTIND=1 + +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 while getopts "v" opt; do @@ -32,12 +42,29 @@ 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 + if [ "$DOCKER_FILE" == "null" ]; then DOCKER_FILE=$(echo $CONFIG | jq -r .build.dockerfile) fi From b3cd3d9699f1ec635ce35af278741d345646061b Mon Sep 17 00:00:00 2001 From: Mandy Schoep Date: Sun, 31 Jan 2021 18:21:31 +0100 Subject: [PATCH 02/11] Added github actions for shellcheck --- .github/workflows/push.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/workflows/push.yml 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 From 051e4b62d9e4466094c77531cee6c154677e360e Mon Sep 17 00:00:00 2001 From: Mandy Schoep Date: Sun, 31 Jan 2021 18:23:19 +0100 Subject: [PATCH 03/11] Added example devcontainer.json Added github actions for shellcheck Made devcontainer.sh comply to shellcheck standards Added logic to devcontainer.sh to change user id and group id Added logic to replace the variables in devcontainer.json Added long option for docker run OPTIONS --- .devcontainer/Dockerfile | 25 ++++++++ .devcontainer/devcontainer.json | 20 ++++++ .gitignore | 3 +- README.md | 12 +++- devcontainer.sh | 108 +++++++++++++++++++++++++------- 5 files changed, 142 insertions(+), 26 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json 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/.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..fcbf704 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,16 @@ 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 need to be installed + ## 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 +- 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. + + +## Development + +### Run shellcheck +`git ls-files | grep '.sh' | xargs shellcheck` \ No newline at end of file diff --git a/devcontainer.sh b/devcontainer.sh index ded24a6..32bc685 100755 --- a/devcontainer.sh +++ b/devcontainer.sh @@ -16,25 +16,71 @@ if ! sed --version | grep "sed (GNU sed)" &>/dev/null; then fi VERBOSE=0 - -while getopts "v" opt; do - case ${opt} in - v ) VERBOSE=1 ;; - esac -done +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 @@ -65,32 +111,30 @@ 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" @@ -100,7 +144,25 @@ 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 . + +debug "DOCKER_TAG: ${DOCKER_TAG}" + +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 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 From f00866e2465dc951745769fda993b9e0ea131ce0 Mon Sep 17 00:00:00 2001 From: Mandy Schoep Date: Sun, 31 Jan 2021 18:25:36 +0100 Subject: [PATCH 04/11] README fix --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index fcbf704..d5cfb44 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,9 @@ sudo sh -c 'curl -s https://raw.githubusercontent.com/BorisWilhelms/devcontainer ``` ## Prerequisites The script uses [jq](https://stedolan.github.io/jq/) to parse the `devcontainer.json`. Therefore it must be installed. -Also GNU sed need to be installed +Also `GNU sed` needs to be installed -## 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. - ## Development From 734759bdb7509940a6d3a8df9f6a2940e3e17b56 Mon Sep 17 00:00:00 2001 From: Mandy Schoep Date: Tue, 2 Feb 2021 12:19:47 +0100 Subject: [PATCH 05/11] Do not continue when Docker build fails --- devcontainer.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/devcontainer.sh b/devcontainer.sh index 32bc685..2ba2946 100755 --- a/devcontainer.sh +++ b/devcontainer.sh @@ -148,6 +148,12 @@ echo "Building and starting container" 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}" From 999fecd1ab0f80c8d1a5a8f701d2c24e00856d67 Mon Sep 17 00:00:00 2001 From: Mandy Schoep Date: Tue, 2 Feb 2021 13:05:45 +0100 Subject: [PATCH 06/11] Check if sudo is owned by root because for some reason the "mcr.microsoft.com/vscode/devcontainers/base:alpine-3.12" image has it owned by 1000? --- devcontainer.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/devcontainer.sh b/devcontainer.sh index 2ba2946..041317a 100755 --- a/devcontainer.sh +++ b/devcontainer.sh @@ -164,10 +164,14 @@ PGID=$(id -g) 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 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 && \ + 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; \ From 74617ceba7f052dd0c4a3acd602909970283ee94 Mon Sep 17 00:00:00 2001 From: Mandy Schoep Date: Tue, 2 Feb 2021 13:10:30 +0100 Subject: [PATCH 07/11] Added usage instructions for `--docker-opts` --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index d5cfb44..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. From c77ed434f538ed7270e4729c3b6656c29e6c1c00 Mon Sep 17 00:00:00 2001 From: Mandy Schoep Date: Tue, 2 Feb 2021 13:11:47 +0100 Subject: [PATCH 08/11] Fix quoting --- devcontainer.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devcontainer.sh b/devcontainer.sh index 041317a..325d395 100755 --- a/devcontainer.sh +++ b/devcontainer.sh @@ -165,7 +165,7 @@ docker run -it $DOCKER_OPTS $PORTS $ENVS $MOUNT -w "$WORK_DIR" "$DOCKER_TAG" "$S if [[ '$REMOTE_USER' != '' ]] && command -v usermod &>/dev/null; \ then \ sudo='' - if [[ "$(stat -f -c '%u' $(which sudo))" = '0' ]]; then + if [[ \"$(stat -f -c '%u' $(which sudo))\" = '0' ]]; then sudo=sudo fi \$sudo usermod -u $PUID $REMOTE_USER && \ From b0fdc23792a4d6af6526de378134202a17edc19e Mon Sep 17 00:00:00 2001 From: Mandy Schoep Date: Tue, 2 Feb 2021 13:16:12 +0100 Subject: [PATCH 09/11] Fix for quoting --- devcontainer.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devcontainer.sh b/devcontainer.sh index 325d395..cb5ec26 100755 --- a/devcontainer.sh +++ b/devcontainer.sh @@ -165,7 +165,7 @@ docker run -it $DOCKER_OPTS $PORTS $ENVS $MOUNT -w "$WORK_DIR" "$DOCKER_TAG" "$S if [[ '$REMOTE_USER' != '' ]] && command -v usermod &>/dev/null; \ then \ sudo='' - if [[ \"$(stat -f -c '%u' $(which sudo))\" = '0' ]]; then + if [[ \"$(stat -f -c '%u' \"$(which sudo)\")\" = '0' ]]; then sudo=sudo fi \$sudo usermod -u $PUID $REMOTE_USER && \ From c1000037b1a47b540f59aa072db9b9428fb4bc54 Mon Sep 17 00:00:00 2001 From: Mandy Schoep Date: Tue, 2 Feb 2021 13:19:39 +0100 Subject: [PATCH 10/11] Fix for quoting --- devcontainer.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devcontainer.sh b/devcontainer.sh index cb5ec26..333c50e 100755 --- a/devcontainer.sh +++ b/devcontainer.sh @@ -165,7 +165,7 @@ docker run -it $DOCKER_OPTS $PORTS $ENVS $MOUNT -w "$WORK_DIR" "$DOCKER_TAG" "$S if [[ '$REMOTE_USER' != '' ]] && command -v usermod &>/dev/null; \ then \ sudo='' - if [[ \"$(stat -f -c '%u' \"$(which sudo)\")\" = '0' ]]; then + if [[ \"$(stat -f -c '%u' "$(which sudo)")\" = '0' ]]; then sudo=sudo fi \$sudo usermod -u $PUID $REMOTE_USER && \ From 0e955363df8a70c35406645f7e6ab00e732043d2 Mon Sep 17 00:00:00 2001 From: Mandy Schoep Date: Tue, 2 Feb 2021 13:22:24 +0100 Subject: [PATCH 11/11] Made chown/chmod part work on /bin/sh --- devcontainer.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/devcontainer.sh b/devcontainer.sh index 333c50e..e957cdc 100755 --- a/devcontainer.sh +++ b/devcontainer.sh @@ -157,15 +157,16 @@ 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; \ +if [ '$REMOTE_USER' != '' ] && command -v usermod &>/dev/null; \ then \ sudo='' - if [[ \"$(stat -f -c '%u' "$(which sudo)")\" = '0' ]]; then + if [ \"$(stat -f -c '%u' "$(which sudo)")\" = '0' ]; then sudo=sudo fi \$sudo usermod -u $PUID $REMOTE_USER && \