diff --git a/Dockerfile-alpine b/Dockerfile-alpine new file mode 100755 index 00000000..fdeca11c --- /dev/null +++ b/Dockerfile-alpine @@ -0,0 +1,61 @@ +# MIT License +# +# Copyright (c) 2019 Fabio Kruger +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +FROM openjdk:8-jdk-alpine +LABEL MAINTAINER="Fabio Kruger " + +ARG user=jenkins +ARG group=jenkins +ARG uid=1000 +ARG gid=1000 +ARG JENKINS_AGENT_HOME=/home/${user} + +ENV JENKINS_AGENT_HOME ${JENKINS_AGENT_HOME} + +RUN mkdir -p "${JENKINS_AGENT_HOME}" \ + && addgroup -g "${gid}" "${group}" \ +# Set the home directory (h), set user and group id (u, G), set the shell, don't ask for password (D) + && adduser -h "${JENKINS_AGENT_HOME}" -u "${uid}" -G "${group}" -s /bin/bash -D "${user}" \ +# Unblock user + && passwd -u "${user}" + +# setup SSH server +RUN apk update --no-cache \ + && apk add --no-cache \ + bash \ + openssh + +RUN sed -i /etc/ssh/sshd_config \ + -e 's/#PermitRootLogin.*/PermitRootLogin no/' \ + -e 's/#PasswordAuthentication.*/PasswordAuthentication no/' \ + -e 's/#SyslogFacility.*/SyslogFacility AUTH/' \ + -e 's/#LogLevel.*/LogLevel INFO/' \ + && mkdir /var/run/sshd + +VOLUME "${JENKINS_AGENT_HOME}" "/tmp" "/run" "/var/run" +WORKDIR "${JENKINS_AGENT_HOME}" + +COPY setup-sshd /usr/local/bin/setup-sshd + +EXPOSE 22 + +ENTRYPOINT ["setup-sshd"] diff --git a/Makefile b/Makefile index b4f03533..fb5b7c30 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,24 @@ ROOT:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) -IMAGE_NAME:=jenkins4eval/ssh-slave:test -build: - docker build -t ${IMAGE_NAME} . +IMAGE_NAME:=jenkins4eval/ssh-slave +IMAGE_ALPINE:=${IMAGE_NAME}:alpine +IMAGE_DEBIAN:=${IMAGE_NAME}:test + +build: build-alpine build-debian + +build-alpine: + docker build -t ${IMAGE_ALPINE} --file Dockerfile-alpine . + +build-debian: + docker build -t ${IMAGE_DEBIAN} --file Dockerfile . + +.PHONY: test +test: test-alpine test-debian + +.PHONY: test-alpine +test-alpine: + @FLAVOR=alpine bats tests/tests.bats + +.PHONY: test-debian +test-debian: + @bats tests/tests.bats diff --git a/setup-sshd b/setup-sshd index 18ccdba4..fad22126 100755 --- a/setup-sshd +++ b/setup-sshd @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -ex @@ -30,13 +30,20 @@ set -ex # docker run -e "JENKINS_SLAVE_SSH_PUBKEY=" jenkinsci/ssh-slave write_key() { - mkdir -p "${JENKINS_AGENT_HOME}/.ssh" - echo "$1" > "${JENKINS_AGENT_HOME}/.ssh/authorized_keys" - chown -Rf jenkins:jenkins "${JENKINS_AGENT_HOME}/.ssh" - chmod 0700 -R "${JENKINS_AGENT_HOME}/.ssh" + local ID_GROUP + + # As user, group, uid, gid and JENKINS_AGENT_HOME can be overridden at build, + # we need to find the values for JENKINS_AGENT_HOME + # ID_GROUP contains the user:group of JENKINS_AGENT_HOME directory + ID_GROUP=$(stat -c '%U:%G' "${JENKINS_AGENT_HOME}") + + mkdir -p "${JENKINS_AGENT_HOME}/.ssh" + echo "$1" > "${JENKINS_AGENT_HOME}/.ssh/authorized_keys" + chown -Rf "${ID_GROUP}" "${JENKINS_AGENT_HOME}/.ssh" + chmod 0700 -R "${JENKINS_AGENT_HOME}/.ssh" } -if [[ $JENKINS_SLAVE_SSH_PUBKEY == ssh-* ]]; then +if [[ ${JENKINS_SLAVE_SSH_PUBKEY} == ssh-* ]]; then write_key "${JENKINS_SLAVE_SSH_PUBKEY}" fi if [[ $# -gt 0 ]]; then @@ -52,5 +59,8 @@ fi # ensure variables passed to docker container are also exposed to ssh sessions env | grep _ >> /etc/environment +# generate host keys if not present ssh-keygen -A + +# do not detach (-D), log to stderr (-e), passthrough other arguments exec /usr/sbin/sshd -D -e "${@}" diff --git a/tests/keys.bash b/tests/keys.bash index d94de31d..20d871e6 100644 --- a/tests/keys.bash +++ b/tests/keys.bash @@ -28,4 +28,4 @@ uiWcmBF4XtMTVXUGcS6DCm/jf/4JDI8B1eJCVQKLbZXZbENWnptDtj098NTt9NdV TUwLP4n7pK4J2sCIs6fRD5kEYms4BnddXeRuI2fGZHGH70Ci/Q== -----END RSA PRIVATE KEY----- EOF -) \ No newline at end of file +) diff --git a/tests/test_helpers.bash b/tests/test_helpers.bash old mode 100644 new mode 100755 index 56250831..1e6e933d --- a/tests/test_helpers.bash +++ b/tests/test_helpers.bash @@ -1,5 +1,6 @@ -#!/bin/bash -exu +#!/usr/bin/env bash +set -eu # check dependencies ( @@ -13,8 +14,8 @@ function assert { local expected_output=$1 shift actual_output=$("$@") - if ! [ "$actual_output" = "$expected_output" ]; then - echo "expected: \"$expected_output\", actual: \"$actual_output\"" + if ! [[ "$actual_output" = "$expected_output" ]]; then + echo "expected: '$expected_output', actual: '$actual_output'" false fi } @@ -29,17 +30,52 @@ function retry { for ((i=0; i < attempts; i++)); do run "$@" - if [ "$status" -eq 0 ]; then + if [[ "$status" -eq 0 ]]; then return 0 fi - sleep $delay + sleep "$delay" done - echo "Command \"$@\" failed $attempts times. Status: $status. Output: $output" + echo "Command '$*' failed $attempts times. Status: $status. Output: $output" false } # return the published port for given container port $1 function get_port { - docker port $SUT_CONTAINER $1 | cut -d: -f2 + docker port "${SUT_CONTAINER}" "$1" | cut -d: -f2 +} + +# run a given command through ssh on the test container. +# Use the $status, $output and $lines variables to make assertions +function run_through_ssh { + SSH_PORT=$(get_port 22) + if [[ "${SSH_PORT}" = "" ]]; then + echo "failed to get SSH port" + false + else + TMP_PRIV_KEY_FILE=$(mktemp "${BATS_TMPDIR}"/bats_private_ssh_key_XXXXXXX) + echo "${PRIVATE_SSH_KEY}" > "${TMP_PRIV_KEY_FILE}" \ + && chmod 0600 "${TMP_PRIV_KEY_FILE}" + + run ssh -i "${TMP_PRIV_KEY_FILE}" \ + -o LogLevel=quiet \ + -o UserKnownHostsFile=/dev/null \ + -o StrictHostKeyChecking=no \ + -l jenkins \ + localhost \ + -p "${SSH_PORT}" \ + "$@" + + rm -f "${TMP_PRIV_KEY_FILE}" + fi +} + +function clean_test_container { + docker kill "${SUT_CONTAINER}" &>/dev/null ||: + docker rm -fv "${SUT_CONTAINER}" &>/dev/null ||: +} + +function is_slave_container_running { + sleep 1 # give time to sshd to eventually fail to initialize + retry 3 1 assert "true" docker inspect -f '{{.State.Running}}' "${SUT_CONTAINER}" } diff --git a/tests/tests.bats b/tests/tests.bats old mode 100644 new mode 100755 index 9e8aa01f..336a8ba8 --- a/tests/tests.bats +++ b/tests/tests.bats @@ -1,107 +1,112 @@ #!/usr/bin/env bats +DOCKERFILE=Dockerfile SUT_IMAGE=jenkins-ssh-slave SUT_CONTAINER=bats-jenkins-ssh-slave +if [[ -z "${FLAVOR}" ]] +then + FLAVOR="debian" +else + DOCKERFILE+="-alpine" + SUT_IMAGE+=":alpine" + SUT_CONTAINER+="-alpine" +fi + load test_helpers load keys -@test "build image" { - cd "${BATS_TEST_DIRNAME}"/.. || false - docker build -t "${SUT_IMAGE}" . -} +clean_test_container -@test "checking image metadatas" { - local VOLUMES_MAP="$(docker inspect -f '{{.Config.Volumes}}' ${SUT_IMAGE})" - echo "${VOLUMES_MAP}" | grep '/tmp' - echo "${VOLUMES_MAP}" | grep '/home/jenkins' - echo "${VOLUMES_MAP}" | grep '/run' - echo "${VOLUMES_MAP}" | grep '/var/run' +@test "[${FLAVOR}] build image" { + cd "${BATS_TEST_DIRNAME}"/.. || false + docker build -t "${SUT_IMAGE}" -f "${DOCKERFILE}" . } -@test "clean test container" { - docker kill "${SUT_CONTAINER}" &>/dev/null ||: - docker rm -fv "${SUT_CONTAINER}" &>/dev/null ||: -} +@test "[${FLAVOR}] checking image metadata" { + local VOLUMES_MAP="$(docker inspect -f '{{.Config.Volumes}}' ${SUT_IMAGE})" -@test "create slave container" { - docker run -d --name "${SUT_CONTAINER}" -P $SUT_IMAGE "$PUBLIC_SSH_KEY" + echo "${VOLUMES_MAP}" | grep '/tmp' + echo "${VOLUMES_MAP}" | grep '/home/jenkins' + echo "${VOLUMES_MAP}" | grep '/run' + echo "${VOLUMES_MAP}" | grep '/var/run' } -@test "image has bash and java installed and in the PATH" { - docker exec "${SUT_CONTAINER}" which bash - docker exec "${SUT_CONTAINER}" bash --version - docker exec "${SUT_CONTAINER}" which java - docker exec "${SUT_CONTAINER}" java -version -} +@test "[${FLAVOR}] image has bash and java installed and in the PATH" { + docker run -d --name "${SUT_CONTAINER}" -P "${SUT_IMAGE}" "${PUBLIC_SSH_KEY}" + + docker exec "${SUT_CONTAINER}" which bash + docker exec "${SUT_CONTAINER}" bash --version + docker exec "${SUT_CONTAINER}" which java + docker exec "${SUT_CONTAINER}" java -version -@test "slave container is running" { - sleep 1 # give time to sshd to eventually fail to initialize - retry 3 1 assert "true" docker inspect -f {{.State.Running}} "${SUT_CONTAINER}" + clean_test_container } -@test "connection with ssh + private key" { - run_through_ssh echo f00 +@test "[${FLAVOR}] create slave container with pubkey as argument" { + docker run -d --name "${SUT_CONTAINER}" -P "${SUT_IMAGE}" "${PUBLIC_SSH_KEY}" - [ "$status" = "0" ] && [ "$output" = "f00" ] \ - || (\ - echo "status: $status"; \ - echo "output: $output"; \ - false \ - ) -} + is_slave_container_running -# run a given command through ssh on the test container. -# Use the $status, $output and $lines variables to make assertions -function run_through_ssh { - SSH_PORT=$(get_port 22) - if [ "$SSH_PORT" = "" ]; then - echo "failed to get SSH port" - false - else - TMP_PRIV_KEY_FILE=$(mktemp "$BATS_TMPDIR"/bats_private_ssh_key_XXXXXXX) - echo "$PRIVATE_SSH_KEY" > $TMP_PRIV_KEY_FILE \ - && chmod 0600 $TMP_PRIV_KEY_FILE - - run ssh -i $TMP_PRIV_KEY_FILE \ - -o LogLevel=quiet \ - -o UserKnownHostsFile=/dev/null \ - -o StrictHostKeyChecking=no \ - -l jenkins \ - localhost \ - -p $SSH_PORT \ - "$@" - - rm -f $TMP_PRIV_KEY_FILE - fi -} + run_through_ssh echo f00 -@test "clean test container" { - docker kill "${SUT_CONTAINER}" &>/dev/null ||: - docker rm -fv "${SUT_CONTAINER}" &>/dev/null ||: -} + [ "$status" = "0" ] && [ "$output" = "f00" ] \ + || (\ + echo "status: $status"; \ + echo "output: $output"; \ + false \ + ) -@test "create slave container with pubkey as environment variable" { - docker run -e "JENKINS_SLAVE_SSH_PUBKEY=$PUBLIC_SSH_KEY" -d --name "${SUT_CONTAINER}" -P $SUT_IMAGE + clean_test_container } -@test "slave container is running" { - sleep 1 # give time to sshd to eventually fail to initialize - retry 3 1 assert "true" docker inspect -f {{.State.Running}} "${SUT_CONTAINER}" -} +@test "[${FLAVOR}] create slave container with pubkey as environment variable" { + docker run -e "JENKINS_SLAVE_SSH_PUBKEY=${PUBLIC_SSH_KEY}" -d --name "${SUT_CONTAINER}" -P "${SUT_IMAGE}" + + is_slave_container_running + + run_through_ssh echo f00 -@test "connection with ssh + private key" { - run_through_ssh echo f00 + [ "$status" = "0" ] && [ "$output" = "f00" ] \ + || (\ + echo "status: $status"; \ + echo "output: $output"; \ + false \ + ) - [ "$status" = "0" ] && [ "$output" = "f00" ] \ - || (\ - echo "status: $status"; \ - echo "output: $output"; \ - false \ - ) + clean_test_container } -@test "clean test container" { - docker kill "${SUT_CONTAINER}" &>/dev/null ||: - docker rm -fv "${SUT_CONTAINER}" &>/dev/null ||: +@test "[${FLAVOR}] use build args correctly" { + cd "${BATS_TEST_DIRNAME}"/.. || false + + local TEST_USER=test-user + local TEST_GROUP=test-group + local TEST_UID=2000 + local TEST_GID=3000 + local TEST_JAH=/home/something + + docker build \ + --build-arg "user=${TEST_USER}" \ + --build-arg "group=${TEST_GROUP}" \ + --build-arg "uid=${TEST_UID}" \ + --build-arg "gid=${TEST_GID}" \ + --build-arg "JENKINS_AGENT_HOME=${TEST_JAH}" \ + -t "${SUT_IMAGE}" \ + -f "${DOCKERFILE}" . + + docker run -d --name "${SUT_CONTAINER}" -P "${SUT_IMAGE}" "${PUBLIC_SSH_KEY}" + + RESULT=$(docker exec "${SUT_CONTAINER}" sh -c "id -u -n ${TEST_USER}") + [ "${RESULT}" = "${TEST_USER}" ] + RESULT=$(docker exec "${SUT_CONTAINER}" sh -c "id -g -n ${TEST_USER}") + [ "${RESULT}" = "${TEST_GROUP}" ] + RESULT=$(docker exec "${SUT_CONTAINER}" sh -c "id -u ${TEST_USER}") + [ "${RESULT}" = "${TEST_UID}" ] + RESULT=$(docker exec "${SUT_CONTAINER}" sh -c "id -g ${TEST_USER}") + [ "${RESULT}" = "${TEST_GID}" ] + RESULT=$(docker exec "${SUT_CONTAINER}" sh -c 'stat -c "%U:%G" "${JENKINS_AGENT_HOME}"') + [ "${RESULT}" = "${TEST_USER}:${TEST_GROUP}" ] + + clean_test_container }