Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' for release 0.1.0-beta1
Browse files Browse the repository at this point in the history
  • Loading branch information
davidalger committed Jun 3, 2019
2 parents ea7ba75 + f4c9cb7 commit 4e763d2
Show file tree
Hide file tree
Showing 17 changed files with 556 additions and 2 deletions.
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,29 @@
# warden
Warden is a CLI utility for managing docker powered developer environments on macOS
# Warden
Warden is a CLI utility for working with docker-compose environments, and enables multiple local developer environments to run simultaneously without port conflicts via the use of a few centrally run services for proxying into each environment's containers.

## Prerequisites

* [Homebrew](https://brew.sh) package manager (for installing Warden)
* [Docker for Mac](https://hub.docker.com/editions/community/docker-ce-desktop-mac) or [Docker for Linux](https://docs.docker.com/install/linux/docker-ce/fedora/)
* `docker-compose` available in your `$PATH` (included in Docker for Mac, can be installed via brew on Linux hosts)

## Installing Warden

brew install davidalger/warden/warden
warden up

## Features

* Traefik for SSL termination and routing/proxying requests into the correct containers.
* Portainer for quick visibility into what's running inside the local Docker host.
* Dnsmasq to serve DNS responses for .test domains eliminating manual editing of `/etc/hosts`
* An SSH tunnel for connecting from SequelPro or TablePlus into any one of multiple running database containers.
* Warden wildcard SSl certificate signing for running https on all local development domains.

## License

This work is licensed under the MIT license. See LICENSE file for details.

## Author Information

This project was started in 2019 by [David Alger](https://davidalger.com/).
68 changes: 68 additions & 0 deletions bin/warden
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
set -e
trap '>&2 printf "\n\e[01;31mError: Command \`%s\` on line $LINENO failed with exit code $?\033[0m\n" "$BASH_COMMAND"' ERR

## find directory where this script is located following symlinks if neccessary
readonly WARDEN_DIR="$(
cd "$(
dirname "$(
(readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOURCE[0]}") \
| sed -e "s#^../#$(dirname "$(dirname "${BASH_SOURCE[0]}")")/#"
)"
)/.." >/dev/null \
&& pwd
)"

## define global directory paths by commands
readonly WARDEN_HOME_DIR=~/.warden
readonly WARDEN_SSL_DIR="${WARDEN_HOME_DIR}/ssl"

## declare variables for flags and arguments
declare WARDEN_HELP=
declare WARDEN_PARAMS=()
declare WARDEN_COMMAND=

## parse first argument as command and determine validity
if (( "$#" )) && [[ -f "${WARDEN_DIR}/commands/${1}.cmd" ]]; then
WARDEN_COMMAND="$1"
shift
else
WARDEN_COMMAND=usage
fi

## parse arguments
while (( "$#" )); do
case "$1" in
-h|--help)
WARDEN_HELP=1
break
;;
--) # end argument parsing
shift
break
;;
-*|--*=) # unsupported flags
>&2 echo "Error: Unsupported flag $1"
exit 1
;;
*) # preserve positional arguments
WARDEN_PARAMS+=($1)
shift
;;
esac
done

function assert_installed {
if [[ ! -f "${WARDEN_HOME_DIR}/.installed" ]]; then
"${WARDEN_DIR}/bin/warden" install
date > "${WARDEN_HOME_DIR}/.installed"
fi
}

## display command specific usage info if help flag is set
if [[ ${WARDEN_HELP} ]]; then
source "${WARDEN_DIR}/commands/usage.cmd"
fi

## execute sub-command in context of this script
source "${WARDEN_DIR}/commands/${WARDEN_COMMAND}.cmd"
5 changes: 5 additions & 0 deletions commands/down.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
[[ ! ${WARDEN_COMMAND} ]] && >&2 echo -e "\033[31mThis script is not intended to be run directly!" && exit 1

pushd "${WARDEN_DIR}" >/dev/null
docker-compose -p warden -f docker/docker-compose.yml down "${WARDEN_PARAMS[@]}" "$@"
70 changes: 70 additions & 0 deletions commands/install.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
[[ ! ${WARDEN_COMMAND} ]] && >&2 echo -e "\033[31mThis script is not intended to be run directly!" && exit 1

if [[ ! -d "${WARDEN_SSL_DIR}/rootca" ]]; then
mkdir -p "${WARDEN_SSL_DIR}/rootca"/{certs,crl,newcerts,private}

touch "${WARDEN_SSL_DIR}/rootca/index.txt"
echo 1000 > "${WARDEN_SSL_DIR}/rootca/serial"
fi

# create CA root certificate if none present
if [[ ! -f "${WARDEN_SSL_DIR}/rootca/private/ca.key.pem" ]]; then
echo "==> Generating private key for local root certificate"
openssl genrsa -out "${WARDEN_SSL_DIR}/rootca/private/ca.key.pem" 2048
fi

if [[ ! -f "${WARDEN_SSL_DIR}/rootca/certs/ca.cert.pem" ]]; then
echo "==> Signing root certificate (Warden Proxy Local CA)"
openssl req -new -x509 -days 7300 -sha256 -extensions v3_ca \
-config "${WARDEN_DIR}/config/openssl/rootca.conf" \
-key "${WARDEN_SSL_DIR}/rootca/private/ca.key.pem" \
-out "${WARDEN_SSL_DIR}/rootca/certs/ca.cert.pem" \
-subj "/C=US/O=Warden Proxy Local CA"
fi

## trust root ca differently on linux-gnu than on macOS
if [[ "$OSTYPE" == "linux-gnu" ]] && [[ ! -f /etc/pki/ca-trust/source/anchors/warden-proxy-local-ca.cert.pem ]]; then
echo "==> Trusting root certificate (requires sudo privileges)"
sudo cp "${WARDEN_SSL_DIR}/rootca/certs/ca.cert.pem" /etc/pki/ca-trust/source/anchors/warden-proxy-local-ca.cert.pem
sudo update-ca-trust
sudo update-ca-trust enable
elif [[ "$OSTYPE" == "darwin"* ]] && ! security dump-trust-settings -d | grep 'Warden Proxy Local CA' >/dev/null; then
echo "==> Trusting root certificate (requires sudo privileges)"
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain "${WARDEN_SSL_DIR}/rootca/certs/ca.cert.pem"
fi

if [[ ! -f "${WARDEN_SSL_DIR}/certs/warden.test.crt.pem" ]]; then
"${WARDEN_DIR}/bin/warden" sign-certificate warden.test
fi

## configure resolver for .test domains
if [[ ! -d /etc/resolver ]] || [[ ! -f /etc/resolver/test ]]; then
echo "==> Configuring resolver for .test domains (requires sudo privileges)"
sudo mkdir /etc/resolver
echo "nameserver 127.0.0.1" | sudo tee /etc/resolver/test >/dev/null
fi

## generate rsa keypair for authenticating to warden sshd service
if [[ ! -f "${WARDEN_HOME_DIR}/tunnel/ssh_key" ]]; then
echo "==> Generating rsa key pair for tunnel into sshd service"
mkdir -p "${WARDEN_HOME_DIR}/tunnel"
ssh-keygen -b 2048 -t rsa -f "${WARDEN_HOME_DIR}/tunnel/ssh_key" -N "" -C "[email protected]"
fi

if ! grep '## WARDEN START ##' /etc/ssh/ssh_config >/dev/null; then
echo "==> Configuring sshd tunnel in host ssh_config (requires sudo privileges)"
cat <<-EOF | sudo tee -a /etc/ssh/ssh_config >/dev/null
## WARDEN START ##
Host tunnel.warden.test
HostName 127.0.0.1
User user
Port 2222
IdentityFile ~/.warden/tunnel/ssh_key
## WARDEN END ##
EOF
fi

echo "==> Installed successfully"
5 changes: 5 additions & 0 deletions commands/restart.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
[[ ! ${WARDEN_COMMAND} ]] && >&2 echo -e "\033[31mThis script is not intended to be run directly!" && exit 1

pushd "${WARDEN_DIR}" >/dev/null
docker-compose -p warden -f docker/docker-compose.yml restart "${WARDEN_PARAMS[@]}" "$@"
57 changes: 57 additions & 0 deletions commands/sign-certificate.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
[[ ! ${WARDEN_COMMAND} ]] && >&2 echo -e "\033[31mThis script is not intended to be run directly!" && exit 1

mkdir -p "${WARDEN_SSL_DIR}/certs"

if [[ ! -f "${WARDEN_SSL_DIR}/rootca/certs/ca.cert.pem" ]]; then
echo -e "\033[31mError: Missing the root CA file. Please run 'warden install' and try again."
exit -1
fi

if (( ${#WARDEN_PARAMS[@]} == 0 )); then
echo -e "\033[33mCommand '${WARDEN_COMMAND}' requires a hostname as an argument, please use --help for details."
exit -1
fi

CERTIFICATE_SAN_LIST=
for (( i = 0; i < ${#WARDEN_PARAMS[@]} * 2; i+=2 )); do
[[ ${CERTIFICATE_SAN_LIST} ]] && CERTIFICATE_SAN_LIST+=","
CERTIFICATE_SAN_LIST+="DNS.$(expr $i + 1):${WARDEN_PARAMS[i/2]}"
CERTIFICATE_SAN_LIST+=",DNS.$(expr $i + 2):*.${WARDEN_PARAMS[i/2]}"
done

CERTIFICATE_NAME="${WARDEN_PARAMS[0]}"

if [[ -f "${WARDEN_SSL_DIR}/certs/${CERTIFICATE_NAME}.key.pem" ]]; then
>&2 echo -e "\033[33mWarning: Certificate for ${CERTIFICATE_NAME} already exists! Overwriting...\033[0m\n"
fi

echo "==> Generating private key ${CERTIFICATE_NAME}.key.pem"
openssl genrsa -out "${WARDEN_SSL_DIR}/certs/${CERTIFICATE_NAME}.key.pem" 2048

echo "==> Generating signing req ${CERTIFICATE_NAME}.crt.pem"
openssl req -new -sha256 -config <(cat \
"${WARDEN_DIR}/config/openssl/certificate.conf" \
<(printf "subjectAltName = %s" "${CERTIFICATE_SAN_LIST}") \
) \
-key "${WARDEN_SSL_DIR}/certs/${CERTIFICATE_NAME}.key.pem" \
-out "${WARDEN_SSL_DIR}/certs/${CERTIFICATE_NAME}.csr.pem" \
-subj "/C=US/CN=${CERTIFICATE_NAME}"

echo "==> Generating certificate ${CERTIFICATE_NAME}.crt.pem"
openssl x509 -req -days 365 -sha256 -extensions v3_req \
-extfile <(cat \
"${WARDEN_DIR}/config/openssl/certificate.conf" \
<(printf "subjectAltName = %s" "${CERTIFICATE_SAN_LIST}") \
) \
-CA "${WARDEN_SSL_DIR}/rootca/certs/ca.cert.pem" \
-CAkey "${WARDEN_SSL_DIR}/rootca/private/ca.key.pem" \
-CAserial "${WARDEN_SSL_DIR}/rootca/serial" \
-in "${WARDEN_SSL_DIR}/certs/${CERTIFICATE_NAME}.csr.pem" \
-out "${WARDEN_SSL_DIR}/certs/${CERTIFICATE_NAME}.crt.pem"

if [[ "$(cd "${WARDEN_DIR}" && docker-compose -p warden -f docker/docker-compose.yml ps -q traefik)" ]]; then
echo "==> Updating traefik"
"${WARDEN_DIR}/bin/warden" up traefik
"${WARDEN_DIR}/bin/warden" restart traefik
fi
12 changes: 12 additions & 0 deletions commands/sign-certificate.help
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
[[ ! ${WARDEN_COMMAND} ]] && >&2 echo -e "\033[31mThis script is not intended to be run directly!" && exit 1

WARDEN_USAGE=$(cat <<EOF
\033[33mUsage:\033[0m
sign-certificate <hostname> [hostname2] [hostname3] ...
\033[33mOptions:\033[0m
-h, --help Display this help menu
-v, --verbose Increases verbosity of output
EOF
)
5 changes: 5 additions & 0 deletions commands/start.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
[[ ! ${WARDEN_COMMAND} ]] && >&2 echo -e "\033[31mThis script is not intended to be run directly!" && exit 1

pushd "${WARDEN_DIR}" >/dev/null
docker-compose -p warden -f docker/docker-compose.yml start "${WARDEN_PARAMS[@]}" "$@"
5 changes: 5 additions & 0 deletions commands/stop.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
[[ ! ${WARDEN_COMMAND} ]] && >&2 echo -e "\033[31mThis script is not intended to be run directly!" && exit 1

pushd "${WARDEN_DIR}" >/dev/null
docker-compose -p warden -f docker/docker-compose.yml stop "${WARDEN_PARAMS[@]}" "$@"
22 changes: 22 additions & 0 deletions commands/up.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
[[ ! ${WARDEN_COMMAND} ]] && >&2 echo -e "\033[31mThis script is not intended to be run directly!" && exit 1

assert_installed

mkdir -p "${WARDEN_HOME_DIR}/etc/traefik"
cp "${WARDEN_DIR}/config/traefik/traefik.toml" "${WARDEN_HOME_DIR}/etc/traefik/traefik.toml"
cp "${WARDEN_DIR}/config/dnsmasq.conf" "${WARDEN_HOME_DIR}/etc/dnsmasq.conf"

# TODO: Determine if a template loop may work in the config file to do this automatically in traefik
for cert in $(find "${WARDEN_SSL_DIR}/certs" -type f -name "*.crt.pem" | sed -E 's#^.*/ssl/certs/(.*)\.crt\.pem$#\1#'); do
[[ "${cert}" = "warden.test" ]] && continue

cat >> "${WARDEN_HOME_DIR}/etc/traefik/traefik.toml" <<-EOF
[[entryPoints.https.tls.certificates]]
certFile = "/etc/ssl/certs/${cert}.crt.pem"
keyFile = "/etc/ssl/certs/${cert}.key.pem"
EOF
done

pushd "${WARDEN_DIR}" >/dev/null
docker-compose -p warden -f docker/docker-compose.yml up -d "${WARDEN_PARAMS[@]}" "$@"
12 changes: 12 additions & 0 deletions commands/usage.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
[[ ! ${WARDEN_COMMAND} ]] && >&2 echo -e "\033[31mThis script is not intended to be run directly!" && exit 1

## load usage info for the given command falling back on default usage text
if [[ -f "${WARDEN_DIR}/commands/${WARDEN_COMMAND}.help" ]]; then
source "${WARDEN_DIR}/commands/${WARDEN_COMMAND}.help"
else
source "${WARDEN_DIR}/commands/usage.help"
fi

echo -e "${WARDEN_USAGE}"
exit 1
32 changes: 32 additions & 0 deletions commands/usage.help
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
[[ ! ${WARDEN_COMMAND} ]] && >&2 echo -e "\033[31mThis script is not intended to be run directly!" && exit 1

WARDEN_HEADER='
_ __ __
| | / /___ __________/ /__ ____
| | /| / / __ `/ ___/ __ / _ \/ __ \
| |/ |/ / /_/ / / / /_/ / __/ / / /
|__/|__/\__,_/_/ \__,_/\___/_/ /_/
'

WARDEN_USAGE=$(cat <<EOF
${WARDEN_HEADER:1}
Warden version 0.1.0-dev
\033[33mUsage:\033[0m
command [options] [arguments]
\033[33mOptions:\033[0m
-h, --help Display this help menu
-v, --verbose Increases verbosity of output
\033[33mCommands:\033[0m
down Stop and remove containers, networks, and services
install Installs local docker environment in the current directory
restart Restarts warden managed containers
sign-certificate Signs a wildcard certificate including all passed hostnames on the SAN list
start Start services
stop Stop services
up Create and start containers, networks, and services
EOF
)
16 changes: 16 additions & 0 deletions config/dnsmasq.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#dnsmasq config, for a complete example, see:
# http://oss.segetech.com/intra/srv/dnsmasq.conf

#log all dns queries
log-queries

#dont use hosts nameservers
no-resolv

#use cloudflare as default nameservers, prefer 1^4
server=1.0.0.1
server=1.1.1.1
strict-order

#explicitly define host-ip mappings
address=/.test/127.0.0.1
20 changes: 20 additions & 0 deletions config/openssl/certificate.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req

[req_distinguished_name]
countryName = Country Name (2 letter code)
countryName_default = US
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default =
localityName = Locality Name (eg, city)
localityName_default =
organizationalUnitName = Organizational Unit Name (eg, section)
organizationalUnitName_default = Warden Proxy
commonName = Internet Widgits Ltd
commonName_max = 64

[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
Loading

0 comments on commit 4e763d2

Please sign in to comment.