mirror of https://github.com/containrrr/watchtower
				
				
				
			Feat/lifecycle hooks (#351)
* feat(update): add lifecycle hooks to the update action * fix(ci): add bash tests for lifecycle-hooks to the ci workflow * fix(ci): move integration tests to an isolated step * fix(ci): fix malformed all-contributors json * fix(ci): disable automatic bash test until we figure out a reasonable way to run it in circlecipull/353/head
							parent
							
								
									874180a518
								
							
						
					
					
						commit
						bfae38dbf8
					
				@ -0,0 +1,45 @@
 | 
			
		||||
 | 
			
		||||
## Executing commands before and after updating
 | 
			
		||||
 | 
			
		||||
> **DO NOTE**: Both commands are shell commands executed with `sh`, and therefore require the 
 | 
			
		||||
> container to provide the `sh` executable.
 | 
			
		||||
 | 
			
		||||
It is possible to execute a *pre-update* command and a *post-update* command 
 | 
			
		||||
**inside** every container updated by watchtower. The *pre-update* command is 
 | 
			
		||||
executed before stopping the container, and the *post-update* command is 
 | 
			
		||||
executed after restarting the container.
 | 
			
		||||
 | 
			
		||||
This feature is disabled by default. To enable it, you need to set the option
 | 
			
		||||
`--enable-lifecycle-hooks` on the command line, or set the environment variable
 | 
			
		||||
`WATCHTOWER_LIFECYCLE_HOOKS` to true.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
 | 
			
		||||
### Specifying update commands
 | 
			
		||||
 | 
			
		||||
The commands are specified using docker container labels, with 
 | 
			
		||||
`com.centurylinklabs.watchtower.pre-update-command` for the *pre-update* 
 | 
			
		||||
command and `com.centurylinklabs.watchtower.lifecycle.post-update` for the
 | 
			
		||||
*post-update* command.
 | 
			
		||||
 | 
			
		||||
These labels can be declared as instructions in a Dockerfile:
 | 
			
		||||
 | 
			
		||||
```docker
 | 
			
		||||
LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="/dump-data.sh"
 | 
			
		||||
LABEL com.centurylinklabs.watchtower.lifecycle.post-update="/restore-data.sh"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Or be specified as part of the `docker run` command line:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
docker run -d \
 | 
			
		||||
  --label=com.centurylinklabs.watchtower.lifecycle.pre-update="/dump-data.sh" \
 | 
			
		||||
  --label=com.centurylinklabs.watchtower.lifecycle.post-update="/restore-data.sh" \
 | 
			
		||||
  someimage
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Execution failure
 | 
			
		||||
 | 
			
		||||
The failure of a command to execute, identified by an exit code different than 
 | 
			
		||||
0, will not prevent watchtower from updating the container. Only an error
 | 
			
		||||
log statement containing the exit code will be reported.
 | 
			
		||||
@ -0,0 +1,39 @@
 | 
			
		||||
package container
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	watchtowerLabel = "com.centurylinklabs.watchtower"
 | 
			
		||||
	signalLabel     = "com.centurylinklabs.watchtower.stop-signal"
 | 
			
		||||
	enableLabel     = "com.centurylinklabs.watchtower.enable"
 | 
			
		||||
	zodiacLabel     = "com.centurylinklabs.zodiac.original-image"
 | 
			
		||||
	preUpdateLabel  = "com.centurylinklabs.watchtower.lifecycle.pre-update"
 | 
			
		||||
	postUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.post-update"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetLifecyclePreUpdateCommand returns the pre-update command set in the container metadata or an empty string
 | 
			
		||||
func (c Container) GetLifecyclePreUpdateCommand() string {
 | 
			
		||||
	return c.getLabelValueOrEmpty(preUpdateLabel)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetLifecyclePostUpdateCommand returns the post-update command set in the container metadata or an empty string
 | 
			
		||||
func (c Container) GetLifecyclePostUpdateCommand() string {
 | 
			
		||||
	return c.getLabelValueOrEmpty(postUpdateLabel)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ContainsWatchtowerLabel takes a map of labels and values and tells
 | 
			
		||||
// the consumer whether it contains a valid watchtower instance label
 | 
			
		||||
func ContainsWatchtowerLabel(labels map[string]string) bool {
 | 
			
		||||
	val, ok := labels[watchtowerLabel]
 | 
			
		||||
	return ok && val == "true"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c Container) getLabelValueOrEmpty(label string) string {
 | 
			
		||||
	if val, ok := c.containerInfo.Config.Labels[label]; ok {
 | 
			
		||||
		return val
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c Container) getLabelValue(label string) (string, bool) {
 | 
			
		||||
	val, ok := c.containerInfo.Config.Labels[label]
 | 
			
		||||
	return val, ok
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,208 @@
 | 
			
		||||
#!/usr/bin/env bash
 | 
			
		||||
 | 
			
		||||
set -e
 | 
			
		||||
 | 
			
		||||
IMAGE=server
 | 
			
		||||
CONTAINER=server
 | 
			
		||||
LINKED_IMAGE=linked
 | 
			
		||||
LINKED_CONTAINER=linked
 | 
			
		||||
WATCHTOWER_INTERVAL=2
 | 
			
		||||
 | 
			
		||||
function remove_container {
 | 
			
		||||
	docker kill $1 >> /dev/null || true && docker rm -v $1 >> /dev/null || true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function cleanup {
 | 
			
		||||
  # Do cleanup on exit or error
 | 
			
		||||
  echo "Final cleanup"
 | 
			
		||||
  sleep 2
 | 
			
		||||
  remove_container $CONTAINER
 | 
			
		||||
  remove_container $LINKED_CONTAINER
 | 
			
		||||
  pkill -9 -f watchtower >> /dev/null || true
 | 
			
		||||
}
 | 
			
		||||
trap cleanup EXIT
 | 
			
		||||
 | 
			
		||||
DEFAULT_WATCHTOWER="$(dirname "${BASH_SOURCE[0]}")/../watchtower"
 | 
			
		||||
WATCHTOWER=$1
 | 
			
		||||
WATCHTOWER=${WATCHTOWER:-$DEFAULT_WATCHTOWER}
 | 
			
		||||
echo "watchtower path is $WATCHTOWER"
 | 
			
		||||
 | 
			
		||||
##################################################################################
 | 
			
		||||
##### PREPARATION ################################################################
 | 
			
		||||
##################################################################################
 | 
			
		||||
 | 
			
		||||
#  Create Dockerfile template
 | 
			
		||||
DOCKERFILE=$(cat << EOF
 | 
			
		||||
FROM node:alpine
 | 
			
		||||
 | 
			
		||||
LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="cat /opt/test/value.txt"
 | 
			
		||||
LABEL com.centurylinklabs.watchtower.lifecycle.post-update="echo image > /opt/test/value.txt"
 | 
			
		||||
 | 
			
		||||
ENV IMAGE_TIMESTAMP=TIMESTAMP
 | 
			
		||||
 | 
			
		||||
WORKDIR /opt/test
 | 
			
		||||
ENTRYPOINT ["/usr/local/bin/node", "/opt/test/server.js"]
 | 
			
		||||
 | 
			
		||||
EXPOSE 8888
 | 
			
		||||
 | 
			
		||||
RUN mkdir -p /opt/test && echo "default" > /opt/test/value.txt
 | 
			
		||||
COPY server.js /opt/test/server.js
 | 
			
		||||
EOF
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# Create temporary directory to build docker image
 | 
			
		||||
TMP_DIR="/tmp/watchtower-commands-test"
 | 
			
		||||
mkdir -p $TMP_DIR
 | 
			
		||||
 | 
			
		||||
# Create simple http server
 | 
			
		||||
cat > $TMP_DIR/server.js << EOF
 | 
			
		||||
const http = require("http");
 | 
			
		||||
const fs = require("fs");
 | 
			
		||||
 | 
			
		||||
http.createServer(function(request, response) {
 | 
			
		||||
	const fileContent = fs.readFileSync("/opt/test/value.txt");
 | 
			
		||||
	response.writeHead(200, {"Content-Type": "text/plain"});
 | 
			
		||||
	response.write(fileContent);
 | 
			
		||||
	response.end();
 | 
			
		||||
}).listen(8888, () => { console.log('server is listening on 8888'); });
 | 
			
		||||
EOF
 | 
			
		||||
 | 
			
		||||
function builddocker {
 | 
			
		||||
	TIMESTAMP=$(date +%s)
 | 
			
		||||
	echo "Building image $TIMESTAMP"
 | 
			
		||||
	echo "${DOCKERFILE/TIMESTAMP/$TIMESTAMP}" > $TMP_DIR/Dockerfile
 | 
			
		||||
	docker build $TMP_DIR -t $IMAGE >> /dev/null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Start watchtower
 | 
			
		||||
echo "Starting watchtower"
 | 
			
		||||
$WATCHTOWER -i $WATCHTOWER_INTERVAL --no-pull --stop-timeout 2s --enable-lifecycle-hooks $CONTAINER $LINKED_CONTAINER &
 | 
			
		||||
sleep 3
 | 
			
		||||
 | 
			
		||||
echo "#################################################################"
 | 
			
		||||
echo "##### TEST CASE 1: Execute commands from base image"
 | 
			
		||||
echo "#################################################################"
 | 
			
		||||
 | 
			
		||||
# Build base image
 | 
			
		||||
builddocker
 | 
			
		||||
 | 
			
		||||
# Run container
 | 
			
		||||
docker run -d -p 0.0.0.0:8888:8888 --name $CONTAINER $IMAGE:latest >> /dev/null
 | 
			
		||||
sleep 1
 | 
			
		||||
echo "Container $CONTAINER is runnning"
 | 
			
		||||
 | 
			
		||||
# Test default value
 | 
			
		||||
RESP=$(curl -s http://localhost:8888)
 | 
			
		||||
if [ $RESP != "default" ]; then
 | 
			
		||||
	echo "Default value of container response is invalid" 1>&2
 | 
			
		||||
	exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Build updated image to trigger watchtower update
 | 
			
		||||
builddocker
 | 
			
		||||
 | 
			
		||||
WAIT_AMOUNT=$(($WATCHTOWER_INTERVAL * 3))
 | 
			
		||||
echo "Wait for $WAIT_AMOUNT seconds"
 | 
			
		||||
sleep $WAIT_AMOUNT
 | 
			
		||||
 | 
			
		||||
# Test value after post-update-command
 | 
			
		||||
RESP=$(curl -s http://localhost:8888)
 | 
			
		||||
if [[ $RESP != "image" ]]; then
 | 
			
		||||
	echo "Value of container response is invalid. Expected: image. Actual: $RESP"
 | 
			
		||||
	exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
remove_container $CONTAINER
 | 
			
		||||
 | 
			
		||||
echo "#################################################################"
 | 
			
		||||
echo "##### TEST CASE 2: Execute commands from container and base image"
 | 
			
		||||
echo "#################################################################"
 | 
			
		||||
 | 
			
		||||
# Build base image
 | 
			
		||||
builddocker
 | 
			
		||||
 | 
			
		||||
# Run container
 | 
			
		||||
docker run -d -p 0.0.0.0:8888:8888 \
 | 
			
		||||
	--label=com.centurylinklabs.watchtower.lifecycle.post-update="echo container > /opt/test/value.txt" \
 | 
			
		||||
	--name $CONTAINER $IMAGE:latest >> /dev/null
 | 
			
		||||
sleep 1
 | 
			
		||||
echo "Container $CONTAINER is runnning"
 | 
			
		||||
 | 
			
		||||
# Test default value
 | 
			
		||||
RESP=$(curl -s http://localhost:8888)
 | 
			
		||||
if [ $RESP != "default" ]; then
 | 
			
		||||
	echo "Default value of container response is invalid" 1>&2
 | 
			
		||||
	exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Build updated image to trigger watchtower update
 | 
			
		||||
builddocker
 | 
			
		||||
 | 
			
		||||
WAIT_AMOUNT=$(($WATCHTOWER_INTERVAL * 3))
 | 
			
		||||
echo "Wait for $WAIT_AMOUNT seconds"
 | 
			
		||||
sleep $WAIT_AMOUNT
 | 
			
		||||
 | 
			
		||||
# Test value after post-update-command
 | 
			
		||||
RESP=$(curl -s http://localhost:8888)
 | 
			
		||||
if [[ $RESP != "container" ]]; then
 | 
			
		||||
	echo "Value of container response is invalid. Expected: container. Actual: $RESP"
 | 
			
		||||
	exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
remove_container $CONTAINER
 | 
			
		||||
 | 
			
		||||
echo "#################################################################"
 | 
			
		||||
echo "##### TEST CASE 3: Execute commands with a linked container"
 | 
			
		||||
echo "#################################################################"
 | 
			
		||||
 | 
			
		||||
# Tag the current image to keep a version for the linked container
 | 
			
		||||
docker tag $IMAGE:latest $LINKED_IMAGE:latest
 | 
			
		||||
 | 
			
		||||
# Build base image
 | 
			
		||||
builddocker
 | 
			
		||||
 | 
			
		||||
# Run container
 | 
			
		||||
docker run -d -p 0.0.0.0:8888:8888 \
 | 
			
		||||
	--label=com.centurylinklabs.watchtower.lifecycle.post-update="echo container > /opt/test/value.txt" \
 | 
			
		||||
	--name $CONTAINER $IMAGE:latest >> /dev/null
 | 
			
		||||
docker run -d -p 0.0.0.0:8989:8888 \
 | 
			
		||||
	--label=com.centurylinklabs.watchtower.lifecycle.post-update="echo container > /opt/test/value.txt" \
 | 
			
		||||
	--link $CONTAINER \
 | 
			
		||||
	--name $LINKED_CONTAINER $LINKED_IMAGE:latest >> /dev/null
 | 
			
		||||
sleep 1
 | 
			
		||||
echo "Container $CONTAINER and $LINKED_CONTAINER are runnning"
 | 
			
		||||
 | 
			
		||||
# Test default value
 | 
			
		||||
RESP=$(curl -s http://localhost:8888)
 | 
			
		||||
if [ $RESP != "default" ]; then
 | 
			
		||||
	echo "Default value of container response is invalid" 1>&2
 | 
			
		||||
	exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Test default value for linked container
 | 
			
		||||
RESP=$(curl -s http://localhost:8989)
 | 
			
		||||
if [ $RESP != "default" ]; then
 | 
			
		||||
	echo "Default value of linked container response is invalid" 1>&2
 | 
			
		||||
	exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Build updated image to trigger watchtower update
 | 
			
		||||
builddocker
 | 
			
		||||
 | 
			
		||||
WAIT_AMOUNT=$(($WATCHTOWER_INTERVAL * 3))
 | 
			
		||||
echo "Wait for $WAIT_AMOUNT seconds"
 | 
			
		||||
sleep $WAIT_AMOUNT
 | 
			
		||||
 | 
			
		||||
# Test value after post-update-command
 | 
			
		||||
RESP=$(curl -s http://localhost:8888)
 | 
			
		||||
if [[ $RESP != "container" ]]; then
 | 
			
		||||
	echo "Value of container response is invalid. Expected: container. Actual: $RESP"
 | 
			
		||||
	exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Test that linked container did not execute pre/post-update-command
 | 
			
		||||
RESP=$(curl -s http://localhost:8989)
 | 
			
		||||
if [[ $RESP != "default" ]]; then
 | 
			
		||||
	echo "Value of linked container response is invalid. Expected: default. Actual: $RESP"
 | 
			
		||||
	exit 1
 | 
			
		||||
fi
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue