Make a new Kubernetes friendly Docker image (#1499)

The following PIE-1598 subtasks are fixed:
fixes PIE-1600 remove entrypoint script

fixes PIE-1601 investigate Dockerfile best practices including :
 - naming
 - multi step build
 - comments
 - labels
 - build args
 - entrypoint
 - minimalism

fixes PIE-1604 rewrite Dockerfile according to best practices
provide a sample build command that can be used as-is or as an example.

Added contents to .dockerignore to make the intermediate build image smaller.

Remove .env file that was supposed to be used long ago for a docker quickstart
and that we forgot to remove ans is useless today.

add jenkins pipeline to test the docker image build

Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
pull/2/head
Nicolas MASSART 5 years ago committed by GitHub
parent 6003ff92a2
commit 48e771b3a6
  1. 13
      .dockerignore
  2. 5
      .env
  3. 2
      .gitignore
  4. 90
      Jenkinsfile
  5. 36
      kubernetes/Dockerfile
  6. 39
      kubernetes/build_image.sh
  7. 24
      kubernetes/test.sh
  8. 40
      kubernetes/tests/01/goss.yaml
  9. 17
      kubernetes/tests/01/goss_wait.yaml
  10. 113
      kubernetes/tests/dgoss
  11. BIN
      kubernetes/tests/goss-linux-amd64

@ -1,4 +1,15 @@
.github
.gradle
.idea
.vertx
build
build
kubernetes
Dockerfile
#Exclude doc related resources
docs
mkdocs.yml
readthedocs.yml
*.md
Jenkins*

@ -1,5 +0,0 @@
#Default env variables for docker compose quickstart
#defaults are empty values
RPC_PORT_MAPPING=
WS_PORT_MAPPING=
EXPLORER_PORT_MAPPING=

2
.gitignore vendored

@ -26,3 +26,5 @@ tmp/
build/
out/
site/
/kubernetes/reports/
/kubernetes/pantheon-*.tar.gz

90
Jenkinsfile vendored

@ -174,29 +174,85 @@ try {
}
}
}
if (env.BRANCH_NAME == "master") {
node {
checkout scm
unstash 'distTarBall'
docker.image(docker_image_dind).withRun('--privileged') { d ->
docker.image(docker_image).inside("-e DOCKER_HOST=tcp://docker:2375 --link ${d.id}:docker") {
stage('build image') {
sh "cd docker && cp ../build/distributions/pantheon-*.tar.gz ."
pantheon = docker.build("pegasyseng/pantheon:develop", "docker")
def registry = 'https://registry.hub.docker.com'
def userAccount = 'dockerhub-pegasysengci'
def imageRepos = 'pegasyseng'
def imageTag = 'develop'
parallel KubernetesDockerImage: {
def stage_name = 'Kubernetes Docker image node: '
def image = imageRepos + '/pantheon-kubernetes:' + imageTag
def kubernetes_folder = 'kubernetes'
def kubernetes_image_build_script = kubernetes_folder + '/build_image.sh'
def version_property_file = 'gradle.properties'
def reports_folder = kubernetes_folder + '/reports'
def dockerfile = kubernetes_folder + '/Dockerfile'
node {
checkout scm
unstash 'distTarBall'
docker.image(build_image).inside() {
stage(stage_name + 'Dockerfile lint') {
sh "docker run --rm -i hadolint/hadolint < ${dockerfile}"
}
stage(stage_name + 'Build image') {
sh "${kubernetes_image_build_script} '${image}'"
}
stage(stage_name + "Test image labels") {
shortCommit = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
version = sh(returnStdout: true, script: "grep -oE \"version=(.*)\" ${version_property_file} | cut -d= -f2").trim()
sh "docker image inspect \
--format='{{index .Config.Labels \"org.label-schema.vcs-ref\"}}' \
${image} \
| grep ${shortCommit}"
sh "docker image inspect \
--format='{{index .Config.Labels \"org.label-schema.version\"}}' \
${image} \
| grep ${version}"
}
try {
stage('test image') {
sh "apk add bash"
sh "mkdir -p docker/reports"
sh "cd docker && bash test.sh pegasyseng/pantheon:develop"
stage(stage_name + 'Test image') {
sh "mkdir -p ${reports_folder}"
sh "cd ${kubernetes_folder} && bash test.sh ${image}"
}
} finally {
junit 'docker/reports/*.xml'
sh "rm -rf docker/reports"
junit "${reports_folder}/*.xml"
sh "rm -rf ${reports_folder}"
}
stage('push image') {
docker.withRegistry('https://registry.hub.docker.com', 'dockerhub-pegasysengci') {
pantheon.push()
stage(stage_name + 'Push image') {
docker.withRegistry(registry, userAccount) {
docker.image(image).push()
}
}
}
}
},
DockerImage: {
def stage_name = 'Docker image node: '
def image = imageRepos + '/pantheon:' + imageTag
node {
checkout scm
unstash 'distTarBall'
docker.image(docker_image_dind).withRun('--privileged') { d ->
docker.image(docker_image).inside("-e DOCKER_HOST=tcp://docker:2375 --link ${d.id}:docker") {
stage(stage_name + 'build image') {
sh "cd docker && cp ../build/distributions/pantheon-*.tar.gz ."
pantheon = docker.build(image, "docker")
}
try {
stage('test image') {
sh "apk add bash"
sh "mkdir -p docker/reports"
sh "cd docker && bash test.sh ${image}"
}
} finally {
junit 'docker/reports/*.xml'
sh "rm -rf docker/reports"
}
stage(stage_name + 'push image') {
docker.withRegistry(registry, userAccount) {
pantheon.push()
}
}
}
}

@ -0,0 +1,36 @@
# extract image stage
# extractin here reduces the number of layers in the final image
FROM alpine:3.9 AS extract-stage
# Copy Pantheon binaries from previous jenkins artefact step
# or from the result of ./gradlew distTar
# and lett ADD unpack them
ADD pantheon-*.tar.gz /tmp/
# Run image stage
# Use openJDK JRE only for running pantheon
FROM openjdk:11.0.2-jre-slim-stretch
# Copy extracted binaries from the previous step image
COPY --from=extract-stage /tmp/pantheon* /opt/pantheon
WORKDIR /opt/pantheon
# Expose services ports
# 8545 HTTP JSON-RPC
# 8546 WS JSON-RPC
# 8547 HTTP GraphQL
# 30303 P2P
EXPOSE 8545 8546 8547 30303
ENTRYPOINT ["/opt/pantheon/bin/pantheon"]
# Build-time metadata as defined at http://label-schema.org
# Use the build_image.sh script in the kubernetes directory of this project to
# easily build this image or as an example of how to inject build parameters.
ARG BUILD_DATE
ARG VCS_REF
ARG VERSION
LABEL org.label-schema.build-date=$BUILD_DATE \
org.label-schema.name="Pantheon" \
org.label-schema.description="Enterprise Ethereum client" \
org.label-schema.url="https://docs.pantheon.pegasys.tech/" \
org.label-schema.vcs-ref=$VCS_REF \
org.label-schema.vcs-url="https://github.com/PegaSysEng/pantheon.git" \
org.label-schema.vendor="Pegasys" \
org.label-schema.version=$VERSION \
org.label-schema.schema-version="1.0"

@ -0,0 +1,39 @@
#!/bin/sh -e
# This script presents a sample way to build Pantheon Docker image
# with automatic build arguments from the current build workspace.
# It must be started from the same path as where the Dockerfile is located.
# you have to pass the imnage tag as an argument like for instance :
# build_image.sh "pegasyseng/pantheon-kubernetes:develop"
CONTEXT_FOLDER=kubernetes/
PANTHEON_BUILD_SOURCE='build/distributions/pantheon-*.tar.gz'
# Checking that you passed the tag for the image to be build
if [ -z "$1" ]
then
me=`basename "$0"`
echo "No image tag argument supplied to ${me}"
echo "ex.: ${me} \"pegasyseng/pantheon-kubernetes:develop\""
exit 1
fi
# looking for the distribution archive, either form CI step that builds form this
# workspace sources but with multiple test steps first
# or it builds it if you don't have one as you are probably
# not in a CI step.
if ls ${PANTHEON_BUILD_SOURCE} 1> /dev/null 2>&1; then
cp ${PANTHEON_BUILD_SOURCE} ${CONTEXT_FOLDER}
else
echo "No pantheon-*.tar.gz archive found."
echo "You are probably not running this from CI so running './gradlew distTar' first to have a local build"
./gradlew distTar
cp ${PANTHEON_BUILD_SOURCE} ${CONTEXT_FOLDER}
fi
# Builds docker image with tags matching the info form this current workspace
docker build \
-t "$1" \
--build-arg BUILD_DATE="`date`" \
--build-arg VCS_REF="`git show -s --format=%h`" \
--build-arg VERSION="`grep -oE "version=(.*)" gradle.properties | cut -d= -f2`" \
${CONTEXT_FOLDER}

@ -0,0 +1,24 @@
#!/bin/bash
export GOSS_PATH=tests/goss-linux-amd64
export GOSS_OPTS="$GOSS_OPTS --format junit"
export GOSS_FILES_STRATEGY=cp
DOCKER_IMAGE=$1
i=0
# Test for normal startup with ports opened
GOSS_FILES_PATH=tests/01 \
bash tests/dgoss \
run $DOCKER_IMAGE \
--network=dev \
--p2p-host=0.0.0.0 \
--rpc-http-enabled \
--rpc-http-host=0.0.0.0 \
--rpc-ws-enabled \
--rpc-ws-host=0.0.0.0 \
--graphql-http-enabled \
--graphql-http-host=0.0.0.0 \
> ./reports/01.xml || i=`expr $i + 1`
exit $i

@ -0,0 +1,40 @@
file:
/opt/pantheon/bin/pantheon:
exists: true
mode: "0755"
owner: root
group: root
filetype: file
contains: []
/opt/pantheon/database:
exists: true
mode: "0755"
owner: root
group: root
filetype: directory
contains: []
/opt/pantheon/key:
exists: true
mode: "0600"
owner: root
group: root
filetype: file
contains: []
port:
tcp:8545:
listening: true
tcp:8546:
listening: true
tcp:8547:
listening: true
tcp:30303:
listening: true
ip:
- 0.0.0.0
udp:30303:
listening: true
ip:
- 0.0.0.0
process:
java:
running: true

@ -0,0 +1,17 @@
port:
tcp:30303:
listening: true
ip:
- 0.0.0.0
tcp:8545:
listening: true
ip:
- 0.0.0.0
tcp:8546:
listening: true
ip:
- 0.0.0.0
tcp:8547:
listening: true
ip:
- 0.0.0.0

@ -0,0 +1,113 @@
#!/bin/bash
set -e
USAGE="USAGE: $(basename "$0") [run|edit] <docker_run_params>"
GOSS_FILES_PATH="${GOSS_FILES_PATH:-.}"
info() {
echo -e "INFO: $*" >&2;
}
error() {
echo -e "ERROR: $*" >&2;
exit 1;
}
cleanup() {
set +e
{ kill "$log_pid" && wait "$log_pid"; } 2> /dev/null
rm -rf "$tmp_dir"
if [[ $id ]];then
info "Deleting container"
docker rm -vf "$id" > /dev/null
fi
}
run(){
# Copy in goss
cp "${GOSS_PATH}" "$tmp_dir/goss"
chmod 755 "$tmp_dir/goss"
[[ -e "${GOSS_FILES_PATH}/goss.yaml" ]] && cp "${GOSS_FILES_PATH}/goss.yaml" "$tmp_dir"
[[ -e "${GOSS_FILES_PATH}/goss_wait.yaml" ]] && cp "${GOSS_FILES_PATH}/goss_wait.yaml" "$tmp_dir"
[[ ! -z "${GOSS_VARS}" ]] && [[ -e "${GOSS_FILES_PATH}/${GOSS_VARS}" ]] && cp "${GOSS_FILES_PATH}/${GOSS_VARS}" "$tmp_dir"
# Switch between mount or cp files strategy
GOSS_FILES_STRATEGY=${GOSS_FILES_STRATEGY:="mount"}
case "$GOSS_FILES_STRATEGY" in
mount)
info "Starting docker container"
id=$(docker run -d -v "$tmp_dir:/goss:z" "${@:2}")
docker logs -f "$id" > "$tmp_dir/docker_output.log" 2>&1 &
;;
cp)
info "Creating docker container"
id=$(docker create ${@:2})
info "Copy goss files into container"
docker cp $tmp_dir/. $id:/goss
info "Starting docker container"
docker start $id > /dev/null
;;
*) error "Wrong goss files strategy used! Correct options are \"mount\" or \"cp\"."
esac
log_pid=$!
info "Container ID: ${id:0:8}"
}
get_docker_file() {
if docker exec "$id" sh -c "test -e $1" > /dev/null;then
mkdir -p "${GOSS_FILES_PATH}"
info "Copied '$1' from container to '${GOSS_FILES_PATH}'"
docker cp "$id:$1" "${GOSS_FILES_PATH}"
fi
}
# Main
tmp_dir=$(mktemp -d /tmp/tmp.XXXXXXXXXX)
chmod 777 "$tmp_dir"
trap 'ret=$?;cleanup;exit $ret' EXIT
GOSS_PATH="${GOSS_PATH:-$(which goss 2> /dev/null || true)}"
[[ $GOSS_PATH ]] || { error "Couldn't find goss installation, please set GOSS_PATH to it"; }
[[ ${GOSS_OPTS+x} ]] || GOSS_OPTS="--color --format documentation"
[[ ${GOSS_WAIT_OPTS+x} ]] || GOSS_WAIT_OPTS="-r 30s -s 1s > /dev/null"
GOSS_SLEEP=${GOSS_SLEEP:-0.2}
case "$1" in
run)
run "$@"
if [[ -e "${GOSS_FILES_PATH}/goss_wait.yaml" ]]; then
info "Found goss_wait.yaml, waiting for it to pass before running tests"
if [[ -z "${GOSS_VARS}" ]]; then
if ! docker exec "$id" sh -c "/goss/goss -g /goss/goss_wait.yaml validate $GOSS_WAIT_OPTS"; then
error "goss_wait.yaml never passed"
fi
else
if ! docker exec "$id" sh -c "/goss/goss -g /goss/goss_wait.yaml --vars='/goss/${GOSS_VARS}' validate $GOSS_WAIT_OPTS"; then
error "goss_wait.yaml never passed"
fi
fi
fi
[[ $GOSS_SLEEP ]] && { info "Sleeping for $GOSS_SLEEP"; sleep "$GOSS_SLEEP"; }
# info "Container health"
# if ! docker top $id; then
# docker logs $id
# fi
info "Running Tests"
if [[ -z "${GOSS_VARS}" ]]; then
docker exec "$id" sh -c "/goss/goss -g /goss/goss.yaml validate $GOSS_OPTS"
else
docker exec "$id" sh -c "/goss/goss -g /goss/goss.yaml --vars='/goss/${GOSS_VARS}' validate $GOSS_OPTS"
fi
;;
edit)
run "$@"
info "Run goss add/autoadd to add resources"
docker exec -it "$id" sh -c 'cd /goss; PATH="/goss:$PATH" exec sh'
get_docker_file "/goss/goss.yaml"
get_docker_file "/goss/goss_wait.yaml"
[[ ! -z "${GOSS_VARS}" ]] && get_docker_file "/goss/${GOSS_VARS}"
;;
*)
error "$USAGE"
esac

Binary file not shown.
Loading…
Cancel
Save