diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7c6145e2a61..bc3974cc9647 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -240,3 +240,12 @@ jobs: run: bash -x scripts/tests.build_antithesis_images.sh env: TEST_SETUP: xsvm + e2e_bootstrap_monitor: + name: Run bootstrap monitor e2e tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-go-for-project + - name: Run e2e tests + shell: bash + run: bash -x ./scripts/tests.e2e.bootstrap_monitor.sh diff --git a/.github/workflows/publish_docker_image.yml b/.github/workflows/publish_docker_image.yml index 2674c429bfd5..38648de3d5df 100644 --- a/.github/workflows/publish_docker_image.yml +++ b/.github/workflows/publish_docker_image.yml @@ -26,4 +26,12 @@ jobs: DOCKER_USERNAME: ${{ secrets.docker_username }} DOCKER_PASS: ${{ secrets.docker_pass }} DOCKER_IMAGE: ${{ secrets.docker_repo }} + BUILD_MULTI_ARCH: 1 run: scripts/build_image.sh + - name: Build and publish bootstrap-monitor image to DockerHub + env: + DOCKER_USERNAME: ${{ secrets.docker_username }} + DOCKER_PASS: ${{ secrets.docker_pass }} + DOCKER_IMAGE: avaplatform/bootstrap-monitor + BUILD_MULTI_ARCH: 1 + run: scripts/build_bootstrap_monitor_image.sh diff --git a/Dockerfile b/Dockerfile index f7fffb848b2d..5a6b58f9c194 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,10 +39,11 @@ RUN [ -d ./build ] && rm -rf ./build/* || true # Build avalanchego. The build environment is configured with build_env.sh from the step # enabling cross-compilation. ARG RACE_FLAG="" +ARG BUILD_SCRIPT=build.sh RUN . ./build_env.sh && \ echo "{CC=$CC, TARGETPLATFORM=$TARGETPLATFORM, BUILDPLATFORM=$BUILDPLATFORM}" && \ export GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) && \ - ./scripts/build.sh ${RACE_FLAG} + ./scripts/${BUILD_SCRIPT} ${RACE_FLAG} # Create this directory in the builder to avoid requiring anything to be executed in the # potentially emulated execution container. diff --git a/go.mod b/go.mod index 30476015824e..71b9401f105a 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/rpc v1.2.0 - github.com/gorilla/websocket v1.4.2 + github.com/gorilla/websocket v1.5.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/holiman/uint256 v1.2.4 github.com/huin/goupnp v1.3.0 @@ -69,6 +69,10 @@ require ( google.golang.org/protobuf v1.34.2 gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.29.0 + k8s.io/apimachinery v0.29.0 + k8s.io/client-go v0.29.0 + k8s.io/utils v0.0.0-20230726121419-3b25d923346b ) require ( @@ -96,6 +100,7 @@ require ( github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/ethereum/c-kzg-4844 v0.4.0 // indirect github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 // indirect github.com/frankban/quicktest v1.14.4 // indirect @@ -106,11 +111,16 @@ require ( github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/hashicorp/go-bexpr v0.1.10 // indirect @@ -118,11 +128,15 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.15 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/magiconair/properties v1.8.6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect @@ -130,6 +144,11 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/pointerstructure v1.2.0 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect + github.com/moby/spdystream v0.2.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect @@ -157,11 +176,19 @@ require ( go.opentelemetry.io/otel/metric v1.22.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect + golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.17.0 // indirect + google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect rsc.io/tmplfunc v0.0.3 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index 8b4a35ca4d07..8a8fa37920ab 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,8 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax github.com/antithesishq/antithesis-sdk-go v0.3.8 h1:OvGoHxIcOXFJLyn9IJQ5DzByZ3YVAWNBc394ObzDRb8= github.com/antithesishq/antithesis-sdk-go v0.3.8/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/ava-labs/coreth v0.13.8-fixed-genesis-upgrade.0.20240815193440-a96bc921e732 h1:wlhGJbmb7s3bU2QWtxKjscGjfHknQiq+cVhhUjONsB8= github.com/ava-labs/coreth v0.13.8-fixed-genesis-upgrade.0.20240815193440-a96bc921e732/go.mod h1:RkQLaQ961Xe/sUb3ycn4Qi18vPPuEetTqDf2eDcquAs= github.com/ava-labs/ledger-avalanche/go v0.0.0-20240610153809-9c955cc90a95 h1:dOVbtdnZL++pENdTCNZ1nu41eYDQkTML4sWebDnnq8c= @@ -177,6 +179,8 @@ github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -218,6 +222,7 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -225,6 +230,12 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -279,6 +290,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -328,8 +341,9 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= @@ -356,6 +370,8 @@ github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:q github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -370,9 +386,13 @@ github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7Bd github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= @@ -409,6 +429,8 @@ github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -442,13 +464,22 @@ github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8oh github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= @@ -554,6 +585,7 @@ github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobt github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.1.5-0.20170601210322-f6abca593680/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -565,6 +597,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= @@ -757,6 +790,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -951,6 +986,8 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1038,6 +1075,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -1049,6 +1088,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= @@ -1066,6 +1106,18 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= +k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= +k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= +k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= +k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8= +k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54= launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= @@ -1073,3 +1125,9 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/scripts/build_bootstrap_monitor.sh b/scripts/build_bootstrap_monitor.sh new file mode 100755 index 000000000000..5a4a86c32faf --- /dev/null +++ b/scripts/build_bootstrap_monitor.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Avalanchego root folder +AVALANCHE_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd ) +# Load the constants +source "$AVALANCHE_PATH"/scripts/constants.sh + +echo "Building bootstrap-monitor..." +go build -ldflags\ + "-X github.com/ava-labs/avalanchego/version.GitCommit=$git_commit $static_ld_flags"\ + -o "$AVALANCHE_PATH/build/bootstrap-monitor"\ + "$AVALANCHE_PATH/tests/fixture/bootstrapmonitor/cmd/"*.go diff --git a/scripts/build_bootstrap_monitor_image.sh b/scripts/build_bootstrap_monitor_image.sh new file mode 100755 index 000000000000..5005fdc57283 --- /dev/null +++ b/scripts/build_bootstrap_monitor_image.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# e.g., +# ./scripts/build_bootstrap_monitor_image.sh # Build local image +# DOCKER_IMAGE=my-bootstrap-monitor ./scripts/build_bootstrap_monitor_image.sh # Build local single arch image with a custom image name +# DOCKER_IMAGE=avaplatform/bootstrap-monitor ./scripts/build_bootstrap_monitor_image.sh # Build and push image to docker hub + +# Builds the image for the bootstrap monitor + +# Directory above this script +AVALANCHE_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd ) + +# Load the constants +source "$AVALANCHE_PATH"/scripts/constants.sh + +# The published name should be 'avaplatform/bootstrap-monitor', but to avoid unintentional pushes it +# is defaulted to 'bootstrap-monitor' (without a repo or registry name) which can only be used to +# create local images. +export DOCKER_IMAGE=${DOCKER_IMAGE:-"bootstrap-monitor"} + +# Skip building the race image +export SKIP_BUILD_RACE=1 + +# Reuse the avalanchego build script for convenience. The image will have a CMD of "./avalanchego", so +# to run the bootstrap monitor will need to specify ./bootstrap-monitor". +# +# TODO(marun) Figure out how to set the CMD for a multi-arch image. +bash -x "${AVALANCHE_PATH}"/scripts/build_image.sh --build-arg BUILD_SCRIPT=build_bootstrap_monitor.sh diff --git a/scripts/build_image.sh b/scripts/build_image.sh index acadc3818baf..f2046054499d 100755 --- a/scripts/build_image.sh +++ b/scripts/build_image.sh @@ -3,12 +3,13 @@ set -euo pipefail # e.g., -# ./scripts/build_image.sh # Build local single-arch image -# SKIP_BUILD_RACE=1 ./scripts/build_image.sh # Build local single-arch image but skip building -r image -# DOCKER_IMAGE=myavalanchego ./scripts/build_image.sh # Build local single arch image with a custom image name -# DOCKER_IMAGE=avaplatform/avalanchego ./scripts/build_image.sh # Build and push multi-arch image to docker hub -# DOCKER_IMAGE=localhost:5001/avalanchego ./scripts/build_image.sh # Build and push multi-arch image to private registry -# DOCKER_IMAGE=localhost:5001/myavalanchego ./scripts/build_image.sh # Build and push multi-arch image to private registry with a custom image name +# ./scripts/build_image.sh # Build local single-arch image +# ./scripts/build_image.sh --no-cache # All arguments are provided to `docker buildx build` +# SKIP_BUILD_RACE=1 ./scripts/build_image.sh # Build local single-arch image but skip building -r image +# DOCKER_IMAGE=myavalanchego ./scripts/build_image.sh # Build local single arch image with a custom image name +# DOCKER_IMAGE=avaplatform/avalanchego ./scripts/build_image.sh # Build and push multi-arch image to docker hub +# DOCKER_IMAGE=localhost:5001/avalanchego ./scripts/build_image.sh # Build and push multi-arch image to private registry +# DOCKER_IMAGE=localhost:5001/avalanchego FORCE_TAG_LATEST=1 ./scripts/build_image.sh # Build and push image to private registry with tag `latest` # Multi-arch builds require Docker Buildx and QEMU. buildx should be enabled by # default in the verson of docker included with Ubuntu 22.04, and qemu can be @@ -28,6 +29,8 @@ AVALANCHE_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd ) SKIP_BUILD_RACE="${SKIP_BUILD_RACE:-}" +FORCE_TAG_LATEST="${FORCE_TAG_LATEST:-}" + # Load the constants source "$AVALANCHE_PATH"/scripts/constants.sh @@ -39,13 +42,24 @@ fi # The published name should be 'avaplatform/avalanchego', but to avoid unintentional # pushes it is defaulted to 'avalanchego' (without a repo or registry name) which can # only be used to create local images. -DOCKER_IMAGE=${DOCKER_IMAGE:-"avalanchego"} +DOCKER_IMAGE="${DOCKER_IMAGE:-avalanchego}" + +# If set to non-empty, prompts the building of a multi-arch image when the image +# name indicates use of a registry. +# +# A registry is required to build a multi-arch image since a multi-arch image is +# not really an image at all. A multi-arch image (also called a manifest) is +# basically a list of arch-specific images available from the same registry that +# hosts the manifest. Manifests are not supported for local images. +# +# Reference: https://docs.docker.com/build/building/multi-platform/ +BUILD_MULTI_ARCH="${BUILD_MULTI_ARCH:-}" # buildx (BuildKit) improves the speed and UI of builds over the legacy builder and # simplifies creation of multi-arch images. # # Reference: https://docs.docker.com/build/buildkit/ -DOCKER_CMD="docker buildx build" +DOCKER_CMD="docker buildx build ${*}" # The dockerfile doesn't specify the golang version to minimize the # changes required to bump the version. Instead, the golang version is @@ -54,20 +68,17 @@ GO_VERSION="$(go list -m -f '{{.GoVersion}}')" DOCKER_CMD="${DOCKER_CMD} --build-arg GO_VERSION=${GO_VERSION}" if [[ "${DOCKER_IMAGE}" == *"/"* ]]; then - # Build a multi-arch image since the image name includes a slash which indicates - # the use of a registry e.g. + # Default to pushing when the image name includes a slash which indicates the + # use of a registry e.g. # # - dockerhub: [repo]/[image name]:[tag] # - private registry: [private registry hostname]/[image name]:[tag] - # - # A registry is required to build a multi-arch image since a multi-arch image is - # not really an image at all. A multi-arch image (also called a manifest) is - # basically a list of arch-specific images available from the same registry that - # hosts the manifest. Manifests are not supported for local images. - # - # Reference: https://docs.docker.com/build/building/multi-platform/ - PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}" - DOCKER_CMD="${DOCKER_CMD} --push --platform=${PLATFORMS}" + DOCKER_CMD="${DOCKER_CMD} --push" + + # Build a multi-arch image if requested + if [[ -n "${BUILD_MULTI_ARCH}" ]]; then + DOCKER_CMD="${DOCKER_CMD} --platform=${PLATFORMS:-linux/amd64,linux/arm64}" + fi # A populated DOCKER_USERNAME env var triggers login if [[ -n "${DOCKER_USERNAME:-}" ]]; then @@ -94,7 +105,7 @@ if [[ -z "${SKIP_BUILD_RACE}" ]]; then fi # Only tag the latest image for the master branch when images are pushed to a registry -if [[ "${DOCKER_IMAGE}" == *"/"* && $image_tag == "master" ]]; then +if [[ "${DOCKER_IMAGE}" == *"/"* && ($image_tag == "master" || -n "${FORCE_TAG_LATEST}") ]]; then echo "Tagging current avalanchego images as $DOCKER_IMAGE:latest" docker buildx imagetools create -t "$DOCKER_IMAGE:latest" "$DOCKER_IMAGE:$commit_hash" fi diff --git a/scripts/build_test.sh b/scripts/build_test.sh index 4a7cbd04f746..1511c351782a 100755 --- a/scripts/build_test.sh +++ b/scripts/build_test.sh @@ -7,7 +7,7 @@ AVALANCHE_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd ) # Load the constants source "$AVALANCHE_PATH"/scripts/constants.sh -EXCLUDED_TARGETS="| grep -v /mocks | grep -v proto | grep -v tests/e2e | grep -v tests/upgrade" +EXCLUDED_TARGETS="| grep -v /mocks | grep -v proto | grep -v tests/e2e | grep -v tests/upgrade | grep -v tests/fixture/bootstrapmonitor/e2e" if [[ "$(go env GOOS)" == "windows" ]]; then # Test discovery for the antithesis test setups is broken due to diff --git a/scripts/tests.build_image.sh b/scripts/tests.build_image.sh index a383b3190f96..f4dd81754165 100755 --- a/scripts/tests.build_image.sh +++ b/scripts/tests.build_image.sh @@ -16,7 +16,7 @@ source "$AVALANCHE_PATH"/scripts/constants.sh build_and_test() { local image_name=$1 - DOCKER_IMAGE="$image_name" ./scripts/build_image.sh + BUILD_MULTI_ARCH=1 DOCKER_IMAGE="$image_name" ./scripts/build_image.sh echo "listing images" docker images diff --git a/scripts/tests.e2e.bootstrap_monitor.sh b/scripts/tests.e2e.bootstrap_monitor.sh new file mode 100755 index 000000000000..2ff4566a5d8b --- /dev/null +++ b/scripts/tests.e2e.bootstrap_monitor.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Run e2e tests for bootstrap monitor. + +if ! [[ "$0" =~ scripts/tests.e2e.bootstrap_monitor.sh ]]; then + echo "must be run from repository root" + exit 255 +fi + +# Determine DIST and ARCH in case installation is required for kubectl and kind +# +# TODO(marun) Factor this out for reuse +if which sw_vers &> /dev/null; then + OS="darwin" + ARCH="$(uname -m)" +else + # Assume linux (windows is not supported) + OS="linux" + RAW_ARCH="$(uname -i)" + # Convert the linux arch string to the string used for k8s releases + if [[ "${RAW_ARCH}" == "aarch64" ]]; then + ARCH="arm64" + elif [[ "${RAW_ARCH}" == "x86_64" ]]; then + ARCH="amd64" + else + echo "Unsupported architecture: ${RAW_ARCH}" + exit 1 + fi +fi + +function ensure_command { + local cmd=$1 + local install_uri=$2 + + if ! command -v "${cmd}" &> /dev/null; then + # Try to use a local version + local local_cmd="${PWD}/bin/${cmd}" + mkdir -p "${PWD}/bin" + if ! command -v "${local_cmd}" &> /dev/null; then + echo "${cmd} not found, attempting to install..." + curl -L -o "${local_cmd}" "${install_uri}" + # TODO(marun) Optionally validate the binary against published checksum + chmod +x "${local_cmd}" + fi + fi +} + +# Ensure the kubectl command is available +KUBECTL_VERSION=v1.30.2 +ensure_command kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/${OS}/${ARCH}/kubectl" + +# Ensure the kind command is available +KIND_VERSION=v0.23.0 +ensure_command kind "https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-${OS}-${ARCH}" + +# Ensure the kind-with-registry command is available +ensure_command "kind-with-registry.sh" "https://raw.githubusercontent.com/kubernetes-sigs/kind/7cb9e6be25b48a0e248097eef29d496ab1a044d0/site/static/examples/kind-with-registry.sh" + +# Deploy a kind cluster with a local registry. Include the local bin in the path to +# ensure locally installed kind and kubectl are available since the script expects to +# call them without a qualifying path. +PATH="${PWD}/bin:$PATH" bash -x "${PWD}/bin/kind-with-registry.sh" + +# TODO(marun) Factor out ginkgo installation to avoid duplicating it across test scripts +go install -v github.com/onsi/ginkgo/v2/ginkgo@v2.13.1 + +KUBECONFIG="$HOME/.kube/config" PATH="${PWD}/bin:$PATH" ginkgo -vv ./tests/fixture/bootstrapmonitor/e2e diff --git a/tests/fixture/bootstrapmonitor/README.md b/tests/fixture/bootstrapmonitor/README.md new file mode 100644 index 000000000000..231c68d85a60 --- /dev/null +++ b/tests/fixture/bootstrapmonitor/README.md @@ -0,0 +1,146 @@ +# Bootstrap testing + +Bootstrapping an avalanchego node on a persistent network like mainnet +or fuji requires that the version of avalanchego that the node is +running be compatible with the historical data of that +network. Running this test regularly is a good way of insuring against +regressions in compatibility. + + +### Full Sync + +### Pruning + +### State Sync + +## Architecture + +### Controller + + - watches for deployments that manage bootstrap testing + - if such a deployment is using `latest` image tags + - get the version currently associated with `latest` (by running a pod) + - update the image tags for the pod, which will prompt a restart to use the new tag + - if such a deployment is using specific tags + - check its health + - require a service with the same name as the deployment + - use an informer with a resync every 5 minutes? + - i.e. only check health every 5 minutes + - if healthy, get the latest version (by running a pod) + - if the version is not different from the running version, do nothing + - if the version is different, update the tags for the deploymnet + - this will prompt a redeployment + - maybe cache the version to avoid running too many pods? + - actually, the check for a new version should be cheap. So every 5 minutes should be fine. + +### Monitor + + - every [interval] + - if node running on localhost port is healthy + - get the image digest of the image that the node is running as + - get image digest avalanchego image currently tagged `latest` + - if the 2 digests differ + - trigger a new bootstrap test by updating the image for the deployment managing the pod + + - every [interval] + - uses curl to check whether an avalanche node running on localhost is healthy + - if the node is healthy + - use kubectl to wait for a pod using image avaplatform/avalanchego:latest to complete + - use kubectl to retrieve the image digest from the terminated pod + - compose the expected image name using the image digest i.e. `avaplatform/avalanchego:[image digest of terminated pod]` + - use kubectl to retrieve the image for the 'node' container in the same pod as the bash script + - if the image for the node container does not match the expected image name + - use kubectl to discover the name of the deployment that is managing the pod the script is running in + - update the 'node' container of the deployment to use the expected image name + + +### Bootstrap pods + +### ArgoCD + +- will need to ignore differences to the image tags of the deployments + +#### Containers + + - init container + - uses avalanchego image + - mounts /data + - initializes the /data path by checking the version against one that was saved +```bash +version_path="/data/bootstrap_version.json" + +latest_version=$(/avalanchego/build/avalanchego --version-json) + +if [ -f "${version_path}" ] && diff <(echo "${latest_version}") "${version_path}"; then + echo "Resuming bootstrap for ${latest_version}" + exit 0 +fi + +echo "Starting bootstrap for ${latest_version}" + +echo "Recording version" +echo "${latest_version}" > "${version_path}" + +echo "Clearing Recording version" +rm -rf /data/node/* + +# Ensure the node path exists +mkdir /data/node +``` + - avalanche container + + +## Alternatives considered + +#### self-hosteed github workers + + - allow triggering / reporting to happen with github + - but 5 day limit on job duration wouldn't probably wouldn't support full-sync + +#### Adding a 'bootstrap mode' to avalanchego + - with a --bootstrap-mode flag, exit on successful bootstrap + - but using it without a controller would require using `latest` to + ensure that the node version could change on restarts + - but when using `latest` there is no way to avoid having pod + restart preventing the completion of an in-process bootstrap + test. Only by using a specific image tag will it be possible for + a restarted pod to reliably resume a bootstrap test. + + +### Primary Requirement + + - Run full sync and state sync bootstrap tests against mainnet and testnet + +### Secondary requiremnts + + - Run tests in infra-managed kubernetes + - Ensures sufficient resources (~2tb required per test) + - Ensures metrics and logs will be collected by Datadog + - Ensure that no more than one test will be run against a given image + - Ensure that an in-process bootstrap test can be resumed if a pod is restarted + +### TODO + - accepts bootstrap configurations + - network-id + - sync-mode + - kube configuration + - in-cluster or not + - start a wait loop (via kubernetes) + - check image version + - for each bootstrap config + - if config.running - + - continue + - if config.last_version != version + - start a new test run + - pass a + + +StartJob(clientset, namespace, imageName, pvcSize, flags) + - starts node + - periodically checks if node pod is running and node is healthy + - errors out on timeout + - log result (use zap) + - on healthy + - log result + - update last version + - update the config to ' diff --git a/tests/fixture/bootstrapmonitor/cmd/main.go b/tests/fixture/bootstrapmonitor/cmd/main.go new file mode 100644 index 000000000000..e20bc7643307 --- /dev/null +++ b/tests/fixture/bootstrapmonitor/cmd/main.go @@ -0,0 +1,103 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "errors" + "fmt" + "os" + "time" + + "github.com/spf13/cobra" + + "github.com/ava-labs/avalanchego/tests/fixture/bootstrapmonitor" + "github.com/ava-labs/avalanchego/version" +) + +const ( + cliVersion = "0.0.1" + commandName = "bootstrap-monitor" + + defaultPollInterval = 60 * time.Second +) + +func main() { + var namespace string + var podName string + var nodeContainerName string + rootCmd := &cobra.Command{ + Use: commandName, + Short: commandName + " commands", + } + rootCmd.PersistentFlags().StringVar(&namespace, "namespace", os.Getenv("POD_NAMESPACE"), "The namespace of the pod") + rootCmd.PersistentFlags().StringVar(&podName, "pod-name", os.Getenv("POD_NAME"), "The name of the pod") + rootCmd.PersistentFlags().StringVar(&nodeContainerName, "node-container-name", "", "The name of the node container in the pod") + + versionCmd := &cobra.Command{ + Use: "version", + Short: "Print version details", + RunE: func(*cobra.Command, []string) error { + msg := cliVersion + if len(version.GitCommit) > 0 { + msg += ", commit=" + version.GitCommit + } + fmt.Fprintf(os.Stdout, msg+"\n") + return nil + }, + } + rootCmd.AddCommand(versionCmd) + + var dataDir string + initCmd := &cobra.Command{ + Use: "init", + Short: "Initialize the data path of athe Start a new temporary network", + RunE: func(*cobra.Command, []string) error { + if err := checkArgs(namespace, podName, nodeContainerName); err != nil { + return err + } + if len(dataDir) == 0 { + return errors.New("data-dir is required") + } + return bootstrapmonitor.InitBootstrapTest(namespace, podName, nodeContainerName, dataDir) + }, + } + initCmd.PersistentFlags().StringVar(&dataDir, "data-dir", "", "The path of the data directory used for the bootstrap job") + rootCmd.AddCommand(initCmd) + + var pollInterval time.Duration + waitCmd := &cobra.Command{ + Use: "wait-for-completion", + Short: "Wait for the local node to report healthy indicating completion of bootstrapping", + RunE: func(*cobra.Command, []string) error { + if err := checkArgs(namespace, podName, nodeContainerName); err != nil { + return err + } + if pollInterval <= 0 { + return errors.New("poll-interval must be greater than 0") + } + return bootstrapmonitor.WaitForCompletion(namespace, podName, nodeContainerName, pollInterval) + }, + } + waitCmd.PersistentFlags().DurationVar(&pollInterval, "poll-interval", defaultPollInterval, "The interval at which to poll") + rootCmd.AddCommand(waitCmd) + + if err := rootCmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "%s failed: %v\n", commandName, err) + os.Exit(1) + } + os.Exit(0) +} + +func checkArgs(namespace string, podName string, nodeContainerName string) error { + if len(namespace) == 0 { + return errors.New("namespace is required") + } + if len(podName) == 0 { + return errors.New("pod-name is required") + } + if len(nodeContainerName) == 0 { + return errors.New("node-container-name is required") + } + return nil +} diff --git a/tests/fixture/bootstrapmonitor/common.go b/tests/fixture/bootstrapmonitor/common.go new file mode 100644 index 000000000000..4a71e779a77f --- /dev/null +++ b/tests/fixture/bootstrapmonitor/common.go @@ -0,0 +1,216 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrapmonitor + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "os" + "strings" + + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func WaitForPodStatus( + ctx context.Context, + clientset *kubernetes.Clientset, + namespace string, + name string, + acceptable func(*corev1.PodStatus) bool, +) error { + watch, err := clientset.CoreV1().Pods(namespace).Watch(ctx, metav1.SingleObject(metav1.ObjectMeta{Name: name})) + if err != nil { + return fmt.Errorf("failed to initiate watch of pod %s/%s: %w", namespace, name, err) + } + + for { + select { + case event := <-watch.ResultChan(): + pod, ok := event.Object.(*corev1.Pod) + if !ok { + continue + } + + if acceptable(&pod.Status) { + return nil + } + case <-ctx.Done(): + return errors.New("timeout waiting for pod readiness") + } + } +} + +// getContainerImage retrieves the image of the specified container in the specified pod +func GetContainerImage(context context.Context, clientset *kubernetes.Clientset, namespace string, podName string, containerName string) (string, error) { + pod, err := clientset.CoreV1().Pods(namespace).Get(context, podName, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + return "", nil + } else if err != nil { + return "", fmt.Errorf("failed to get pod %s.%s: %w", namespace, podName, err) + } + for _, container := range pod.Spec.Containers { + if container.Name == containerName { + return container.Image, nil + } + } + return "", fmt.Errorf("failed to find container %q in pod %s.%s", containerName, namespace, podName) +} + +// setContainerImage sets the image of the specified container of the pod's owning statefulset +func setContainerImage(ctx context.Context, clientset *kubernetes.Clientset, namespace string, podName string, containerName string, image string) error { + // Determine the name of the statefulset to update + pod, err := clientset.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get pod %s: %w", podName, err) + } + if len(pod.OwnerReferences) == 0 { + return errors.New("pod has no owner references") + } + statefulSetName := pod.OwnerReferences[0].Name + + // Define the strategic merge patch data updating the image + patchData := map[string]interface{}{ + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []map[string]interface{}{ + { + "name": containerName, + "image": image, + }, + }, + }, + }, + }, + } + + // Convert patch data to JSON + patchBytes, err := json.Marshal(patchData) + if err != nil { + return fmt.Errorf("failed to marshal patch data: %w", err) + } + + // Apply the patch + _, err = clientset.AppsV1().StatefulSets(namespace).Patch(context.TODO(), statefulSetName, types.StrategicMergePatchType, patchBytes, metav1.PatchOptions{}) + if err != nil { + return fmt.Errorf("failed to patch statefulset %s: %w", statefulSetName, err) + } + log.Printf("Updated statefulset %s.%s to target image %q", namespace, statefulSetName, image) + + return nil +} + +// getBaseImageName removes the tag from the image name +func getBaseImageName(imageName string) (string, error) { + if strings.Contains(imageName, "@") { + // Image name contains a digest, remove it + return strings.Split(imageName, "@")[0], nil + } + + imageNameParts := strings.Split(imageName, ":") + switch len(imageNameParts) { + case 1: + // No tag or registry + return imageName, nil + case 2: + // Ambiguous image name - could contain a tag or a registry + log.Printf("Derived image name of %q from %q", imageNameParts[0], imageName) + return imageNameParts[0], nil + case 3: + // Image name contains a registry and a tag - remove the tag + return strings.Join(imageNameParts[0:2], ":"), nil + default: + return "", fmt.Errorf("unexpected image name format: %q", imageName) + } +} + +// getLatestImageID retrieves the image id for the avalanchego image with tag `latest`. +func getLatestImageID( + ctx context.Context, + clientset *kubernetes.Clientset, + namespace string, + imageName string, + containerName string, +) (string, error) { + baseImageName, err := getBaseImageName(imageName) + if err != nil { + return "", err + } + + // Start a new pod with the `latest`-tagged avalanchego image to discover its image ID + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "avalanchego-version-check-", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: containerName, + Command: []string{"./avalanchego"}, + Args: []string{"--version"}, + Image: baseImageName + ":latest", + }, + }, + RestartPolicy: corev1.RestartPolicyNever, + }, + } + createdPod, err := clientset.CoreV1().Pods(namespace).Create(ctx, pod, metav1.CreateOptions{}) + if err != nil { + return "", fmt.Errorf("failed to start pod %w", err) + } + + err = WaitForPodStatus(ctx, clientset, namespace, createdPod.Name, func(status *corev1.PodStatus) bool { + return status.Phase == corev1.PodSucceeded || status.Phase == corev1.PodFailed + }) + if err != nil { + return "", fmt.Errorf("failed to wait for pod termination: %w", err) + } + + terminatedPod, err := clientset.CoreV1().Pods(namespace).Get(ctx, createdPod.Name, metav1.GetOptions{}) + if err != nil { + return "", fmt.Errorf("failed to load terminated pod: %w", err) + } + + // Get the image id for the avalanchego image + imageID := "" + for _, status := range terminatedPod.Status.ContainerStatuses { + if status.Name == containerName { + imageID = status.ImageID + break + } + } + if len(imageID) == 0 { + return "", fmt.Errorf("failed to get image id for pod %s.%s", namespace, createdPod.Name) + } + + // Only delete the pod if successful to aid in debugging + err = clientset.CoreV1().Pods(namespace).Delete(ctx, createdPod.Name, metav1.DeleteOptions{}) + if err != nil { + return "", err + } + + return imageID, nil +} + +func getClientset() (*kubernetes.Clientset, error) { + kubeconfigPath := os.Getenv("KUBECONFIG") + kubeConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath) + if err != nil { + return nil, fmt.Errorf("failed to build kubeconfig: %w", err) + } + clientset, err := kubernetes.NewForConfig(kubeConfig) + if err != nil { + return nil, fmt.Errorf("failed to create clientset: %w", err) + } + return clientset, nil +} diff --git a/tests/fixture/bootstrapmonitor/e2e/e2e_test.go b/tests/fixture/bootstrapmonitor/e2e/e2e_test.go new file mode 100644 index 000000000000..96a8816a84a6 --- /dev/null +++ b/tests/fixture/bootstrapmonitor/e2e/e2e_test.go @@ -0,0 +1,612 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package e2e + +import ( + "flag" + "fmt" + "io" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/onsi/ginkgo/v2" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/portforward" + "k8s.io/client-go/transport/spdy" + "k8s.io/utils/pointer" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/config" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/tests" + "github.com/ava-labs/avalanchego/tests/fixture/bootstrapmonitor" + "github.com/ava-labs/avalanchego/tests/fixture/e2e" + "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/logging" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + restclient "k8s.io/client-go/rest" +) + +func TestE2E(t *testing.T) { + ginkgo.RunSpecs(t, "bootstrap test suite") +} + +const ( + nodeImage = "localhost:5001/avalanchego" + latestNodeImage = nodeImage + ":latest" + monitorImage = "localhost:5001/bootstrap-monitor" + latestMonitorImage = monitorImage + ":latest" + + initContainerName = "init" + monitorContainerName = "monitor" + nodeContainerName = "node" + + volumeSize = "128Mi" + volumeName = "data" + + dataDir = "/data" + nodeDataDir = dataDir + "/node" // Use a subdirectory of the data path so that os.RemoveAll can be used when starting a new test +) + +var ( + skipNodeImageBuild bool + skipMonitorImageBuild bool +) + +func init() { + flag.BoolVar( + &skipNodeImageBuild, + "skip-node-image-build", + false, + "whether to skip building the avalanchego image", + ) + flag.BoolVar( + &skipMonitorImageBuild, + "skip-monitor-image-build", + false, + "whether to skip building the bootstrap-monitor image", + ) +} + +var _ = ginkgo.Describe("[Bootstrap Tester]", func() { + const () + + ginkgo.It("should support continuous testing of node bootstrap", func() { + tc := e2e.NewTestContext() + require := require.New(tc) + + if skipNodeImageBuild { + tc.Outf("{{yellow}}skipping build of avalanchego image{{/}}\n") + } else { + ginkgo.By("Building the avalanchego image") + buildNodeImage(tc, nodeImage, false /* forceNewHash */) + } + + if skipMonitorImageBuild { + tc.Outf("{{yellow}}skipping build of bootstrap-monitor image{{/}}\n") + } else { + ginkgo.By("Building the bootstrap-monitor image") + buildImage(tc, monitorImage, false /* forceNewHash */, "build_bootstrap_monitor_image.sh") + } + + ginkgo.By("Configuring a kubernetes client") + kubeconfigPath := os.Getenv("KUBECONFIG") + kubeConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath) + require.NoError(err) + clientset, err := kubernetes.NewForConfig(kubeConfig) + require.NoError(err) + + // TODO(marun) Consider optionally deleting the namespace after the test + ginkgo.By("Creating a kube namespace to ensure isolation between test runs") + createdNamespace, err := clientset.CoreV1().Namespaces().Create(tc.DefaultContext(), &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "bootstrap-test-e2e-", + }, + }, metav1.CreateOptions{}) + require.NoError(err) + namespace := createdNamespace.Name + + ginkgo.By("Creating a node to bootstrap from") + nodeStatefulSet := createNode(tc, clientset, namespace) + nodePodName := nodeStatefulSet.Name + "-0" + waitForPodCondition(tc, clientset, namespace, nodePodName, corev1.PodReady) + bootstrapID := waitForNodeHealthy(tc, kubeConfig, namespace, nodePodName) + pod, err := clientset.CoreV1().Pods(namespace).Get(tc.DefaultContext(), nodePodName, metav1.GetOptions{}) + require.NoError(err) + bootstrapIP := pod.Status.PodIP + + ginkgo.By("Creating a node that will bootstrap from the first node") + bootstrapStatefulSet := createBootstrapTester(tc, clientset, namespace, bootstrapIP, bootstrapID) + bootstrapPodName := bootstrapStatefulSet.Name + "-0" + + ginkgo.By("Waiting for the pod image to be updated to include an image digest") + var containerImage string + require.Eventually(func() bool { + image, err := bootstrapmonitor.GetContainerImage(tc.DefaultContext(), clientset, namespace, bootstrapPodName, nodeContainerName) + if err != nil { + tc.Outf("Error determining image used by the %q container of pod %s.%s: %v \n", nodeContainerName, namespace, bootstrapPodName, err) + return false + } + if strings.Contains(image, "sha256") { + containerImage = image + return true + } + return false + }, e2e.DefaultTimeout, e2e.DefaultPollingInterval) + + ginkgo.By("Waiting for the init container to report the start of a bootstrap test") + waitForPodCondition(tc, clientset, namespace, bootstrapPodName, corev1.PodInitialized) + bootstrapStartingMessage := bootstrapmonitor.BootstrapStartingMessage(containerImage) + waitForLogOutput(tc, clientset, namespace, bootstrapPodName, initContainerName, bootstrapStartingMessage) + + ginkgo.By("Waiting for the pod to report readiness") + waitForPodCondition(tc, clientset, namespace, bootstrapPodName, corev1.PodReady) + + ginkgo.By("Waiting for the monitor container to report the success of the bootstrap test") + bootstrapSucceededMessage := bootstrapmonitor.BootstrapSucceededMessage(containerImage) + waitForLogOutput(tc, clientset, namespace, bootstrapPodName, monitorContainerName, bootstrapSucceededMessage) + _ = waitForNodeHealthy(tc, kubeConfig, namespace, nodePodName) + + ginkgo.By("Checking that bootstrap testing is resumed when a pod is rescheduled") + // Retrieve the UID of the pod pre-deletion + pod, err = clientset.CoreV1().Pods(namespace).Get(tc.DefaultContext(), bootstrapPodName, metav1.GetOptions{}) + require.NoError(err) + podUID := pod.UID + require.NoError(clientset.CoreV1().Pods(namespace).Delete(tc.DefaultContext(), bootstrapPodName, metav1.DeleteOptions{})) + // Wait for the pod to be recreated with a new UID + require.Eventually(func() bool { + pod, err := clientset.CoreV1().Pods(namespace).Get(tc.DefaultContext(), bootstrapPodName, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + return false + } + if err != nil { + tc.Outf("Error getting pod %s.%s: %v\n", namespace, bootstrapPodName, err) + return false + } + return pod.UID != podUID + }, e2e.DefaultTimeout, e2e.DefaultPollingInterval) + waitForPodCondition(tc, clientset, namespace, bootstrapPodName, corev1.PodInitialized) + bootstrapResumingMessage := bootstrapmonitor.BootstrapResumingMessage(containerImage) + waitForLogOutput(tc, clientset, namespace, bootstrapPodName, initContainerName, bootstrapResumingMessage) + + ginkgo.By("Building and pushing a new avalanchego image to prompt the start of a new bootstrap test") + buildNodeImage(tc, nodeImage, true /* forceNewHash */) + + ginkgo.By("Waiting for the pod image to change") + require.Eventually(func() bool { + image, err := bootstrapmonitor.GetContainerImage(tc.DefaultContext(), clientset, namespace, bootstrapPodName, nodeContainerName) + if err != nil { + tc.Outf("Error determining image used by the %q container of pod %s.%s: %v \n", nodeContainerName, namespace, bootstrapPodName, err) + return false + } + if len(image) > 0 && image != containerImage { + containerImage = image + return true + } + return false + }, e2e.DefaultTimeout, e2e.DefaultPollingInterval) + + ginkgo.By("Waiting for the init container to report the start of a new bootstrap test") + waitForPodCondition(tc, clientset, namespace, bootstrapPodName, corev1.PodInitialized) + bootstrapStartingMessage = bootstrapmonitor.BootstrapStartingMessage(containerImage) + waitForLogOutput(tc, clientset, namespace, bootstrapPodName, initContainerName, bootstrapStartingMessage) + }) +}) + +func buildNodeImage(tc tests.TestContext, imageName string, forceNewHash bool) { + buildImage(tc, imageName, forceNewHash, "build_image.sh") +} + +func buildImage(tc tests.TestContext, imageName string, forceNewHash bool, scriptName string) { + require := require.New(tc) + + relativePath := "tests/fixture/bootstrapmonitor/e2e" + repoRoot, err := getRepoRootPath(relativePath) + require.NoError(err) + + var args []string + if forceNewHash { + // Ensure the build results in a new image hash by preventing use of a cached final stage + args = append(args, "--no-cache-filter", "execution") + } + + cmd := exec.CommandContext( + tc.DefaultContext(), + filepath.Join(repoRoot, "scripts", scriptName), + args..., + ) // #nosec G204 + cmd.Env = append(os.Environ(), + "DOCKER_IMAGE="+imageName, + "FORCE_TAG_LATEST=1", + "SKIP_BUILD_RACE=1", + ) + output, err := cmd.CombinedOutput() + if err != nil { + require.FailNow("Image build failed: %v\nWith output: %s", err, output) + } +} + +func createNode(tc tests.TestContext, clientset kubernetes.Interface, namespace string) *appsv1.StatefulSet { + statefulSet := newNodeStatefulSet("avalanchego-node", defaultNodeFlags()) + createdStatefulSet, err := clientset.AppsV1().StatefulSets(namespace).Create(tc.DefaultContext(), statefulSet, metav1.CreateOptions{}) + require.NoError(tc, err) + return createdStatefulSet +} + +func createBootstrapTester(tc tests.TestContext, clientset *kubernetes.Clientset, namespace string, bootstrapIP string, bootstrapNodeID ids.NodeID) *appsv1.StatefulSet { + flags := defaultNodeFlags() + flags[config.BootstrapIPsKey] = fmt.Sprintf("%s:%d", bootstrapIP, config.DefaultStakingPort) + flags[config.BootstrapIDsKey] = bootstrapNodeID.String() + + statefulSet := newNodeStatefulSet("bootstrap-tester", flags) + + // Add the monitor containers to enable continuous bootstrap testing + initContainer := getMonitorContainer(initContainerName, []string{ + "init", + "--node-container-name=" + nodeContainerName, + "--data-dir=" + dataDir, + }) + initContainer.VolumeMounts = []corev1.VolumeMount{ + { + Name: volumeName, + MountPath: dataDir, + }, + } + statefulSet.Spec.Template.Spec.InitContainers = append(statefulSet.Spec.Template.Spec.InitContainers, initContainer) + monitorContainer := getMonitorContainer(monitorContainerName, []string{ + "wait-for-completion", + "--node-container-name=" + nodeContainerName, + "--poll-interval=1s", + }) + statefulSet.Spec.Template.Spec.Containers = append(statefulSet.Spec.Template.Spec.Containers, monitorContainer) + + grantMonitorPermissions(tc, clientset, namespace) + + createdStatefulSet, err := clientset.AppsV1().StatefulSets(namespace).Create(tc.DefaultContext(), statefulSet, metav1.CreateOptions{}) + require.NoError(tc, err) + + return createdStatefulSet +} + +func getMonitorContainer(name string, args []string) corev1.Container { + return corev1.Container{ + Name: name, + Image: latestMonitorImage, + Command: []string{"./bootstrap-monitor"}, + Args: args, + Env: []corev1.EnvVar{ + { + Name: "POD_NAME", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.name", + }, + }, + }, + { + Name: "POD_NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, + }, + } +} + +// grantMonitorPermissions grants the permissions required by the bootstrap monitor to the namespace's default service account. +func grantMonitorPermissions(tc tests.TestContext, clientset *kubernetes.Clientset, namespace string) { + require := require.New(tc) + + role := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "bootstrap-monitor-role-", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"pods"}, + Verbs: []string{"get", "create", "watch", "delete"}, + }, + { + APIGroups: []string{"apps"}, + Resources: []string{"statefulsets"}, + Verbs: []string{"patch"}, + }, + }, + } + createdRole, err := clientset.RbacV1().Roles(namespace).Create(tc.DefaultContext(), role, metav1.CreateOptions{}) + require.NoError(err) + + roleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "bootstrap-monitor-role-binding-", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: "default", + Namespace: namespace, + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "Role", + Name: createdRole.Name, + APIGroup: "rbac.authorization.k8s.io", + }, + } + _, err = clientset.RbacV1().RoleBindings(namespace).Create(tc.DefaultContext(), roleBinding, metav1.CreateOptions{}) + require.NoError(err) +} + +func newNodeStatefulSet(name string, flags map[string]string) *appsv1.StatefulSet { + return &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: name, + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: pointer.Int32(1), + ServiceName: name, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": name, + }, + }, + VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: volumeName, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse(volumeSize), + }, + }, + }, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": name, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: nodeContainerName, + Image: latestNodeImage, + Ports: []corev1.ContainerPort{ + { + Name: "http", + ContainerPort: config.DefaultHTTPPort, + }, + { + Name: "staker", + ContainerPort: config.DefaultStakingPort, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: volumeName, + MountPath: nodeDataDir, + }, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/ext/health", + Port: intstr.FromInt(config.DefaultHTTPPort), + }, + }, + PeriodSeconds: 1, + SuccessThreshold: 1, + }, + Env: StringMapToEnvVarSlice(flags), + }, + }, + }, + }, + }, + } +} + +func getRepoRootPath(suffix string) (string, error) { + // - When executed via a test binary, the working directory will be wherever + // the binary is executed from, but scripts should require execution from + // the repo root. + // + // - When executed via ginkgo (nicer for development + supports + // parallel execution) the working directory will always be the + // target path (e.g. [repo root]./tests/bootstrap/e2e) and getting the repo + // root will require stripping the target path suffix. + cwd, err := os.Getwd() + if err != nil { + return "", err + } + return strings.TrimSuffix(cwd, suffix), nil +} + +func StringMapToEnvVarSlice(mapping map[string]string) []corev1.EnvVar { + envVars := make([]corev1.EnvVar, len(mapping)) + var i int + for k, v := range mapping { + envVars[i] = corev1.EnvVar{ + Name: envVarName(config.EnvPrefix, k), + Value: v, + } + i++ + } + return envVars +} + +// TODO(marun) Share one implementation with antithesis configuration +func envVarName(prefix string, key string) string { + // e.g. MY_PREFIX, network-id -> MY_PREFIX_NETWORK_ID + return strings.ToUpper(prefix + "_" + config.DashesToUnderscores.Replace(key)) +} + +func getContainerLogs(tc tests.TestContext, clientset kubernetes.Interface, namespace string, podName string, containerName string) (string, error) { + // Request the logs + req := clientset.CoreV1().Pods(namespace).GetLogs(podName, &corev1.PodLogOptions{ + Container: containerName, + }) + + // Stream the logs + readCloser, err := req.Stream(tc.DefaultContext()) + if err != nil { + return "", err + } + defer readCloser.Close() + + // Marshal the logs into the versions type + bytes, err := io.ReadAll(readCloser) + if err != nil { + return "", err + } + + return string(bytes), nil +} + +func waitForPodCondition(tc tests.TestContext, clientset *kubernetes.Clientset, namespace string, podName string, conditionType corev1.PodConditionType) { + require.NoError(tc, bootstrapmonitor.WaitForPodStatus( + tc.DefaultContext(), + clientset, + namespace, + podName, + func(status *corev1.PodStatus) bool { + for _, condition := range status.Conditions { + if condition.Type == conditionType && condition.Status == corev1.ConditionTrue { + return true + } + } + return false + }, + )) +} + +func waitForNodeHealthy(tc tests.TestContext, kubeConfig *restclient.Config, namespace string, podName string) ids.NodeID { + require := require.New(tc) + + ginkgo.By(fmt.Sprintf("Enabling a local forward for pod %s.%s", namespace, podName)) + localPort, localPortStopChan, err := EnableLocalForwardForPod(kubeConfig, namespace, podName, config.DefaultHTTPPort, ginkgo.GinkgoWriter, ginkgo.GinkgoWriter) + require.NoError(err) + defer close(localPortStopChan) + localNodeURI := fmt.Sprintf("http://127.0.0.1:%d", localPort) + + infoClient := info.NewClient(localNodeURI) + bootstrapNodeID, _, err := infoClient.GetNodeID(tc.DefaultContext()) + require.NoError(err) + + ginkgo.By(fmt.Sprintf("Waiting for pod %s.%s to report a healthy status at %s", namespace, podName, localNodeURI)) + require.Eventually(func() bool { + healthReply, err := tmpnet.CheckNodeHealth(tc.DefaultContext(), localNodeURI) + if err != nil { + tc.Outf("Error checking node health: %v\n", err) + return false + } + return healthReply.Healthy + }, e2e.DefaultTimeout, e2e.DefaultPollingInterval) + + return bootstrapNodeID +} + +func waitForLogOutput(tc tests.TestContext, clientset *kubernetes.Clientset, namespace string, podName string, containerName string, desiredOutput string) { + require.Eventually(tc, func() bool { + logs, err := getContainerLogs(tc, clientset, namespace, podName, containerName) + if err != nil { + tc.Outf("Error getting container logs: %v\n", err) + return false + } + return strings.Contains(logs, desiredOutput) + }, e2e.DefaultTimeout, e2e.DefaultPollingInterval) +} + +func defaultNodeFlags() map[string]string { + return map[string]string{ + config.DataDirKey: nodeDataDir, + config.NetworkNameKey: constants.LocalName, + config.SybilProtectionEnabledKey: "false", + config.HealthCheckFreqKey: "500ms", // Ensure rapid detection of a healthy state + config.LogDisplayLevelKey: logging.Debug.String(), + config.LogLevelKey: logging.Debug.String(), + config.HTTPHostKey: "0.0.0.0", // Need to bind to pod IP to ensure kubelet can access the http port for the readiness check + } +} + +// enableLocalForwardForPod enables traffic forwarding from a local +// port to the specified pod with client-go. The returned stop channel +// should be closed to stop the port forwarding. +func EnableLocalForwardForPod(kubeConfig *restclient.Config, namespace string, name string, port int, out, errOut io.Writer) (uint16, chan struct{}, error) { + transport, upgrader, err := spdy.RoundTripperFor(kubeConfig) + if err != nil { + return 0, nil, fmt.Errorf("failed to create round tripper: %w", err) + } + + dialer := spdy.NewDialer( + upgrader, + &http.Client{ + Transport: transport, + }, + http.MethodPost, + &url.URL{ + Scheme: "https", + Path: fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", namespace, name), + Host: strings.TrimPrefix(kubeConfig.Host, "https://"), + }, + ) + ports := []string{fmt.Sprintf("0:%d", port)} + + // Need to specify 127.0.0.1 to ensure that forwarding is only attempted for the ipv4 + // address of the pod. By default, kind is deployed with only ipv4, and attempting to + // connect to a pod with ipv6 will fail. + addresses := []string{"127.0.0.1"} + + stopChan, readyChan := make(chan struct{}, 1), make(chan struct{}, 1) + forwarder, err := portforward.NewOnAddresses(dialer, addresses, ports, stopChan, readyChan, out, errOut) + if err != nil { + return 0, nil, fmt.Errorf("failed to create forwarder: %w", err) + } + + go func() { + if err := forwarder.ForwardPorts(); err != nil { + // TODO(marun) Need better error handling here? Or is ok for test-only usage? + panic(err) + } + }() + + <-readyChan // Wait for port forwarding to be ready + + // Retrieve the dynamically allocated local port + forwardedPorts, err := forwarder.GetPorts() + if err != nil { + close(stopChan) + return 0, nil, fmt.Errorf("failed to get forwarded ports: %w", err) + } + if len(forwardedPorts) == 0 { + close(stopChan) + return 0, nil, fmt.Errorf("failed to find at least one forwarded port: %w", err) + } + return forwardedPorts[0].Local, stopChan, nil +} diff --git a/tests/fixture/bootstrapmonitor/init.go b/tests/fixture/bootstrapmonitor/init.go new file mode 100644 index 000000000000..b7b94eafade6 --- /dev/null +++ b/tests/fixture/bootstrapmonitor/init.go @@ -0,0 +1,117 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrapmonitor + +import ( + "context" + "errors" + "log" + "os" + "strings" + "time" + + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + + "github.com/ava-labs/avalanchego/utils/perms" +) + +const ( + initTimeout = 2 * time.Minute + retryInterval = 5 * time.Second +) + +func InitBootstrapTest(namespace string, podName string, nodeContainerName string, dataDir string) error { + var ( + clientset *kubernetes.Clientset + containerImage string + ) + return wait.PollImmediateInfinite(retryInterval, func() (bool, error) { + if clientset == nil { + log.Println("Initializing clientset") + var err error + if clientset, err = getClientset(); err != nil { + log.Printf("failed to get clientset: %v", err) + return false, nil + } + } + + ctx, cancel := context.WithTimeout(context.Background(), initTimeout) + defer cancel() + + if len(containerImage) == 0 { + // Retrieve the image used by the node container + var err error + log.Printf("Retrieving pod %s.%s to determine the image of container %q", namespace, podName, nodeContainerName) + if containerImage, err = GetContainerImage(ctx, clientset, namespace, podName, nodeContainerName); err != nil { + log.Printf("failed to get container image: %v", err) + return false, nil + } + log.Printf("Image for container %q: %s", nodeContainerName, containerImage) + } + + // If the image uses the latest tag, determine the latest image id and set the container image to that + if strings.HasSuffix(containerImage, ":latest") { + log.Printf("Determining image id for image %q", containerImage) + imageID, err := getLatestImageID(ctx, clientset, namespace, containerImage, nodeContainerName) + if err != nil { + log.Printf("failed to get latest image id: %v", err) + return false, nil + } + log.Printf("Updating owning statefulset with image %q", containerImage) + if err := setContainerImage(ctx, clientset, namespace, podName, nodeContainerName, imageID); err != nil { + log.Printf("failed to set container image: %v", err) + return false, nil + } + } + + // A bootstrap is being resumed if a version file exists and the image name it contains matches the container + // image. If a bootstrap is being started, the version file should be created and the data path cleared. + log.Println("Determining whether a bootstrap is starting or being resumed") + + recordedImagePath := dataDir + "/bootstrap_image.txt" + + var recordedImage string + if recordedImageBytes, err := os.ReadFile(recordedImagePath); errors.Is(err, os.ErrNotExist) { + log.Println("Recorded image file not found") + } else if err != nil { + log.Printf("failed to read image file: %v", err) + return false, nil + } else { + recordedImage = string(recordedImageBytes) + log.Printf("Recorded image: %s", recordedImage) + } + + if recordedImage == containerImage { + log.Println(BootstrapResumingMessage(containerImage)) + return true, nil + } + + // TODO(marun) Create this value with a function + nodeDataDir := dataDir + "/node" + log.Printf("Removing contents of node directory %s", nodeDataDir) + if err := os.RemoveAll(nodeDataDir); err != nil { + log.Printf("failed to remove contents of node directory: %v", err) + return false, nil + } + + log.Printf("Writing image %q to %s", containerImage, recordedImagePath) + if err := os.WriteFile(recordedImagePath, []byte(containerImage), perms.ReadWrite); err != nil { + log.Printf("failed to write version file: %v", err) + return false, nil + } + + log.Println(BootstrapStartingMessage(containerImage)) + + return true, nil + }) +} + +func BootstrapStartingMessage(containerImage string) string { + return "Starting bootstrap test for image " + containerImage +} + +func BootstrapResumingMessage(containerImage string) string { + return "Resuming bootstrap test for image " + containerImage +} diff --git a/tests/fixture/bootstrapmonitor/wait.go b/tests/fixture/bootstrapmonitor/wait.go new file mode 100644 index 000000000000..74c3b79fc387 --- /dev/null +++ b/tests/fixture/bootstrapmonitor/wait.go @@ -0,0 +1,94 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrapmonitor + +import ( + "context" + "fmt" + "log" + "time" + + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + + "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" +) + +const ( + defaultContextDuration = 30 * time.Second +) + +func WaitForCompletion(namespace string, podName string, nodeContainerName string, interval time.Duration) error { + var ( + clientset *kubernetes.Clientset + reportedSuccess bool + containerImage string + ) + err := wait.PollImmediateInfinite(interval, func() (bool, error) { + ctx, cancel := context.WithTimeout(context.Background(), defaultContextDuration) + defer cancel() + + if healthy, err := tmpnet.CheckNodeHealth(ctx, "http://localhost:9650"); err != nil { + log.Printf("failed to wait for node health: %v", err) + return false, nil + } else if !healthy.Healthy { + return false, nil + } + + if clientset == nil { + var err error + clientset, err = getClientset() + if err != nil { + log.Printf("failed to get clientset: %v", err) + return false, nil + } + } + + if len(containerImage) == 0 { + var err error + log.Printf("Retrieving pod %s.%s to determine the image of container %q", namespace, podName, nodeContainerName) + containerImage, err = GetContainerImage(ctx, clientset, namespace, podName, nodeContainerName) + if err != nil { + log.Printf("failed to get container image: %v", err) + return false, nil + } + log.Printf("Image for container %q: %s", nodeContainerName, containerImage) + } + + if !reportedSuccess { + log.Println(BootstrapSucceededMessage(containerImage)) + reportedSuccess = true + } + + latestImageID, err := getLatestImageID(ctx, clientset, namespace, containerImage, nodeContainerName) + if err != nil { + log.Printf("failed to get latest image id: %v", err) + return false, nil + } + + if latestImageID == containerImage { + log.Printf("Latest image %s has already bootstrapped successfully", latestImageID) + return false, nil + } + + if err := setContainerImage(ctx, clientset, namespace, podName, nodeContainerName, latestImageID); err != nil { + log.Printf("failed to set container image: %v", err) + return false, nil + } + + // Statefulset will restart the pod with the new image + return true, nil + }) + if err != nil { + return fmt.Errorf("failed to wait for completion: %w", err) + } + + // Avoid exiting immediately to avoid container restart before the pod is recreated with the new image + time.Sleep(5 * time.Minute) + return nil +} + +func BootstrapSucceededMessage(containerImage string) string { + return "Bootstrap completed successfully for " + containerImage +} diff --git a/tests/fixture/tmpnet/node_process.go b/tests/fixture/tmpnet/node_process.go index b33fe32f0730..c11d83d3639a 100644 --- a/tests/fixture/tmpnet/node_process.go +++ b/tests/fixture/tmpnet/node_process.go @@ -10,7 +10,6 @@ import ( "fmt" "io" "io/fs" - "net" "os" "os/exec" "path/filepath" @@ -19,7 +18,6 @@ import ( "syscall" "time" - "github.com/ava-labs/avalanchego/api/health" "github.com/ava-labs/avalanchego/config" "github.com/ava-labs/avalanchego/node" "github.com/ava-labs/avalanchego/utils/perms" @@ -37,29 +35,6 @@ var ( errNotRunning = errors.New("node is not running") ) -func checkNodeHealth(ctx context.Context, uri string) (bool, error) { - // Check that the node is reporting healthy - health, err := health.NewClient(uri).Health(ctx, nil) - if err == nil { - return health.Healthy, nil - } - - switch t := err.(type) { - case *net.OpError: - if t.Op == "read" { - // Connection refused - potentially recoverable - return false, nil - } - case syscall.Errno: - if t == syscall.ECONNREFUSED { - // Connection refused - potentially recoverable - return false, nil - } - } - // Assume all other errors are not recoverable - return false, fmt.Errorf("failed to query node health: %w", err) -} - // Defines local-specific node configuration. Supports setting default // and node-specific values. type NodeProcess struct { @@ -199,7 +174,11 @@ func (p *NodeProcess) IsHealthy(ctx context.Context) (bool, error) { return false, errNotRunning } - return checkNodeHealth(ctx, p.node.URI) + healthReply, err := CheckNodeHealth(ctx, p.node.URI) + if err != nil { + return false, err + } + return healthReply.Healthy, nil } func (p *NodeProcess) getProcessContextPath() string { diff --git a/tests/fixture/tmpnet/utils.go b/tests/fixture/tmpnet/utils.go index ea320f2a8801..f2613f462208 100644 --- a/tests/fixture/tmpnet/utils.go +++ b/tests/fixture/tmpnet/utils.go @@ -7,8 +7,11 @@ import ( "context" "encoding/json" "fmt" + "net" + "syscall" "time" + "github.com/ava-labs/avalanchego/api/health" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" ) @@ -17,6 +20,29 @@ const ( DefaultNodeTickerInterval = 50 * time.Millisecond ) +func CheckNodeHealth(ctx context.Context, uri string) (*health.APIReply, error) { + // Check that the node is reporting healthy + healthReply, err := health.NewClient(uri).Health(ctx, nil) + if err == nil { + return healthReply, nil + } + + switch t := err.(type) { + case *net.OpError: + if t.Op == "read" { + // Connection refused - potentially recoverable + return nil, nil + } + case syscall.Errno: + if t == syscall.ECONNREFUSED { + // Connection refused - potentially recoverable + return nil, nil + } + } + // Assume all other errors are not recoverable + return nil, fmt.Errorf("failed to query node health: %w", err) +} + // WaitForHealthy blocks until Node.IsHealthy returns true or an error (including context timeout) is observed. func WaitForHealthy(ctx context.Context, node *Node) error { if _, ok := ctx.Deadline(); !ok {