diff --git a/.github/workflows/publish_docker_image.yml b/.github/workflows/publish_docker_image.yml index 2674c429bfd5..77518d98dfc4 100644 --- a/.github/workflows/publish_docker_image.yml +++ b/.github/workflows/publish_docker_image.yml @@ -27,3 +27,9 @@ jobs: DOCKER_PASS: ${{ secrets.docker_pass }} DOCKER_IMAGE: ${{ secrets.docker_repo }} run: scripts/build_image.sh + - name: Build and publish boostrap tester image to DockerHub + env: + DOCKER_USERNAME: ${{ secrets.docker_username }} + DOCKER_PASS: ${{ secrets.docker_pass }} + IMAGE_PREFIX: avaplatform + run: scripts/build_bootstrap_tester_image.sh diff --git a/scripts/build_bootstrap_tester.sh b/scripts/build_bootstrap_tester.sh new file mode 100755 index 000000000000..6978fc1eaa56 --- /dev/null +++ b/scripts/build_bootstrap_tester.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if ! [[ "$0" =~ scripts/build_bootstrap_tester.sh ]]; then + echo "must be run from repository root" + exit 255 +fi + +source ./scripts/constants.sh + +echo "Building bootstrap tester..." +go build -o ./build/bootstrap-tester ./tests/bootstrap/ diff --git a/scripts/build_bootstrap_tester_image.sh b/scripts/build_bootstrap_tester_image.sh new file mode 100755 index 000000000000..d792c8a63543 --- /dev/null +++ b/scripts/build_bootstrap_tester_image.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Directory above this script +AVALANCHE_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd ) + +source ./scripts/constants.sh + +IMAGE_NAME="avalanchego-bootstrap-tester" + +IMAGE_TAG="${IMAGE_TAG:-}" +if [[ -z "${IMAGE_TAG}" ]]; then + # Default to tagging with the commit hash + IMAGE_TAG="${commit_hash}" +fi + +# Build the avalanchego image +./scripts/build_image.sh + +DOCKER_CMD="docker buildx build" + +# Specifying an image prefix will ensure the image is pushed after build +IMAGE_PREFIX="${IMAGE_PREFIX:-}" +if [[ -n "${IMAGE_PREFIX}" ]]; then + IMAGE_NAME="${IMAGE_PREFIX}/${IMAGE_NAME}" + DOCKER_CMD="${DOCKER_CMD} --push" + + # Tag the image as latest for the master branch + if [[ "${image_tag}" == "master" ]]; then + DOCKER_CMD="${DOCKER_CMD} -t ${IMAGE_NAME}:latest" + fi + + # A populated DOCKER_USERNAME env var triggers login + if [[ -n "${DOCKER_USERNAME:-}" ]]; then + echo "$DOCKER_PASS" | docker login --username "$DOCKER_USERNAME" --password-stdin + fi +fi + +# The dockerfiles don't specify the golang version to minimize the changes required to bump +# the version. Instead, the golang version is provided as an argument. +GO_VERSION="$(go list -m -f '{{.GoVersion}}')" + +# Build the image for the bootstrap tester +${DOCKER_CMD} -t "${IMAGE_NAME}:${IMAGE_TAG}" \ + --build-arg GO_VERSION="${GO_VERSION}" --build-arg AVALANCHEGO_NODE_IMAGE="avalanchego:${IMAGE_TAG}" \ + -f "${AVALANCHE_PATH}/tests/bootstrap/Dockerfile" "${AVALANCHE_PATH}" diff --git a/tests/bootstrap/Dockerfile b/tests/bootstrap/Dockerfile new file mode 100644 index 000000000000..da356b16c0bd --- /dev/null +++ b/tests/bootstrap/Dockerfile @@ -0,0 +1,34 @@ +# The version is supplied as a build argument rather than hard-coded +# to minimize the cost of version changes. +ARG GO_VERSION + +# AVALANCHEGO_NODE_IMAGE needs to identify an existing node image and should include the tag +ARG AVALANCHEGO_NODE_IMAGE + +FROM golang:$GO_VERSION-bullseye as builder + +WORKDIR /builder_workdir + +# Copy and download avalanche dependencies using go mod +COPY go.mod . +COPY go.sum . +RUN go mod download + +# Copy the code into the container +COPY . . + +# Ensure pre-existing builds are not available for inclusion in the final image +RUN [ -d ./build ] && rm -rf ./build/* || true + +# Build tester binary +RUN ./scripts/build_bootstrap_tester.sh + +# ============= Cleanup Stage ================ +FROM $AVALANCHEGO_NODE_IMAGE as execution + +COPY --from=builder /builder_workdir/build/bootstrap-tester /avalanchego/build/bootstrap-tester + +# Clear the CMD set by the base image +CMD [ "" ] + +ENTRYPOINT [ "/avalanchego/build/bootstrap-tester" ] diff --git a/tests/bootstrap/main.go b/tests/bootstrap/main.go index c6dee4f6c4b4..cff7338759df 100644 --- a/tests/bootstrap/main.go +++ b/tests/bootstrap/main.go @@ -15,6 +15,7 @@ import ( "github.com/ava-labs/avalanchego/config" "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" + "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/logging" ) @@ -25,6 +26,8 @@ func main() { networkID := flag.Int64("network-id", 0, "The ID of the network to bootstrap from") stateSyncEnabled := flag.Bool("state-sync-enabled", false, "Whether state syncing should be enabled") maxDuration := flag.Duration("max-duration", time.Hour*72, "The maximum duration the network should run for") + dataDir := flag.String("data-dir", "", "The directory to store the node's data") + useDynamicPorts := flag.Bool("use-dynamic-ports", false, "Whether the bootstrapping node should bind to dynamic ports") flag.Parse() @@ -38,25 +41,43 @@ func main() { log.Fatal("max-duration is required") } - if err := checkBootstrap(*avalanchegoPath, uint32(*networkID), *stateSyncEnabled, *maxDuration); err != nil { + if err := checkBootstrap(*dataDir, *avalanchegoPath, uint32(*networkID), *useDynamicPorts, *stateSyncEnabled, *maxDuration); err != nil { log.Fatalf("Failed to check bootstrap: %v\n", err) } } -func checkBootstrap(avalanchegoPath string, networkID uint32, stateSyncEnabled bool, maxDuration time.Duration) error { +func checkBootstrap( + dataDir string, + avalanchegoPath string, + networkID uint32, + useDynamicPorts bool, + stateSyncEnabled bool, + maxDuration time.Duration, +) error { flags := tmpnet.DefaultLocalhostFlags() flags.SetDefaults(tmpnet.FlagsMap{ config.HealthCheckFreqKey: "30s", // Minimize logging overhead config.LogDisplayLevelKey: logging.Off.String(), - config.LogLevelKey: logging.Info.String(), }) + if !useDynamicPorts { + flags.SetDefaults(tmpnet.FlagsMap{ + config.HTTPPortKey: config.DefaultHTTPPort, + config.StakingPortKey: config.DefaultStakingPort, + }) + } + + networkName := constants.NetworkName(networkID) + syncString := "full-sync" + if stateSyncEnabled { + syncString = "state-sync" + } // Create a new single-node network that will bootstrap from the specified network network := &tmpnet.Network{ UUID: uuid.NewString(), NetworkID: networkID, - Owner: "bootstrap-test", + Owner: fmt.Sprintf("bootstrap-test-%s-%s", networkName, syncString), Nodes: tmpnet.NewNodesOrPanic(1), DefaultFlags: flags, DefaultRuntimeConfig: tmpnet.NodeRuntimeConfig{ @@ -70,7 +91,7 @@ func checkBootstrap(avalanchegoPath string, networkID uint32, stateSyncEnabled b }, } - if err := network.Create(""); err != nil { + if err := network.Create(dataDir); err != nil { return fmt.Errorf("failed to create network: %w", err) } node := network.Nodes[0]