diff --git a/scripts/docker_lib.sh b/scripts/docker_lib.sh new file mode 100644 index 000000000000..f3baca91575f --- /dev/null +++ b/scripts/docker_lib.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash + +# Source of truth, these should not be modified elsewhere +readonly IMAGE_ARCH_LIST="amd64 arm64 ppc64le s390x" +readonly CONTAINER_REGISTRY_LIST="quay.io/coreos gcr.io/etcd-development" + +function compareSemanticVersions() { + + v1=$(echo "$1" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+') + v2=$(echo "$2" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+') + + if [ -z "$v1" ] || [ -z "$v2" ]; then + log_error "ERROR: Invalid version string(s) provided ${v1} and ${v2}." + exit 1 + fi + + read -ra ver1 <<< "${v1//./ }" + read -ra ver2 <<< "${v2//./ }" + + len=${#ver1[@]} + if ((len < ${#ver2[@]})); then + len=${#ver2[@]} + fi + + # compare each component of the versions + for ((i=0; i num2)); then + log_info "INFO: ${v1} is greater than ${v2}." + return 0 + fi + done + + # if we reach this point, the versions are equal + log_info "INFO: ${v1} and ${v2} are equal." + return 0 +} + +function checkContainerEngine() { + + # Validate args + if [ "${#}" -ne 1 ]; then + log_error "ERROR: checkContainerEngine requires exactly one parameter." + exit 1 + fi + + ENGINE="${1}" + + # Etcd releases require a container engine like docker + if ! command -v "${ENGINE}" >/dev/null; then + log_error "ERROR: Cannot find container engine ${ENGINE}." + exit 1 + fi + + # Etcd releases require a modern version of docker + # TODO Consider adding support for a recent version of podman + MINIMUM_DOCKER_VERSION="20.10.0" + CURRENT_VERSION=$("${ENGINE}" version --format '{{.Server.Version}}' | cut -d '+' -f1) + + if compareSemanticVersions "${MINIMUM_DOCKER_VERSION}" "${CURRENT_VERSION}"; then + log_success "SUCCESS: ${ENGINE} version ${CURRENT_VERSION} is >= minimum version ${MINIMUM_DOCKER_VERSION}." + else + log_error "ERROR: ${ENGINE} version ${CURRENT_VERSION} is less than the minimum version ${MINIMUM_DOCKER_VERSION}." + exit 1 + fi + + # Etcd releases require a linux host + if [[ $(go env GOOS) == "darwin" ]]; then + log_error "ERROR: Please use linux machine for release builds." + exit 1 + fi +} + +function checkImageExists() { + + # Validate args + if [ "${#}" -ne 1 ]; then + log_error "ERROR: checkImageExists requires exactly one parameter." + exit 1 + fi + + IMAGE="${1}" + log_info "INFO: Checking image ${IMAGE} is present locally." + if [[ "$(docker images --quiet "${IMAGE}" 2> /dev/null)" == "" ]]; then + log_error "ERROR: Image ${IMAGE} not present locally." + exit 1 + fi + + log_success "SUCCESS: Image ${IMAGE} exists locally." +} + + +function checkImageHasArch() { + + # Validate args + if [ "${#}" -ne 2 ]; then + log_error "ERROR: checkImageHasArch requires exactly two parameters." + exit 1 + fi + + IMAGE="${1}" + EXPECTED_ARCH="${2}" + + # Validate the image exists + checkImageExists "${IMAGE}" + + # Validate that manifest exists + MANIFEST=$(docker manifest inspect "${IMAGE}") + if [ -z "${MANIFEST}" ]; then + log_error "ERROR: Manifest not found for image ${IMAGE}." + exit 1 + fi + + # Check that expected architecture is included + PLATFORMS=$(echo "${MANIFEST}" | sed -n 's/.*"platform":{\(.*\)},/\1/p' | tr -d ' ') + if [[ "${PLATFORMS}" != *"${EXPECTED_ARCH}"* ]]; then + log_error "ERROR: Image ${IMAGE} does not contain the expected architecture ${EXPECTED_ARCH}." + exit 1 + fi + + log_success "SUCCESS: Image ${IMAGE} contains the expected architecture ${EXPECTED_ARCH}." + return 0 +} + +function buildMultiArchImage() { + + # Validate args + if [ "${#}" -ne 1 ]; then + log_error "ERROR: buildMultiArchImage requires exactly one parameter." + exit 1 + fi + + RELEASE_VERSION="${1}" + IFS=' ' read -ra image_arch_array <<< "${IMAGE_ARCH_LIST}" + IFS=' ' read -ra registries_array <<< "${CONTAINER_REGISTRY_LIST}" + + for TARGET_ARCH in "${image_arch_array[@]}"; do + for REGISTRY in "${registries_array[@]}"; do + + # Validate required images exist + checkImageExists "${REGISTRY}/etcd:${RELEASE_VERSION}" + checkImageExists "${REGISTRY}/etcd:${RELEASE_VERSION}-${TARGET_ARCH}" + + # Update the manifest + log_info "INFO: Updating manifest ${REGISTRY}/etcd:${RELEASE_VERSION} with ${REGISTRY}/etcd:${RELEASE_VERSION}-${TARGET_ARCH}." + docker manifest create --amend "${REGISTRY}/etcd:${RELEASE_VERSION}" "${REGISTRY}/etcd:${RELEASE_VERSION}-${TARGET_ARCH}" + docker manifest annotate "${REGISTRY}/etcd:${RELEASE_VERSION}" "${REGISTRY}/etcd:${RELEASE_VERSION}-${TARGET_ARCH}" --arch "${TARGET_ARCH}" + done + done +} + +function startContainer { + + # Validate args + if [ "${#}" -ne 2 ]; then + log_error "ERROR: startContainer requires exactly two parameters." + exit 1 + fi + + CONTAINER_NAME="${1}" + IMAGE="${2}" + + # Run docker in the background + log_info "INFO: Starting container named: ${CONTAINER_NAME} with image: ${IMAGE}." + docker run --detach --rm --name "${CONTAINER_NAME}" "${IMAGE}" + + # Wait for etcd daemon to bootstrap + sleep 5 + + # Cleanup in case of failures + trap 'docker stop "${CONTAINER_NAME}"' EXIT +} diff --git a/scripts/release.sh b/scripts/release.sh index 47e840f21df9..a50583ded1a1 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -5,6 +5,7 @@ set -o nounset set -o pipefail source ./scripts/test_lib.sh +source ./scripts/docker_lib.sh source ./scripts/release_mod.sh DRY_RUN=${DRY_RUN:-true} @@ -72,13 +73,8 @@ main() { log_callout "REPOSITORY=${REPOSITORY}" log_callout "" - # Required to enable 'docker manifest ...' - export DOCKER_CLI_EXPERIMENTAL=enabled - - if ! command -v docker >/dev/null; then - log_error "cannot find docker" - exit 1 - fi + # Releases can't proceed without docker + checkContainerEngine "docker" # Expected umask for etcd release artifacts umask 022 @@ -258,16 +254,18 @@ main() { log_warning "login failed, retrying" done - log_callout "Creating manifest-list (multi-image)..." - - for TARGET_ARCH in "amd64" "arm64" "ppc64le" "s390x"; do - maybe_run docker manifest create --amend "quay.io/coreos/etcd:${RELEASE_VERSION}" "quay.io/coreos/etcd:${RELEASE_VERSION}-${TARGET_ARCH}" - maybe_run docker manifest annotate "quay.io/coreos/etcd:${RELEASE_VERSION}" "quay.io/coreos/etcd:${RELEASE_VERSION}-${TARGET_ARCH}" --arch "${TARGET_ARCH}" - - maybe_run docker manifest create --amend "gcr.io/etcd-development/etcd:${RELEASE_VERSION}" "gcr.io/etcd-development/etcd:${RELEASE_VERSION}-${TARGET_ARCH}" - maybe_run docker manifest annotate "gcr.io/etcd-development/etcd:${RELEASE_VERSION}" "gcr.io/etcd-development/etcd:${RELEASE_VERSION}-${TARGET_ARCH}" --arch "${TARGET_ARCH}" + IFS=' ' read -ra image_arch_array <<< "${IMAGE_ARCH_LIST}" + IFS=' ' read -ra registries_array <<< "${CONTAINER_REGISTRY_LIST}" + for TARGET_ARCH in "${image_arch_array[@]}"; do + for REGISTRY in "${registries_array[@]}"; do + log_callout "Pushing container images to ${REGISTRY} ${RELEASE_VERSION}-${TARGET_ARCH}" + maybe_run docker push "${REGISTRY}/etcd:${RELEASE_VERSION}-${TARGET_ARCH}" + done done + log_callout "Creating manifest-list (multi-image)..." + maybe_run buildMultiArchImage "${RELEASE_VERSION}" + log_callout "Pushing container manifest list to quay.io ${RELEASE_VERSION}" maybe_run docker manifest push "quay.io/coreos/etcd:${RELEASE_VERSION}" diff --git a/scripts/test_images.sh b/scripts/test_images.sh index e5ab24c3bc40..6ad6e81612cf 100755 --- a/scripts/test_images.sh +++ b/scripts/test_images.sh @@ -6,108 +6,69 @@ IFS=$'\n\t' source ./scripts/test_lib.sh source ./scripts/build_lib.sh +source ./scripts/docker_lib.sh -function startContainer { - # run docker in the background - docker run -d --rm --name "${RUN_NAME}" "${IMAGE}" - - # wait for etcd daemon to bootstrap - sleep 5 -} - -function runVersionCheck { - Out=$(docker run --rm "${IMAGE}" "${@}") - foundVersion=$(echo "$Out" | head -1 | rev | cut -d" " -f 1 | rev ) +function runVersionCheck() { + out=$(docker exec "${1}" "${@:2}") + foundVersion=$(echo "${out}" | head -1 | rev | cut -d" " -f 1 | rev ) if [[ "${foundVersion}" != "${VERSION}" ]]; then - echo "error: Invalid Version. Got $foundVersion, expected $VERSION. Error: $Out" + log_error "ERROR: Invalid version. Got ${foundVersion}, expected ${VERSION}. Error: ${out}" exit 1 fi } -function checkImageHasArch() { - IMAGE="${1}" - EXPECTED_ARCH="${2}" - - MANIFEST=$(docker manifest inspect "${IMAGE}") - - # Check that manifest exists - if [ -z "${MANIFEST}" ]; then - echo "ERROR: Manifest not found for image ${IMAGE}" - exit 1 - fi - - PLATFORMS=$(echo "${MANIFEST}" | sed -n 's/.*"platform":{\(.*\)},/\1/p' | tr -d ' ') - - # Check that expected architecture is included - if [[ "${PLATFORMS}" != *"${EXPECTED_ARCH}"* ]]; then - echo "ERROR: Image ${IMAGE} does not contain the expected architecture ${EXPECTED_ARCH}" - exit 1 - fi - - echo "SUCCESS: Image ${IMAGE} contains the expected architecture ${EXPECTED_ARCH}" - return 0 -} - -# Can't proceed without docker -if ! command -v docker >/dev/null; then - log_error "cannot find docker" - exit 1 -fi - -# You can't run darwin binaries in linux containers -if [[ $(go env GOOS) == "darwin" ]]; then - echo "Please use linux machine for release builds." - exit 1 -fi +# Tests can't proceed without docker +checkContainerEngine "docker" # Pick defaults based on release workflow ARCH=$(go env GOARCH) REPOSITORY=${REPOSITORY:-"gcr.io/etcd-development/etcd"} -if [ -n "$VERSION" ]; then +if [ -n "${VERSION}" ]; then # Expected Format: v3.6.99-amd64 TAG=v"${VERSION}"-"${ARCH}" else - echo "Terminating test, VERSION not supplied" + log_error "ERROR: Terminating test, VERSION not supplied" exit 1 fi IMAGE=${IMAGE:-"${REPOSITORY}:${TAG}"} -# ETCD related values -RUN_NAME="test_etcd" +# Etcd related values +CONTAINER_NAME="test_etcd" KEY="foo" VALUE="bar" -if [[ "$(docker images -q "${IMAGE}" 2> /dev/null)" == "" ]]; then - echo "${IMAGE} not present locally" - exit 1 -fi +# Start an etcd container +startContainer "${CONTAINER_NAME}" "${IMAGE}" -# Version check -runVersionCheck "/usr/local/bin/etcd" "--version" -runVersionCheck "/usr/local/bin/etcdctl" "version" -runVersionCheck "/usr/local/bin/etcdutl" "version" +# Test versions in container +runVersionCheck "${CONTAINER_NAME}" "etcd" "--version" +runVersionCheck "${CONTAINER_NAME}" "etcdctl" "version" +runVersionCheck "${CONTAINER_NAME}" "etcdutl" "version" -startContainer -# stop container -trap 'docker stop "${RUN_NAME}"' EXIT - - -# Put/Get check -PUT=$(docker exec "${RUN_NAME}" /usr/local/bin/etcdctl put "${KEY}" "${VALUE}") +# Test etcd kv working +PUT=$(docker exec "${CONTAINER_NAME}" etcdctl put "${KEY}" "${VALUE}") if [ "${PUT}" != "OK" ]; then - echo "Problem with Putting in etcd" + log_error "ERROR: Etcd PUT test failed in container ${CONTAINER_NAME}. Got ${PUT}" exit 1 fi -GET=$(docker exec "${RUN_NAME}" /usr/local/bin/etcdctl get "$KEY" --print-value-only) +GET=$(docker exec "${CONTAINER_NAME}" etcdctl get "${KEY}" --print-value-only) if [ "${GET}" != "${VALUE}" ]; then - echo "Problem with getting foo bar in etcd. Got ${GET}" + log_error "ERROR: Etcd GET test failed in container ${CONTAINER_NAME}. Got ${GET}" exit 1 fi -echo "Succesfully tested etcd local image ${TAG}" +# Test we can assemble the multi arch etcd image +buildMultiArchImage "v${VERSION}" + +# Test local image manifest contains all expected architectures +if [ -n "${IMAGE_ARCH_LIST}" ]; then + for TARGET_ARCH in ${IMAGE_ARCH_LIST}; do + checkImageHasArch "${REPOSITORY}:${VERSION}" "${TARGET_ARCH}" + done +else + log_error "ERROR: IMAGE_ARCH_LIST not populated." + exit 1 +fi -# Test image manifest contains all expected architectures -for TARGET_ARCH in "amd64" "arm64" "ppc64le" "s390x"; do - checkImageHasArch "${REPOSITORY}:${VERSION}" "${TARGET_ARCH}" -done +log_success "SUCCESS: Etcd local image tests for ${IMAGE} passed."