This is an automated email from the ASF dual-hosted git repository. yihua pushed a commit to branch release-1.2.0 in repository https://gitbox.apache.org/repos/asf/hudi.git
commit 3dddb15ac610d013ff694381d8d65556923f860b Author: Xinli Shang <[email protected]> AuthorDate: Tue May 19 12:05:07 2026 -0700 test(azure): skip ITAzureStorageLockClientAzurite when MCR image pull fails (#18772) Co-authored-by: Xinli Shang <[email protected]> --- .../lock/ITAzureStorageLockClientAzurite.java | 72 ++++++++++++++++++---- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/hudi-azure/src/test/java/org/apache/hudi/azure/transaction/lock/ITAzureStorageLockClientAzurite.java b/hudi-azure/src/test/java/org/apache/hudi/azure/transaction/lock/ITAzureStorageLockClientAzurite.java index a7897a6ac3d7..8f5b57aab0aa 100644 --- a/hudi-azure/src/test/java/org/apache/hudi/azure/transaction/lock/ITAzureStorageLockClientAzurite.java +++ b/hudi-azure/src/test/java/org/apache/hudi/azure/transaction/lock/ITAzureStorageLockClientAzurite.java @@ -30,12 +30,15 @@ import org.apache.hudi.config.AzureStorageLockConfig; import com.azure.storage.blob.BlobContainerClient; import com.azure.storage.blob.BlobServiceClient; import com.azure.storage.blob.BlobServiceClientBuilder; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; +import org.testcontainers.containers.ContainerFetchException; +import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; import java.time.Duration; @@ -43,18 +46,28 @@ import java.util.Properties; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.abort; /** * Integration tests for {@link AzureStorageLockClient} using Azurite (Azure Storage emulator). * * <p>Run with: {@code mvn -Pazure-integration-tests -pl hudi-azure verify} + * + * <p>If the Azurite Docker image cannot be pulled (Microsoft Container Registry blocked, + * rate-limited, or network unavailable), the test class is skipped via {@link + * org.junit.jupiter.api.Assumptions#abort(String)} rather than failing the CI run. Integration + * tests against external container images should not gate the overall build on registry-side + * outages that are outside the project's control. */ -@Testcontainers(disabledWithoutDocker = true) +@Slf4j @DisabledIfEnvironmentVariable(named = "SKIP_AZURITE_IT", matches = "true") public class ITAzureStorageLockClientAzurite { + // Pin the Azurite version. The previous unpinned reference (implicit ":latest") was more + // susceptible to MCR rate-limiting / WAF blocks because :latest is queried more aggressively + // and is not cached as effectively as a tagged manifest. Bump deliberately when needed. private static final DockerImageName AZURITE_IMAGE = - DockerImageName.parse("mcr.microsoft.com/azure-storage/azurite"); + DockerImageName.parse("mcr.microsoft.com/azure-storage/azurite:3.34.0"); // Standard Azurite defaults (documented by Microsoft) private static final String ACCOUNT_NAME = "devstoreaccount1"; @@ -63,16 +76,53 @@ public class ITAzureStorageLockClientAzurite { private static final String ACCOUNT_KEY = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="; - @Container - public static final GenericContainer<?> AZURITE = - new GenericContainer<>(AZURITE_IMAGE) - .withExposedPorts(10000) - .withCommand("azurite-blob", "--blobHost", "0.0.0.0", "--blobPort", "10000", "--loose") - .waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(60))); + // Manual container lifecycle (instead of @Testcontainers + @Container) so the test class is + // skipped — not failed — when the image cannot be pulled. The @Testcontainers extension + // surfaces image-pull errors as test errors, which is the wrong outcome for an external + // infrastructure failure (e.g. MCR returning HTTP 403 "request blocked" via Azure WAF). + private static GenericContainer<?> azurite; + + @BeforeAll + static void startAzurite() { + GenericContainer<?> container = new GenericContainer<>(AZURITE_IMAGE) + .withExposedPorts(10000) + .withCommand("azurite-blob", "--blobHost", "0.0.0.0", "--blobPort", "10000", "--loose") + .waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(60))); + try { + container.start(); + azurite = container; + } catch (ContainerFetchException | ContainerLaunchException e) { + // Image pull / container start failure. Most commonly caused by MCR being unreachable + // or rate-limited from CI runners. Skip the suite rather than failing the build. + log.warn("Azurite container unavailable; skipping suite.", e); + abort("Azurite container could not be started (image pull or launch failed): " + e.getMessage()); + } catch (RuntimeException e) { + // Testcontainers wraps a variety of Docker / network failures in plain RuntimeExceptions + // during start(). Treat any "could not start container" failure as a skip rather than + // an error for the same reason — these are infrastructure flakes, not test failures. + // Re-throw if Docker itself is missing so the developer gets a clear diagnosis locally. + // Known messages we want to surface: "Could not find a valid Docker environment", + // "Docker not found", "docker: command not found". + String msg = e.getMessage() == null ? "" : e.getMessage().toLowerCase(); + if (msg.contains("docker") && (msg.contains("not found") || msg.contains("no such") + || msg.contains("could not find") || msg.contains("not running"))) { + throw e; + } + log.warn("Azurite container unavailable; skipping suite.", e); + abort("Azurite container could not be started: " + e.getMessage()); + } + } + + @AfterAll + static void stopAzurite() { + if (azurite != null && azurite.isRunning()) { + azurite.stop(); + } + } private static String blobEndpoint() { // Azurite expects /<accountName> in the endpoint URL path - return "http://" + AZURITE.getHost() + ":" + AZURITE.getMappedPort(10000) + "/" + ACCOUNT_NAME; + return "http://" + azurite.getHost() + ":" + azurite.getMappedPort(10000) + "/" + ACCOUNT_NAME; } private static String connectionString() {
