diff --git a/.github/workflows/dockerhubpush.yml b/.github/workflows/dockerhubpush.yml index 307bd46..ddc823b 100644 --- a/.github/workflows/dockerhubpush.yml +++ b/.github/workflows/dockerhubpush.yml @@ -13,6 +13,7 @@ env: DOTNET_CORE_IMAGE_NAME: octopuslabs/tentacle-dotnetcoreworker JAVA_IMAGE_NAME: octopuslabs/tentacle-javaworker NODEJS_IMAGE_NAME: octopuslabs/tentacle-nodejsworker + POLLING_MULTI_SERVER_IMAGE_NAME: octopuslabs/tentacle-multiserverpolling jobs: # Push image to GitHub Packages. @@ -141,4 +142,19 @@ jobs: docker push $K8S_IMAGE_NAME:$VERSION docker push $K8S_IMAGE_NAME:latest + - name: Build multi-server polling image + run: docker build ./tentacle-multiserverpolling --tag $POLLING_MULTI_SERVER_IMAGE_NAME + + - name: Push multi-server polling image to DockerHub + run: | + # Strip git ref prefix from version + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + # Strip "v" prefix from tag name + [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') + echo VERSION=$VERSION + + docker tag $POLLING_MULTI_SERVER_IMAGE_NAME $POLLING_MULTI_SERVER_IMAGE_NAME:latest + docker tag $POLLING_MULTI_SERVER_IMAGE_NAME $POLLING_MULTI_SERVER_IMAGE_NAME:$VERSION + docker push $POLLING_MULTI_SERVER_IMAGE_NAME:$VERSION + docker push $POLLING_MULTI_SERVER_IMAGE_NAME:latest diff --git a/tentacle-multiserverpolling/Dockerfile b/tentacle-multiserverpolling/Dockerfile new file mode 100644 index 0000000..4364715 --- /dev/null +++ b/tentacle-multiserverpolling/Dockerfile @@ -0,0 +1,46 @@ +FROM octopusdeploy/tentacle + +RUN apt-get update && \ + apt-get install -y curl sudo dos2unix jq && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +EXPOSE 10933 + +WORKDIR /tmp + +COPY docker/linux/install-scripts/* /install-scripts/ +RUN chmod +x /install-scripts/*.sh + +COPY docker/linux/scripts/* /scripts/ +RUN chmod +x /scripts/*.sh + +WORKDIR / + +# We know this won't reduce the image size at all. It's just to make the filesystem a little tidier. +RUN rm -rf /tmp/* + +ENV OCTOPUS_RUNNING_IN_CONTAINER=Y +ENV ACCEPT_EULA=N +ENV CustomPublicHostName="" +ENV DISABLE_DIND=N +ENV ListeningPort="" +ENV MachinePolicy="Default Machine Policy" +ENV PublicHostNameConfiguration="ComputerName" +ENV ServerApiKey="" +ENV ServerPassword="" +ENV ServerPort="" +ENV ServerUrl="" +ENV ServerUsername="" +ENV Space="Default" +ENV TargetEnvironment="" +ENV TargetName="" +ENV TargetRole="" +ENV TargetTenant="" +ENV TargetTenantTag="" +ENV TargetTenantedDeploymentParticipation="" +ENV TargetWorkerPool="" + +VOLUME /var/lib/docker + +CMD /scripts/configure-tentacle.sh && /scripts/run-tentacle.sh diff --git a/tentacle-multiserverpolling/install-scripts/install-docker.sh b/tentacle-multiserverpolling/install-scripts/install-docker.sh new file mode 100644 index 0000000..7ef8f81 --- /dev/null +++ b/tentacle-multiserverpolling/install-scripts/install-docker.sh @@ -0,0 +1,45 @@ +#!/bin/bash +set -eux + +# This script is adapted from https://github.com/docker-library/docker/blob/master/19.03/dind/Dockerfile + +apt-get update + +# https://github.com/docker/docker/blob/master/project/PACKAGERS.md#runtime-dependencies +apt-get install -y \ + btrfs-progs \ + e2fsprogs \ + iptables \ + openssl \ + uidmap \ + xfsprogs \ + xz-utils \ + pigz \ + dos2unix + +# set up subuid/subgid so that "--userns-remap=default" works out-of-the-box +addgroup --system dockremap +adduser --system --group dockremap +echo 'dockremap:165536:65536' >> /etc/subuid +echo 'dockremap:165536:65536' >> /etc/subgid + +# https://github.com/docker/docker/tree/master/hack/dind +export DIND_COMMIT=37498f009d8bf25fbb6199e8ccd34bed84f2874b + +curl -o /usr/local/bin/dind "https://raw.githubusercontent.com/docker/docker/${DIND_COMMIT}/hack/dind" +chmod +x /usr/local/bin/dind +dos2unix /usr/local/bin/dind + +chmod +x /usr/local/bin/dockerd-entrypoint.sh +dos2unix /usr/local/bin/dockerd-entrypoint.sh + +export VERSION=19 +curl -sSL https://get.docker.com/ | sh + +# https://forums.docker.com/t/failing-to-start-dockerd-failed-to-create-nat-chain-docker/78269 +update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy +update-alternatives --set iptables /usr/sbin/iptables-legacy + +# Remove the apt cache +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/tentacle-multiserverpolling/scripts/configure-tentacle.sh b/tentacle-multiserverpolling/scripts/configure-tentacle.sh new file mode 100644 index 0000000..9a9bb3b --- /dev/null +++ b/tentacle-multiserverpolling/scripts/configure-tentacle.sh @@ -0,0 +1,264 @@ +#!/bin/bash +set -e + +if [[ "$ACCEPT_EULA" != "Y" ]]; then + echo "ERROR: You must accept the EULA at https://octopus.com/company/legal by passing an environment variable 'ACCEPT_EULA=Y'" + exit 1 +fi + +# Tentacle Docker images only support once instance per container. Running multiple instances can be achieved by running multiple containers. +instanceName=Tentacle +configurationDirectory=/etc/octopus +applicationsDirectory=/home/Octopus/Applications +alreadyConfiguredSemaphore="$configurationDirectory/.configuredSemaphore" +internalListeningPort=10933 + +mkdir -p $configurationDirectory +mkdir -p $applicationsDirectory + +if [ ! -f /usr/bin/tentacle ]; then + ln -s /opt/octopus/tentacle/Tentacle /usr/bin/tentacle +fi + +if [ -f "$alreadyConfiguredSemaphore" ]; then + echo "Octopus Tentacle is already configured. Skipping reconfiguration." + echo "If you want to force reconfiguration, please delete the file $alreadyConfiguredSemaphore and re-launch the container." + exit 0 +fi + +function getPublicHostName() { + if [[ "$PublicHostNameConfiguration" == "PublicIp" ]]; then + curl https://api.ipify.org/ + elif [[ "$PublicHostNameConfiguration" == "FQDN" ]]; then + hostname --fqdn + elif [[ "$PublicHostNameConfiguration" == "ComputerName" ]]; then + hostname + elif [[ "$PublicHostNameConfiguration" == "CustomInternal" ]]; then + hostname -i + else + echo $CustomPublicHostName + fi +} + +function validateVariables() { + if [[ -z "$ServerApiKey" ]]; then + if [[ -z "$ServerPassword" || -z "$ServerUsername" ]]; then + echo "Please specify either an API key or a username/password with the 'ServerApiKey' or 'ServerUsername'/'ServerPassword' environment variables" >&2 + exit 1 + fi + fi + + if [[ -z "$ServerUrl" ]]; then + echo "Please specify an Octopus Server with the 'ServerUrl' environment variable" >&2 + exit 1 + fi + + if [[ ! -z "$TargetWorkerPool" ]]; then + if [[ ! -z "$TargetEnvironment" ]]; then + echo "The 'TargetEnvironment' environment variable is not valid in combination with the 'TargetWorkerPool' variable" >&2 + exit 1 + fi + + if [[ ! -z "$TargetRole" ]]; then + echo "The 'TargetRole' environment variable is not valid in combination with the 'TargetWorkerPool' variable" >&2 + exit 1 + fi + else + if [[ -z "$TargetEnvironment" ]]; then + echo "Please specify an environment name with the 'TargetEnvironment' environment variable" >&2 + exit 1 + fi + + if [[ -z "$TargetRole" ]]; then + echo "Please specify a role name with the 'TargetRole' environment variable" >&2 + exit 1 + fi + fi + + echo " - server endpoint '$ServerUrl'" + echo " - api key '##########'" + if [[ ! -z "$ServerPort" ]]; then + echo " - communication mode 'Polling' (Active)" + echo " - server port $ServerPort" + else + echo " - communication mode 'Listening' (Passive)" + echo " - registered port $ListeningPort" + fi + if [[ ! -z "$TargetWorkerPool" ]]; then + echo " - worker pool '$TargetWorkerPool'" + else + echo " - environment '$TargetEnvironment'" + echo " - role '$TargetRole'" + fi + echo " - host '$PublicHostNameConfiguration'" + if [[ ! -z "$TargetName" ]]; then + echo " - name '$TargetName'" + fi + if [[ ! -z "$TargetTenant" ]]; then + echo " - tenant '$TargetTenant'" + fi + if [[ ! -z "$TargetTenantTag" ]]; then + echo " - tenant tag '$TargetTenantTag'" + fi + if [[ ! -z "$TargetTenantedDeploymentParticipation" ]]; then + echo " - tenanted deployment participation '$TargetTenantedDeploymentParticipation'" + fi + if [[ ! -z "$Space" ]]; then + echo " - space '$Space'" + fi +} + +function configureTentacle() { + tentacle create-instance --instance "$instanceName" --config "$configurationDirectory/tentacle.config" + + echo "Setting directory paths ..." + tentacle configure --instance "$instanceName" --app "$applicationsDirectory" + + echo "Configuring communication type ..." + if [[ ! -z "$ServerPort" ]]; then + tentacle configure --instance "$instanceName" --noListen "True" + else + tentacle configure --instance "$instanceName" --port $internalListeningPort --noListen "False" + fi + + echo "Updating trust ..." + tentacle configure --instance "$instanceName" --reset-trust + + echo "Creating certificate ..." + tentacle new-certificate --instance "$instanceName" --if-blank +} + +function registerTentacle() { + echo "Registering with server ..." + + local ARGS=() + + if [[ ! -z "$TargetWorkerPool" ]]; then + ARGS+=('register-worker') + + IFS=',' read -ra WORKER_POOLS <<<"$TargetWorkerPool" + for i in "${WORKER_POOLS[@]}"; do + ARGS+=('--workerpool' "$i") + done + else + ARGS+=('register-with') + + if [[ ! -z "$TargetEnvironment" ]]; then + IFS=',' read -ra ENVIRONMENTS <<<"$TargetEnvironment" + for i in "${ENVIRONMENTS[@]}"; do + ARGS+=('--environment' "$i") + done + fi + + if [[ ! -z "$TargetRole" ]]; then + IFS=',' read -ra ROLES <<<"$TargetRole" + for i in "${ROLES[@]}"; do + ARGS+=('--role' "$i") + done + fi + + if [[ ! -z "$TargetTenant" ]]; then + IFS=',' read -ra TENANTS <<<"$TargetTenant" + for i in "${TENANTS[@]}"; do + ARGS+=('--tenant' "$i") + done + fi + + if [[ ! -z "$TargetTenantTag" ]]; then + IFS=',' read -ra TENANTTAGS <<<"$TargetTenantTag" + for i in "${TENANTTAGS[@]}"; do + ARGS+=('--tenanttag' "$i") + done + fi + fi + + ARGS+=( + '--instance' "$instanceName" + '--server' "$1" + '--space' "$Space" + '--policy' "$MachinePolicy" + '--force') + + if [[ ! -z "$ServerPort" ]]; then + ARGS+=( + '--comms-style' 'TentacleActive' + '--server-comms-port' $ServerPort) + else + ARGS+=( + '--comms-style' 'TentaclePassive' + '--publicHostName' $(getPublicHostName)) + + if [[ ! -z "$ListeningPort" && "$ListeningPort" != "$internalListeningPort" ]]; then + ARGS+=('--tentacle-comms-port' $ListeningPort) + fi + fi + + if [[ ! -z "$ServerApiKey" ]]; then + echo "Registering Tentacle with API key" + ARGS+=('--apiKey' $ServerApiKey) + else + echo "Registering Tentacle with username/password" + ARGS+=( + '--username' "$ServerUsername" + '--password' "$ServerPassword") + fi + + if [[ ! -z "$TargetName" ]]; then + ARGS+=('--name' "$TargetName") + fi + + if [[ ! -z "$TargetTenantedDeploymentParticipation" ]]; then + ARGS+=('--tenanted-deployment-participation' "$TargetTenantedDeploymentParticipation") + fi + + tentacle "${ARGS[@]}" +} + +function registerAdditionalInstances() { + + local ARGS=() + + ARGS+=( + 'poll-server' + '--instance' "$instanceName" + '--server' "$1" + ) + + if [[ ! -z "$ServerApiKey" ]]; then + echo "Registering Tentacle with API key" + ARGS+=('--apiKey' $ServerApiKey) + else + echo "Registering Tentacle with username/password" + ARGS+=( + '--username' "$ServerUsername" + '--password' "$ServerPassword") + fi + + tentacle "${ARGS[@]}" +} + +echo "===============================================" +echo "Configuring Octopus Deploy Tentacle" + +validateVariables + +echo "===============================================" + +if [[ ! -z "$ServerUrl" ]]; then + IFS=',' read -ra SERVERURLS <<<"$ServerUrl" + for i in "${SERVERURLS[@]}"; do + if [[ "${SERVERURLS[0]}" == $i ]]; then + echo "Configuring and registering tentacle with this server: $i" + configureTentacle + registerTentacle $i + else + echo "Registering additional server with this tentacle: $i" + registerAdditionalInstances $i + fi + done +fi + +touch $alreadyConfiguredSemaphore + +echo "Configuration successful." +echo "" diff --git a/tentacle-multiserverpolling/scripts/dockerd-entrypoint.sh b/tentacle-multiserverpolling/scripts/dockerd-entrypoint.sh new file mode 100644 index 0000000..9668ba3 --- /dev/null +++ b/tentacle-multiserverpolling/scripts/dockerd-entrypoint.sh @@ -0,0 +1,189 @@ +#!/bin/sh + +# Originally from https://raw.githubusercontent.com/docker-library/docker/master/19.03/dind/dockerd-entrypoint.sh + +set -eu + +_tls_ensure_private() { + local f="$1"; shift + [ -s "$f" ] || openssl genrsa -out "$f" 4096 +} +_tls_san() { + { + ip -oneline address | awk '{ gsub(/\/.+$/, "", $4); print "IP:" $4 }' + { + cat /etc/hostname + echo 'docker' + echo 'localhost' + hostname -f + hostname -s + } | sed 's/^/DNS:/' + [ -z "${DOCKER_TLS_SAN:-}" ] || echo "$DOCKER_TLS_SAN" + } | sort -u | xargs printf '%s,' | sed "s/,\$//" +} +_tls_generate_certs() { + local dir="$1"; shift + + # if ca/key.pem || !ca/cert.pem, generate CA public if necessary + # if ca/key.pem, generate server public + # if ca/key.pem, generate client public + # (regenerating public certs every startup to account for SAN/IP changes and/or expiration) + + # https://github.com/FiloSottile/mkcert/issues/174 + local certValidDays='825' + + if [ -s "$dir/ca/key.pem" ] || [ ! -s "$dir/ca/cert.pem" ]; then + # if we either have a CA private key or do *not* have a CA public key, then we should create/manage the CA + mkdir -p "$dir/ca" + _tls_ensure_private "$dir/ca/key.pem" + openssl req -new -key "$dir/ca/key.pem" \ + -out "$dir/ca/cert.pem" \ + -subj '/CN=docker:dind CA' -x509 -days "$certValidDays" + fi + + if [ -s "$dir/ca/key.pem" ]; then + # if we have a CA private key, we should create/manage a server key + mkdir -p "$dir/server" + _tls_ensure_private "$dir/server/key.pem" + openssl req -new -key "$dir/server/key.pem" \ + -out "$dir/server/csr.pem" \ + -subj '/CN=docker:dind server' + cat > "$dir/server/openssl.cnf" <<-EOF + [ x509_exts ] + subjectAltName = $(_tls_san) + EOF + openssl x509 -req \ + -in "$dir/server/csr.pem" \ + -CA "$dir/ca/cert.pem" \ + -CAkey "$dir/ca/key.pem" \ + -CAcreateserial \ + -out "$dir/server/cert.pem" \ + -days "$certValidDays" \ + -extfile "$dir/server/openssl.cnf" \ + -extensions x509_exts + cp "$dir/ca/cert.pem" "$dir/server/ca.pem" + openssl verify -CAfile "$dir/server/ca.pem" "$dir/server/cert.pem" + fi + + if [ -s "$dir/ca/key.pem" ]; then + # if we have a CA private key, we should create/manage a client key + mkdir -p "$dir/client" + _tls_ensure_private "$dir/client/key.pem" + chmod 0644 "$dir/client/key.pem" # openssl defaults to 0600 for the private key, but this one needs to be shared with arbitrary client contexts + openssl req -new \ + -key "$dir/client/key.pem" \ + -out "$dir/client/csr.pem" \ + -subj '/CN=docker:dind client' + cat > "$dir/client/openssl.cnf" <<-'EOF' + [ x509_exts ] + extendedKeyUsage = clientAuth + EOF + openssl x509 -req \ + -in "$dir/client/csr.pem" \ + -CA "$dir/ca/cert.pem" \ + -CAkey "$dir/ca/key.pem" \ + -CAcreateserial \ + -out "$dir/client/cert.pem" \ + -days "$certValidDays" \ + -extfile "$dir/client/openssl.cnf" \ + -extensions x509_exts + cp "$dir/ca/cert.pem" "$dir/client/ca.pem" + openssl verify -CAfile "$dir/client/ca.pem" "$dir/client/cert.pem" + fi +} + +# no arguments passed +# or first arg is `-f` or `--some-option` +if [ "$#" -eq 0 ] || [ "${1#-}" != "$1" ]; then + # set "dockerSocket" to the default "--host" *unix socket* value (for both standard or rootless) + uid="$(id -u)" + if [ "$uid" = '0' ]; then + dockerSocket='unix:///var/run/docker.sock' + else + # if we're not root, we must be trying to run rootless + : "${XDG_RUNTIME_DIR:=/run/user/$uid}" + dockerSocket="unix://$XDG_RUNTIME_DIR/docker.sock" + fi + case "${DOCKER_HOST:-}" in + unix://*) + dockerSocket="$DOCKER_HOST" + ;; + esac + + # add our default arguments + if [ -n "${DOCKER_TLS_CERTDIR:-}" ] \ + && _tls_generate_certs "$DOCKER_TLS_CERTDIR" \ + && [ -s "$DOCKER_TLS_CERTDIR/server/ca.pem" ] \ + && [ -s "$DOCKER_TLS_CERTDIR/server/cert.pem" ] \ + && [ -s "$DOCKER_TLS_CERTDIR/server/key.pem" ] \ + ; then + # generate certs and use TLS if requested/possible (default in 19.03+) + set -- dockerd \ + --host="$dockerSocket" \ + --host=tcp://0.0.0.0:2376 \ + --tlsverify \ + --tlscacert "$DOCKER_TLS_CERTDIR/server/ca.pem" \ + --tlscert "$DOCKER_TLS_CERTDIR/server/cert.pem" \ + --tlskey "$DOCKER_TLS_CERTDIR/server/key.pem" \ + "$@" + DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS="${DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS:-} -p 0.0.0.0:2376:2376/tcp" + else + # TLS disabled (-e DOCKER_TLS_CERTDIR='') or missing certs + set -- dockerd \ + --host="$dockerSocket" \ + --host=tcp://0.0.0.0:2375 \ + "$@" + DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS="${DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS:-} -p 0.0.0.0:2375:2375/tcp" + fi +fi + +if [ "$1" = 'dockerd' ]; then + # explicitly remove Docker's default PID file to ensure that it can start properly if it was stopped uncleanly (and thus didn't clean up the PID file) + find /run /var/run -iname 'docker*.pid' -delete || : + + uid="$(id -u)" + if [ "$uid" != '0' ]; then + # if we're not root, we must be trying to run rootless + if ! command -v rootlesskit > /dev/null; then + echo >&2 "error: attempting to run rootless dockerd but missing 'rootlesskit' (perhaps the 'docker:dind-rootless' image variant is intended?)" + exit 1 + fi + user="$(id -un 2>/dev/null || :)" + if ! grep -qE "^($uid${user:+|$user}):" /etc/subuid || ! grep -qE "^($uid${user:+|$user}):" /etc/subgid; then + echo >&2 "error: attempting to run rootless dockerd but missing necessary entries in /etc/subuid and/or /etc/subgid for $uid" + exit 1 + fi + : "${XDG_RUNTIME_DIR:=/run/user/$uid}" + export XDG_RUNTIME_DIR + if ! mkdir -p "$XDG_RUNTIME_DIR" || [ ! -w "$XDG_RUNTIME_DIR" ] || ! mkdir -p "$HOME/.local/share/docker" || [ ! -w "$HOME/.local/share/docker" ]; then + echo >&2 "error: attempting to run rootless dockerd but need writable HOME ($HOME) and XDG_RUNTIME_DIR ($XDG_RUNTIME_DIR) for user $uid" + exit 1 + fi + if [ -f /proc/sys/kernel/unprivileged_userns_clone ] && unprivClone="$(cat /proc/sys/kernel/unprivileged_userns_clone)" && [ "$unprivClone" != '1' ]; then + echo >&2 "error: attempting to run rootless dockerd but need 'kernel.unprivileged_userns_clone' (/proc/sys/kernel/unprivileged_userns_clone) set to 1" + exit 1 + fi + if [ -f /proc/sys/user/max_user_namespaces ] && maxUserns="$(cat /proc/sys/user/max_user_namespaces)" && [ "$maxUserns" = '0' ]; then + echo >&2 "error: attempting to run rootless dockerd but need 'user.max_user_namespaces' (/proc/sys/user/max_user_namespaces) set to a sufficiently large value" + exit 1 + fi + # TODO overlay support detection? + exec rootlesskit \ + --net="${DOCKERD_ROOTLESS_ROOTLESSKIT_NET:-vpnkit}" \ + --mtu="${DOCKERD_ROOTLESS_ROOTLESSKIT_MTU:-1500}" \ + --disable-host-loopback \ + --port-driver=builtin \ + --copy-up=/etc \ + --copy-up=/run \ + ${DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS:-} \ + "$@" + elif [ -x '/usr/local/bin/dind' ]; then + # if we have the (mostly defunct now) Docker-in-Docker wrapper script, use it + set -- '/usr/local/bin/dind' "$@" + fi +else + # if it isn't `dockerd` we're trying to run, pass it through `docker-entrypoint.sh` so it gets `DOCKER_HOST` set appropriately too + set -- docker-entrypoint.sh "$@" +fi + +exec "$@" diff --git a/tentacle-multiserverpolling/scripts/run-tentacle.sh b/tentacle-multiserverpolling/scripts/run-tentacle.sh new file mode 100644 index 0000000..d7935db --- /dev/null +++ b/tentacle-multiserverpolling/scripts/run-tentacle.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -eux + +if [[ "$DISABLE_DIND" == "Y" ]]; then + echo Docker-in-Docker is disabled. +else + echo "Starting Docker-in-Docker daemon. This requires that this container be run in privileged mode." + nohup /usr/local/bin/dockerd-entrypoint.sh dockerd & +fi + +tentacle agent --instance Tentacle --noninteractive