Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -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",
],
}
11 changes: 11 additions & 0 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.devcontainer
# .devcontainer
.history
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.
### Run shellcheck
`git ls-files | grep '.sh' | xargs shellcheck`
154 changes: 127 additions & 27 deletions devcontainer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,66 +4,137 @@ 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
echo "Folder contains no devcontainer configuration"
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"
Expand All @@ -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
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 "\
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I run it like this: .devcontainer.sh -v --docker-opts="-u root"

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"