Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade to support a Kubernetes nginx deployment and run as non-root #3

Merged
merged 2 commits into from
Feb 1, 2024
Merged
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
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "tests/test_helpers/bats-support"]
path = tests/test_helpers/bats-support
url = https://github.com/bats-core/bats-support.git
[submodule "tests/test_helpers/bats-assert"]
path = tests/test_helpers/bats-assert
url = https://github.com/bats-core/bats-assert.git
10 changes: 8 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,13 @@ RUN set -x \
;

RUN apk update \
&& apk add --no-cache bash git nginx aws-cli \
&& apk add --no-cache bash tree git nginx aws-cli \
&& cd /etc/nginx \
&& mkdir -p /run/nginx /var/www/html \
&& ln -s /dev/stdout /var/log/nginx/access.log \
&& ln -s /dev/stderr /var/log/nginx/error.log \
&& chown nginx:nginx /etc/nginx/http.d \
&& chown nginx:nginx /var/www/html \
;

RUN echo "daemon off;" >> /etc/nginx/nginx.conf
Expand All @@ -49,7 +53,9 @@ COPY *.sh /

ENTRYPOINT ["/entry.sh"]

EXPOSE 80
EXPOSE 8080
STOPSIGNAL SIGTERM

USER nginx

CMD ["nginx"]
35 changes: 32 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ This image is available on quay.io `quay.io/panubo/staticsite` and AWS ECR Publi
## Entrypoint Commands

- `nginx` - Serve static files from `/var/www/html` (default)
- `s3sync` - Synchronize the files in `/var/www/html` with a S3 bucket (uses awscli).
- `s3sync` - Synchronize the files in `/var/www/html` with a S3 bucket (uses awscli)
- `k8s-init` - Copy all files to volume and template files (intended for a Kubernetes initContainer)
- `k8s-nginx` - Start nginx only (no rendering, intended to run after `k8s-init`)

## Configuration / Environment Options

For `nginx` entrypoint:

- `PORT` - server port for nginx to listen on (Default: `8080`)
- `NGINX_SERVER_ROOT` - server web root (Default: `/var/www/html`)
- `NGINX_SERVER_INDEX` - server index page(s) (Default: `index.html index.htm`)
- `NGINX_SINGLE_PAGE_ENABLED` - if set to `true` all requests will be routed through `/$NGINX_SINGLE_PAGE_INDEX`
Expand Down Expand Up @@ -48,7 +51,10 @@ Additional entrypoint pre-commands or post-commands can be specified in a `Deplo
- `DEPLOYFILE_POST` - Post Deployfile location, (Default: `/Deployfile.post`)
- `RUN_DEPLOYFILE_COMMANDS` - Set to `true` to enable this functionality.

N.B. When running `nginx` command, only the `DEPLOYFILE_PRE` is able to execute.
Notes:

* When running `nginx` command, only the `DEPLOYFILE_PRE` is able to execute.
* Pre and post Deployfile is disabled for the `k8s-*` entrypoints

### Cache Control Override

Expand All @@ -70,11 +76,34 @@ See [docs](./docs/) for usage examples.

This is used for deploying production sites, however it should be considered subject to functionality changes.

## v0.4.0 Upgrade **BREAKING CHANGES**

There are two main breaking changes includes in the v0.4.0 release.

**Run as non-root**

The image is setup to be run as non-root. This requires any content added to the image be owned by the `nginx` user. This can be achieved with one of the following methods.

Using `COPY --chown=nginx:nginx . /var/www/html` (recommended)

Or, using

```
USER root
RUN chown -R nginx:nginx /var/www/html
USER nginx
```

Alternatively this change can be simply reverted by adding `USER root` to your downstream image.

**Change nginx port to 8080**

Previously nginx listened on port `80` however this is considered a privileged port, the image now defaults to listening on port `8080`. This can be overridden by setting the env var `PORT=80`.

### Known issues

* Setting both cache control override and content type override may result in unexpected behaviour.

### TODO

* Implement similar Cache-Control functionality for Nginx hosted static sites.
* Get everything to work with a non-root user
4 changes: 2 additions & 2 deletions default.conf.tmpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
listen {{env.Getenv "PORT" "8080"}} default_server;
listen [::]:{{env.Getenv "PORT" "8080"}} default_server;

index {{.Env.NGINX_SERVER_INDEX}};
root {{.Env.NGINX_SERVER_ROOT}};
Expand Down
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ ADD . .
# Build the site using yarn
RUN yarn install --pure-lockfile --ignore-optional --no-progress

FROM panubo/staticsite:latest
FROM quay.io/panubo/staticsite:latest

# Copy build artefacts to staticsite image
COPY --from=build /usr/src/app/dist /var/www/html
COPY --from=build --chown=nginx:nginx /usr/src/app/dist /var/www/html

WORKDIR /var/www/html

Expand Down
4 changes: 2 additions & 2 deletions docs/example/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ ADD . .
# Build the site using yarn
RUN yarn install --pure-lockfile --ignore-optional --no-progress

FROM panubo/staticsite:latest
FROM quay.io/panubo/staticsite:latest

# Copy build artefacts to staticsite image
# COPY --from=build /usr/src/app/dist /var/www/html
COPY --from=build --chown=nginx:nginx /usr/src/app/dist /var/www/html

WORKDIR /var/www/html

Expand Down
29 changes: 22 additions & 7 deletions entry.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,42 @@ export NGINX_SINGLE_PAGE_INDEX=${NGINX_SINGLE_PAGE_INDEX:-'index.html'}
export DEPLOYFILE_PRE=${DEPLOYFILE_PRE:-'/Deployfile.pre'}
export DEPLOYFILE_POST=${DEPLOYFILE_POST:-'/Deployfile.post'}

echo "> Running templater"
/templater.sh
templater() {
echo "> Running templater"
/templater.sh
}

deployfile_pre() {
if [[ -f "${DEPLOYFILE_PRE}" ]] && [[ "${RUN_DEPLOYFILE_COMMANDS:-}" == 'true' ]]; then
echo "> Running all commands in ${DEPLOYFILE_PRE}"
run_all "${DEPLOYFILE_PRE}"
fi
}

echo "> Entrypoint command:" "${@}"

if [[ -f "${DEPLOYFILE_PRE}" ]] && [[ "${RUN_DEPLOYFILE_COMMANDS:-}" == 'true' ]]; then
echo "> Running all commands in ${DEPLOYFILE_PRE}"
run_all "${DEPLOYFILE_PRE}"
fi

if [[ "${1}" == "s3sync" ]]; then
templater
deployfile_pre
echo "> Running s3sync"
/s3sync.sh
if [[ -f "${DEPLOYFILE_POST}" ]] && [[ "${RUN_DEPLOYFILE_COMMANDS:-}" == 'true' ]]; then
echo "> Running all commands in ${DEPLOYFILE_POST}"
run_all "${DEPLOYFILE_POST}"
fi
elif [[ "${1}" == "nginx" ]]; then
templater
deployfile_pre
echo "> Running nginx"
render_templates /etc/nginx/http.d/default.conf.tmpl
nginx
elif [[ "${1}" == "k8s-nginx" ]]; then
echo "> Running nginx"
nginx
elif [[ "${1}" == "k8s-init" ]]; then
echo "> Running Kubernetes template script"
/k8s-init.sh
exit
else
if [[ "$1" != "" ]]; then
exec "${@}"
Expand Down
27 changes: 27 additions & 0 deletions k8s-init.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash

# This script will copy the nginx config and html content to /volume then run
# the normal renderers. This is used in an initContainer in a Kubernetes pod.
# The /volume mount should then be mounted into the main container as
# readOnly mounts and using `k8s-nginx` as the command.

K8S_VOLUME_PATH="${K8S_VOLUME_PATH:=/volume}"

source /panubo-functions.sh

set -euo pipefail
IFS=$'\n\t'

[[ "${DEBUG:-}" == 'true' ]] && set -x

mkdir "${K8S_VOLUME_PATH}/config"
mkdir "${K8S_VOLUME_PATH}/content"

cp -a /etc/nginx/http.d "${K8S_VOLUME_PATH}/config"
cp -a "${NGINX_SERVER_ROOT}" "${K8S_VOLUME_PATH}/content"

export OLD_NGINX_SERVER_ROOT="${NGINX_SERVER_ROOT}"
export NGINX_SERVER_ROOT=${K8S_VOLUME_PATH}/content/html
/templater.sh

render_templates "${K8S_VOLUME_PATH}/config/http.d/default.conf.tmpl"
6 changes: 4 additions & 2 deletions templater.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ IFS=$'\n\t'

# render templates when set
while read -r line; do
echo ">> Running templater on ${line#*\=}"
newline="${line#*\=}"
echo ">> Running templater on ${newline}"
(
# relative paths to server root
cd "${NGINX_SERVER_ROOT}"
render_templates "${line#*\=}"
# render templates and replace any full paths if OLD_NGINX_SERVER_ROOT is set (set by k8s-init.sh)
render_templates "${newline/${OLD_NGINX_SERVER_ROOT:-}/${NGINX_SERVER_ROOT}}"
)
done < <(env | grep "^RENDER_TEMPLATE")
6 changes: 4 additions & 2 deletions tests/html/v1/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ ENV \
CACHE_CONTROL_OVERRIDE_INDEX="index.html" \
CACHE_CONTROL_OVERRIDE_404="404.html" \
CACHE_CONTROL_DEFAULT_OVERRIDE="public, max-age=60, s-maxage=60" \
CACHE_CONTROL_OVERRIDE_SPECIAL="special-cache.html:public, max-age=600"
CACHE_CONTROL_OVERRIDE_SPECIAL="special-cache.html:public, max-age=600" \
RENDER_TEMPLATE_ENV_CONFIG=/var/www/html/env-config.js.tmpl \
RENDER_TEMPLATE_ENV_CONFIG2=env-config2.js.tmpl

COPY . /var/www/html
COPY --chown=nginx:nginx . /var/www/html
3 changes: 3 additions & 0 deletions tests/html/v1/env-config.js.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
window._env_ = {
"hostname": "{{ env.Getenv "HOSTNAME" }}",
}
3 changes: 3 additions & 0 deletions tests/html/v1/env-config2.js.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
window._env_ = {
"hostname": "{{ env.Getenv "HOSTNAME" }}",
}
4 changes: 4 additions & 0 deletions tests/html/v2/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ ENV \
CACHE_CONTROL_OVERRIDE_CACHED_JSON="cached-json:public, max-age=1200"

COPY . /var/www/html

USER root
RUN chown -R nginx:nginx /var/www/html
USER nginx
56 changes: 56 additions & 0 deletions tests/k8s.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
load test_helpers/bats-support/load
load test_helpers/bats-assert/load
load functions.bash
# load setup.bash

setup_file() {
# Disable parallel execution in this file
export BATS_NO_PARALLELIZE_WITHIN_FILE=true

docker_volume="$(docker volume create)"

export docker_volume
}

teardown_file() {
docker volume rm -f "${docker_volume}" || true
}

@test "k8s-init" {
# Fix volume permissions to match K8s behaviour
docker run --rm -v "${docker_volume}:/volume" busybox install -d -o root -g 2000 -m 2775 /volume

# Run k8s-init - content and config should be copied to the volume
docker run --rm -v "${docker_volume}:/volume" --group-add 2000 panubo/staticsite-testsite:1 k8s-init

# Print the content of the volume
run docker run --rm -v "${docker_volume}:/volume" busybox sh -c 'find /volume | sort'

# diag "${output}"
assert_line '/volume/config/http.d/default.conf'
assert_line '/volume/content/html/env-config.js'
assert_line '/volume/content/html/env-config2.js'

# assert_output --regexp '/volume/config/http\.d/default\.conf[^.]'
# assert_output --regexp '/volume/content/html/env-config\.js[^.]'
# assert_output --regexp '/volume/content/html/env-config2\.js[^.]'
}

@test "k8s-nginx" {
# This test isn't possible with docker since your cannot mount a subPath
# from a docker volume. eg `-v "$
# {docker_volume}/config/http.d:/etc/nginx/http.d:ro` (or whatever the
# syntax will be if implemented in docker).
skip "Unable to implement with docker, missing subPath support"

container="$(docker run -d -v "${docker_volume}:/volume:ro" --group-add 2000 -p 8080 panubo/staticsite-testsite:1 k8s-nginx)"
container_ip="$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' ${container})"
container_http_port="$(docker inspect --format '{{(index (index .NetworkSettings.Ports "8080/tcp") 0).HostPort}}' ${container} || { docker logs ${container} >&3 2>&3; return 1; })"
( wait_http "http://127.0.0.1:${container_http_port}"; )

run curl -sSf http://127.0.0.1:${container_http_port}

docker rm -f "${container}" || true

assert_success
}
10 changes: 6 additions & 4 deletions tests/nginx.bats
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
load test_helpers/bats-support/load
load test_helpers/bats-assert/load
load functions.bash
# load setup.bash

setup_file() {
container="$(docker run -d -p 80 panubo/staticsite-testsite:1 nginx)"
container="$(docker run -d -p 8080 panubo/staticsite-testsite:1 nginx)"
container_ip="$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' ${container})"
container_http_port="$(docker inspect --format '{{(index (index .NetworkSettings.Ports "80/tcp") 0).HostPort}}' ${container})"
container_http_port="$(docker inspect --format '{{(index (index .NetworkSettings.Ports "8080/tcp") 0).HostPort}}' ${container} || { docker logs ${container} >&3 2>&3; return 1; })"
( wait_http "http://127.0.0.1:${container_http_port}"; )
export container container_ip container_http_port
}
Expand All @@ -17,6 +19,6 @@ teardown_file() {
# echo "# curl -sSf http://127.0.0.1:${container_http_port}" >&3
run curl -sSf http://127.0.0.1:${container_http_port}
# diag "${output}"
[[ "${status}" -eq 0 ]]
[[ "${lines[0]}" = "<h1>Hello World!</h1>" ]]
assert_success
assert_line "<h1>Hello World!</h1>"
}
14 changes: 8 additions & 6 deletions tests/s3-rollback.bats
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
load test_helpers/bats-support/load
load test_helpers/bats-assert/load
load functions.bash
# load setup.bash

Expand Down Expand Up @@ -51,8 +53,8 @@ teardown_file() {
run curl -i -sSf http://127.0.0.1:${minio_container_http_port}/test-bucket/index.html
# diag "${output}"

[[ "${status}" -eq 0 ]]
grep "<p>v1</p>" <<<"${output}"
assert_success
assert_line "<p>v1</p>"
}

@test "s3-rollback upload v2" {
Expand All @@ -69,8 +71,8 @@ teardown_file() {
run curl -i -sSf http://127.0.0.1:${minio_container_http_port}/test-bucket/index.html
# diag "${output}"

[[ "${status}" -eq 0 ]]
grep "<p>v2</p>" <<<"${output}"
assert_success
assert_line "<p>v2</p>"
}

@test "s3-rollback rollback to v1" {
Expand All @@ -87,6 +89,6 @@ teardown_file() {
run curl -sSf http://127.0.0.1:${minio_container_http_port}/test-bucket/index.html
# diag "${output}"

[[ "${status}" -eq 0 ]]
grep "<p>v1</p>" <<<"${output}"
assert_success
assert_line "<p>v1</p>"
}
Loading
Loading