anton-vinogradov commented on code in PR #13184:
URL: https://github.com/apache/ignite/pull/13184#discussion_r3477020640


##########
modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java:
##########
@@ -1269,7 +1269,8 @@ LinkedHashSet<InetSocketAddress> 
getEffectiveNodeAddresses(TcpDiscoveryNode node
 
         // Do not give own loopback to avoid requesting current node.
         if (!node.equals(locNode))
-            addrs.removeIf(addr -> addr.getAddress().isLoopbackAddress() && 
locNode.socketAddresses().contains(addr));
+            addrs.removeIf(addr -> addr.getAddress() == null ||
+                (addr.getAddress().isLoopbackAddress() && 
locNode.socketAddresses().contains(addr)));

Review Comment:
   **Core discovery behavior change bundled in a test PR.** This is the only 
production change here, and it alters `getEffectiveNodeAddresses` for *every* 
deployment: it now drops any address whose `getAddress() == null` (an 
*unresolved* `InetSocketAddress`, e.g. a peer that advertised a hostname this 
node can't resolve).
   
   The NPE guard itself looks legitimate — without it the original 
`addr.getAddress().isLoopbackAddress()` NPEs on an unresolved address. But (a) 
silently dropping unresolved addresses is a real behavior change for split-DNS 
/ hostname-advertised topologies, and (b) it ships with no dedicated unit test, 
inside a “basic RU test” PR.
   
   Please extract it into its own JIRA ticket with a focused unit test (a node 
whose `socketAddresses()` contains an unresolved address → assert no NPE + 
correct filtering) and a discovery-maintainer review. Also update the comment 
on the line above — it still only mentions “own loopback” and no longer 
describes the unresolved-address case.



##########
modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java:
##########
@@ -0,0 +1,232 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.compatibility.ru;
+
+import java.io.File;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.client.ClientCache;
+import org.apache.ignite.client.ClientCacheConfiguration;
+import org.apache.ignite.client.IgniteClient;
+import 
org.apache.ignite.compatibility.testframework.testcontainers.IgniteClusterContainer;
+import 
org.apache.ignite.compatibility.testframework.testcontainers.IgniteContainer;
+import org.apache.ignite.configuration.ClientConfiguration;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static 
org.apache.ignite.compatibility.testframework.testcontainers.IgniteContainer.LOCAL_WORK_DIR_PATH;
+import static org.apache.ignite.testframework.GridTestUtils.DFLT_TEST_TIMEOUT;
+import static org.apache.ignite.testframework.GridTestUtils.waitForCondition;
+
+/** Smoke test for rolling upgrade with persistence. */
+public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest {
+    /** Consistent ID's. */
+    private static final List<String> CONSISTENT_IDS = List.of(
+        "ad26bff6-5ff5-49f1-9a61-425a827953ed",
+        "c1099d16-e7d7-49f4-925c-53329286c444",
+        "7b880b69-8a9e-4b84-b555-250d365e2e67"
+    );
+
+    /** Source commit hash. Used for docker image tag. */
+    private static final String SOURCE_COMMIT_HASH = 
"0ad4656eef09acda288cbad96f80f0138732d94a";
+
+    /** Cache name. */
+    private static final String CACHE_NAME = "ru-test-cache";
+
+    /** Local work directory. */
+    private static final File LOCAL_WORK_DIR = new File(LOCAL_WORK_DIR_PATH);
+
+    /** Local nodes. */
+    private final List<IgniteEx> nodes = new ArrayList<>();
+
+    /** Consistent ID -> discovery address. */
+    private final Map<String, String> addrs = new HashMap<>();
+
+    /** Thin client. */
+    private IgniteClient client;
+
+    /** */
+    @BeforeClass
+    public static void beforeClass() {
+        U.delete(LOCAL_WORK_DIR);
+    }
+
+    /** */
+    @AfterClass
+    public static void afterClass() {
+        U.delete(LOCAL_WORK_DIR);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected boolean isMultiJvm() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected long getTestTimeout() {
+        return super.getTestTimeout() * 2;
+    }
+
+    /** Basic RU test. */
+    @Test
+    public void testRollingUpgrade() throws Exception {
+        try (IgniteClusterContainer cluster = new 
IgniteClusterContainer(SOURCE_COMMIT_HASH, CONSISTENT_IDS)) {
+            cluster.start();
+
+            for (IgniteContainer container : cluster.containers())
+                addrs.put(container.consistentId(), 
container.discoveryAddress());
+
+            ClientCacheConfiguration cfg = new ClientCacheConfiguration()
+                .setName(CACHE_NAME)
+                .setBackups(1)
+                .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL);
+
+            ClientCache<Integer, Integer> cache = 
client(cluster.containers().get(0).clientAddress()).createCache(cfg);
+
+            for (int i = 0; i < 1000; i++)
+                cache.put(i, i);
+
+            closeClient();
+            
+            upgradeCluster(cluster);
+
+            IgniteCache<Integer, Integer> targetCache = 
nodes.get(0).cache(CACHE_NAME);
+
+            for (int i = 0; i < 1000; i++)
+                assertEquals("Data mismatch after upgrade at key: " + i, i, 
(int)targetCache.get(i));

Review Comment:
   If a key is missing after upgrade, `targetCache.get(i)` is `null` and the 
`(int)` cast throws an NPE *before* the assertion message is built — masking 
the “Data mismatch” diagnostic. Compare as boxed `Integer` so a miss fails 
cleanly with the message.
   
   ```suggestion
                   assertEquals("Data mismatch after upgrade at key: " + i, 
(Integer)i, targetCache.get(i));
   ```



##########
modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java:
##########
@@ -0,0 +1,232 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.compatibility.ru;
+
+import java.io.File;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.client.ClientCache;
+import org.apache.ignite.client.ClientCacheConfiguration;
+import org.apache.ignite.client.IgniteClient;
+import 
org.apache.ignite.compatibility.testframework.testcontainers.IgniteClusterContainer;
+import 
org.apache.ignite.compatibility.testframework.testcontainers.IgniteContainer;
+import org.apache.ignite.configuration.ClientConfiguration;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static 
org.apache.ignite.compatibility.testframework.testcontainers.IgniteContainer.LOCAL_WORK_DIR_PATH;
+import static org.apache.ignite.testframework.GridTestUtils.DFLT_TEST_TIMEOUT;
+import static org.apache.ignite.testframework.GridTestUtils.waitForCondition;
+
+/** Smoke test for rolling upgrade with persistence. */
+public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest {
+    /** Consistent ID's. */
+    private static final List<String> CONSISTENT_IDS = List.of(
+        "ad26bff6-5ff5-49f1-9a61-425a827953ed",
+        "c1099d16-e7d7-49f4-925c-53329286c444",
+        "7b880b69-8a9e-4b84-b555-250d365e2e67"
+    );
+
+    /** Source commit hash. Used for docker image tag. */
+    private static final String SOURCE_COMMIT_HASH = 
"0ad4656eef09acda288cbad96f80f0138732d94a";
+
+    /** Cache name. */
+    private static final String CACHE_NAME = "ru-test-cache";
+
+    /** Local work directory. */
+    private static final File LOCAL_WORK_DIR = new File(LOCAL_WORK_DIR_PATH);
+
+    /** Local nodes. */
+    private final List<IgniteEx> nodes = new ArrayList<>();
+
+    /** Consistent ID -> discovery address. */
+    private final Map<String, String> addrs = new HashMap<>();
+
+    /** Thin client. */
+    private IgniteClient client;
+
+    /** */
+    @BeforeClass
+    public static void beforeClass() {
+        U.delete(LOCAL_WORK_DIR);
+    }
+
+    /** */
+    @AfterClass
+    public static void afterClass() {
+        U.delete(LOCAL_WORK_DIR);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected boolean isMultiJvm() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected long getTestTimeout() {
+        return super.getTestTimeout() * 2;
+    }
+
+    /** Basic RU test. */
+    @Test
+    public void testRollingUpgrade() throws Exception {
+        try (IgniteClusterContainer cluster = new 
IgniteClusterContainer(SOURCE_COMMIT_HASH, CONSISTENT_IDS)) {
+            cluster.start();
+
+            for (IgniteContainer container : cluster.containers())
+                addrs.put(container.consistentId(), 
container.discoveryAddress());
+
+            ClientCacheConfiguration cfg = new ClientCacheConfiguration()
+                .setName(CACHE_NAME)
+                .setBackups(1)
+                .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL);
+
+            ClientCache<Integer, Integer> cache = 
client(cluster.containers().get(0).clientAddress()).createCache(cfg);
+
+            for (int i = 0; i < 1000; i++)
+                cache.put(i, i);
+
+            closeClient();
+            

Review Comment:
   Trailing whitespace on this blank line — Ignite checkstyle 
(trailing-whitespace rule) flags it.
   
   ```suggestion
   
   ```



##########
modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java:
##########
@@ -0,0 +1,296 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.compatibility.testframework.testcontainers;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.time.Duration;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.cluster.ClusterState;
+import 
org.apache.ignite.compatibility.testframework.plugins.TestCompatibilityPluginProvider;
+import org.apache.ignite.configuration.ClientConnectorConfiguration;
+import org.apache.ignite.internal.IgniteInterruptedCheckedException;
+import org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import static 
org.apache.ignite.compatibility.testframework.testcontainers.ContainerAddressResolver.EXT_ADDR_PROP_PREFIX;
+import static org.apache.ignite.testframework.GridTestUtils.waitForCondition;
+import static org.testcontainers.shaded.org.awaitility.Awaitility.await;
+import static org.testcontainers.utility.MountableFile.forClasspathResource;
+import static org.testcontainers.utility.MountableFile.forHostPath;
+
+/** Ignite container. */
+public class IgniteContainer extends GenericContainer<IgniteContainer> {
+    /** Property for local work directory. */
+    private static final String LOCAL_WORK_DIR_PROP = "local.work.dir";
+
+    /** Local work directory. */
+    public static final String LOCAL_WORK_DIR_PATH = 
System.getProperty(LOCAL_WORK_DIR_PROP,
+        System.getProperty("user.dir") + "/target/test-ignite-work");
+
+    /** Logger. */
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(IgniteContainer.class);
+
+    /** Ignite root directory in container. */
+    private static final String ROOT_DIR_PATH = "/opt/ignite/apache-ignite/";
+
+    /** Ignite work directory in container. */
+    private static final String WORK_DIR_PATH = ROOT_DIR_PATH + "work";
+
+    /** Config path in container. */
+    private static final String CFG_PATH = ROOT_DIR_PATH + 
"config/test-config.xml";
+
+    /** */
+    private static final String ENABLE_EXPERIMENTAL_FLAG = 
"--enable-experimental";
+
+    /** */
+    private static final Pattern CLUSTER_STATE_PATTERN = 
Pattern.compile("Cluster state: (ACTIVE|INACTIVE)");
+
+    /** Base host port for the published discovery port (node index added). 
Kept clear of the host-node ports. */
+    private static final int DISCO_HOST_PORT_BASE = 50500;
+
+    /** Base host port for the published communication port (node index 
added). */
+    private static final int COMM_HOST_PORT_BASE = 50100;
+
+    /** Base host port for the published thin-client port (node index added). 
*/
+    private static final int CLIENT_HOST_PORT_BASE = 50800;
+
+    /** Custom classes used by node in containers. */
+    private static final List<String> TEST_CLASSES = List.of(
+        ContainerAddressResolver.class.getName(),
+        TestCompatibilityPluginProvider.class.getName()
+    );
+
+    /** Jar holding {@link #TEST_CLASSES}, injected so the old image can load 
it. */
+    private static volatile File testClassesJar;
+
+    /** Hostname. */
+    private final String hostname;
+
+    /** Consistent ID. */
+    private final String consistentId;
+
+    /** Path to work directory. */
+    private final String workDirPath;
+
+    /** Constructor. */
+    public IgniteContainer(String commitHash, Network net, String hostname, 
String consistentId, int idx) throws IOException {
+        super(DockerImageName.parse("apacheignite/ignite:" + commitHash));
+
+        this.hostname = hostname;
+        this.consistentId = consistentId;
+        workDirPath = WORK_DIR_PATH + "/" + hostname;
+
+        int discoHostPort = DISCO_HOST_PORT_BASE + idx;
+        int commHostPort = COMM_HOST_PORT_BASE + idx;
+
+        withEnv("CONFIG_URI", "file://" + CFG_PATH);
+        withEnv("IGNITE_QUIET", "false");
+        withEnv("IGNITE_WORK_DIR", workDirPath);
+        withEnv("IGNITE_LOCAL_HOST", "0.0.0.0");
+        withEnv("TZ", ZoneId.systemDefault().toString());
+
+        // On macOS the host JVM cannot reach container-internal addresses, so 
each node advertises its
+        // host-published discovery/communication ports (127.0.0.1:hostPort) 
via ContainerAddressResolver.
+        // node.consistent.id pins the node's consistent id (and thus its 
persistence folder) to consistentId so the
+        // upgraded host node, started with the same consistent id, inherits 
this node's persisted data.
+        withEnv("JVM_OPTS", "-Xms512m -Xmx1g" + " -Dnode.consistent.id=" + 
consistentId
+            + " -D" + EXT_ADDR_PROP_PREFIX + TcpDiscoverySpi.DFLT_PORT + 
"=127.0.0.1:" + discoHostPort
+            + " -D" + EXT_ADDR_PROP_PREFIX + TcpCommunicationSpi.DFLT_PORT + 
"=127.0.0.1:" + commHostPort);
+
+        withFileSystemBind(LOCAL_WORK_DIR_PATH, WORK_DIR_PATH, 
BindMode.READ_WRITE);
+        
withCopyFileToContainer(forClasspathResource("docker/test-config.xml"), 
CFG_PATH);
+        
withCopyFileToContainer(forHostPath(testClassesJar().getAbsolutePath()), 
ROOT_DIR_PATH + "libs/test-classes.jar");
+
+        withNetwork(net);
+        withNetworkAliases(hostname);
+
+        // Stream container logs to stdout so they appear in the IDE test 
runner.
+        withLogConsumer(frame -> System.out.println("[" + consistentId + "] " 
+ frame.getUtf8String().trim()));
+
+        // Fixed host ports so the host JVM node can target each container 
deterministically.
+        addFixedExposedPort(CLIENT_HOST_PORT_BASE + idx, 
ClientConnectorConfiguration.DFLT_PORT);
+        addFixedExposedPort(commHostPort, TcpCommunicationSpi.DFLT_PORT);
+        addFixedExposedPort(discoHostPort, TcpDiscoverySpi.DFLT_PORT);
+
+        waitingFor(Wait.forLogMessage(".*Node started.*", 1)
+            .withStartupTimeout(Duration.ofSeconds(600)));
+    }
+
+    /** {@inheritDoc} */
+    @Override public void stop() {
+        if (isRunning()) {
+            try {
+                LOGGER.info("Sending SIGTERM to Ignite node {} for graceful 
shutdown...", hostname);
+
+                getDockerClient().killContainerCmd(getContainerId())
+                    .withSignal("TERM")
+                    .exec();
+
+                await()
+                    .atMost(Duration.ofSeconds(60))
+                    .pollInterval(Duration.ofMillis(500))
+                    .until(() -> !isRunning());
+            }
+            catch (Exception e) {
+                LOGGER.warn("Graceful shutdown failed for node {}. Proceeding 
with forceful stop.", hostname, e);
+            }
+        }
+
+        LOGGER.info("Ignite node {} shut down gracefully.", hostname);

Review Comment:
   This logs “…shut down gracefully.” unconditionally — even when the SIGTERM 
path threw, we already logged a warning, and fell back to a forceful 
`super.stop()`. Move it inside the `try` (after `await(...)`) so it only 
reports actual graceful success.



##########
modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java:
##########
@@ -0,0 +1,296 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.compatibility.testframework.testcontainers;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.time.Duration;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.cluster.ClusterState;
+import 
org.apache.ignite.compatibility.testframework.plugins.TestCompatibilityPluginProvider;
+import org.apache.ignite.configuration.ClientConnectorConfiguration;
+import org.apache.ignite.internal.IgniteInterruptedCheckedException;
+import org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import static 
org.apache.ignite.compatibility.testframework.testcontainers.ContainerAddressResolver.EXT_ADDR_PROP_PREFIX;
+import static org.apache.ignite.testframework.GridTestUtils.waitForCondition;
+import static org.testcontainers.shaded.org.awaitility.Awaitility.await;
+import static org.testcontainers.utility.MountableFile.forClasspathResource;
+import static org.testcontainers.utility.MountableFile.forHostPath;
+
+/** Ignite container. */
+public class IgniteContainer extends GenericContainer<IgniteContainer> {
+    /** Property for local work directory. */
+    private static final String LOCAL_WORK_DIR_PROP = "local.work.dir";
+
+    /** Local work directory. */
+    public static final String LOCAL_WORK_DIR_PATH = 
System.getProperty(LOCAL_WORK_DIR_PROP,
+        System.getProperty("user.dir") + "/target/test-ignite-work");
+
+    /** Logger. */
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(IgniteContainer.class);
+
+    /** Ignite root directory in container. */
+    private static final String ROOT_DIR_PATH = "/opt/ignite/apache-ignite/";
+
+    /** Ignite work directory in container. */
+    private static final String WORK_DIR_PATH = ROOT_DIR_PATH + "work";
+
+    /** Config path in container. */
+    private static final String CFG_PATH = ROOT_DIR_PATH + 
"config/test-config.xml";
+
+    /** */
+    private static final String ENABLE_EXPERIMENTAL_FLAG = 
"--enable-experimental";
+
+    /** */
+    private static final Pattern CLUSTER_STATE_PATTERN = 
Pattern.compile("Cluster state: (ACTIVE|INACTIVE)");
+
+    /** Base host port for the published discovery port (node index added). 
Kept clear of the host-node ports. */
+    private static final int DISCO_HOST_PORT_BASE = 50500;
+
+    /** Base host port for the published communication port (node index 
added). */
+    private static final int COMM_HOST_PORT_BASE = 50100;
+
+    /** Base host port for the published thin-client port (node index added). 
*/
+    private static final int CLIENT_HOST_PORT_BASE = 50800;
+
+    /** Custom classes used by node in containers. */
+    private static final List<String> TEST_CLASSES = List.of(
+        ContainerAddressResolver.class.getName(),
+        TestCompatibilityPluginProvider.class.getName()
+    );
+
+    /** Jar holding {@link #TEST_CLASSES}, injected so the old image can load 
it. */
+    private static volatile File testClassesJar;
+
+    /** Hostname. */
+    private final String hostname;
+
+    /** Consistent ID. */
+    private final String consistentId;
+
+    /** Path to work directory. */
+    private final String workDirPath;
+
+    /** Constructor. */
+    public IgniteContainer(String commitHash, Network net, String hostname, 
String consistentId, int idx) throws IOException {
+        super(DockerImageName.parse("apacheignite/ignite:" + commitHash));
+
+        this.hostname = hostname;
+        this.consistentId = consistentId;
+        workDirPath = WORK_DIR_PATH + "/" + hostname;
+
+        int discoHostPort = DISCO_HOST_PORT_BASE + idx;
+        int commHostPort = COMM_HOST_PORT_BASE + idx;
+
+        withEnv("CONFIG_URI", "file://" + CFG_PATH);
+        withEnv("IGNITE_QUIET", "false");
+        withEnv("IGNITE_WORK_DIR", workDirPath);
+        withEnv("IGNITE_LOCAL_HOST", "0.0.0.0");
+        withEnv("TZ", ZoneId.systemDefault().toString());
+
+        // On macOS the host JVM cannot reach container-internal addresses, so 
each node advertises its
+        // host-published discovery/communication ports (127.0.0.1:hostPort) 
via ContainerAddressResolver.
+        // node.consistent.id pins the node's consistent id (and thus its 
persistence folder) to consistentId so the
+        // upgraded host node, started with the same consistent id, inherits 
this node's persisted data.
+        withEnv("JVM_OPTS", "-Xms512m -Xmx1g" + " -Dnode.consistent.id=" + 
consistentId
+            + " -D" + EXT_ADDR_PROP_PREFIX + TcpDiscoverySpi.DFLT_PORT + 
"=127.0.0.1:" + discoHostPort
+            + " -D" + EXT_ADDR_PROP_PREFIX + TcpCommunicationSpi.DFLT_PORT + 
"=127.0.0.1:" + commHostPort);
+
+        withFileSystemBind(LOCAL_WORK_DIR_PATH, WORK_DIR_PATH, 
BindMode.READ_WRITE);
+        
withCopyFileToContainer(forClasspathResource("docker/test-config.xml"), 
CFG_PATH);
+        
withCopyFileToContainer(forHostPath(testClassesJar().getAbsolutePath()), 
ROOT_DIR_PATH + "libs/test-classes.jar");
+
+        withNetwork(net);
+        withNetworkAliases(hostname);
+
+        // Stream container logs to stdout so they appear in the IDE test 
runner.
+        withLogConsumer(frame -> System.out.println("[" + consistentId + "] " 
+ frame.getUtf8String().trim()));
+
+        // Fixed host ports so the host JVM node can target each container 
deterministically.
+        addFixedExposedPort(CLIENT_HOST_PORT_BASE + idx, 
ClientConnectorConfiguration.DFLT_PORT);
+        addFixedExposedPort(commHostPort, TcpCommunicationSpi.DFLT_PORT);
+        addFixedExposedPort(discoHostPort, TcpDiscoverySpi.DFLT_PORT);
+
+        waitingFor(Wait.forLogMessage(".*Node started.*", 1)
+            .withStartupTimeout(Duration.ofSeconds(600)));
+    }
+
+    /** {@inheritDoc} */
+    @Override public void stop() {
+        if (isRunning()) {
+            try {
+                LOGGER.info("Sending SIGTERM to Ignite node {} for graceful 
shutdown...", hostname);
+
+                getDockerClient().killContainerCmd(getContainerId())
+                    .withSignal("TERM")
+                    .exec();
+
+                await()
+                    .atMost(Duration.ofSeconds(60))
+                    .pollInterval(Duration.ofMillis(500))
+                    .until(() -> !isRunning());
+            }
+            catch (Exception e) {
+                LOGGER.warn("Graceful shutdown failed for node {}. Proceeding 
with forceful stop.", hostname, e);
+            }
+        }
+
+        LOGGER.info("Ignite node {} shut down gracefully.", hostname);
+
+        super.stop();
+    }
+
+    /** @return Consistent ID. */
+    public String consistentId() {
+        return consistentId;
+    }
+
+    /** */
+    public String localWorkDirectory() {
+        return LOCAL_WORK_DIR_PATH + "/" + hostname;
+    }
+
+    /** Activate cluster. */
+    public void activateCluster(int nodeCnt) {
+        execControl("--set-state", "ACTIVE", "--yes");
+
+        try {
+            boolean success = waitForCondition(() -> {
+                String out = execControl("--state");
+
+                Matcher matcher = CLUSTER_STATE_PATTERN.matcher(out);
+
+                if (matcher.find())
+                    return ClusterState.valueOf(matcher.group(1)) == 
ClusterState.ACTIVE;
+
+                return false;
+            }, 30_000);
+
+            if (!success)
+                throw new IllegalStateException("Failed to set state ACTIVE");
+
+            success = waitForCondition(() -> {
+                String out = execControl("--baseline");
+
+                System.out.println(">>> Out=" + out);
+

Review Comment:
   Leftover debug `System.out.println` — drop it (or use `LOGGER.debug`); it 
spams the full `--baseline` output on every poll.
   
   ```suggestion
   
   ```



##########
modules/compatibility/src/test/java/org/apache/ignite/compatibility/testsuites/IgniteRollingUpgradeDockerTestSuite.java:
##########
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.compatibility.testsuites;
+
+import org.apache.ignite.compatibility.ru.IgniteRebalanceOnUpgradeTest;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/** Contains RU tests based on testcontainers. */
+@RunWith(Suite.class)
[email protected]({
+    IgniteRebalanceOnUpgradeTest.class
+})
+public class IgniteRollingUpgradeDockerTestSuite {

Review Comment:
   This suite isn't referenced by any other suite or by the TC/CI config 
(grepped the tree — no references), so nothing runs it automatically; as 
committed it adds no regression coverage. What's the CI plan? Wiring it into 
TeamCity will have to handle: a Docker daemon on the agent, a prebuilt source 
image, the hardcoded `SOURCE_COMMIT_HASH`, and the macOS-specific networking 
below (Linux agents route to containers directly and don't resolve 
`host.docker.internal` by default).



##########
modules/compatibility/src/test/resources/docker/build_docker_image.sh:
##########
@@ -0,0 +1,156 @@
+#!/bin/bash
+
+# Get the absolute path to the project root directory
+PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../../../" && pwd)"
+
+# Change to the project root directory
+cd "$PROJECT_ROOT" || exit 1
+
+# Save current git state (branch name and commit hash)
+ORIGINAL_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
+ORIGINAL_COMMIT=$(git rev-parse HEAD)
+
+# Track whether Dockerfiles were patched (for cleanup on exit)
+DOCKERFILES_PATCHED=0
+PATCHED_DOCKERFILES=""
+
+# Function to restore original Dockerfiles
+restore_dockerfiles() {
+    if [ "$DOCKERFILES_PATCHED" -eq 1 ] && [ -n "$PATCHED_DOCKERFILES" ]; then
+        echo -e "\nRestoring patched Dockerfiles"
+        git checkout "$ORIGINAL_COMMIT" -- $PATCHED_DOCKERFILES
+    fi
+}
+
+# Function to restore original git state
+restore_git_state() {
+    if [ -n "$ORIGINAL_BRANCH" ]; then
+        echo -e "\nRestoring git state to branch: $ORIGINAL_BRANCH"
+        git checkout "$ORIGINAL_BRANCH" 2>/dev/null
+    else
+        echo -e "\nRestoring git state to detached commit: $ORIGINAL_COMMIT"
+        git checkout "$ORIGINAL_COMMIT" 2>/dev/null
+    fi
+    restore_dockerfiles
+}
+
+# Set trap to restore git state on exit (success or failure)
+trap restore_git_state EXIT
+
+# Check that commit hash is provided, or use the latest commit in current 
branch
+if [ $# -eq 1 ]; then
+    COMMIT_HASH=$1
+elif [ $# -eq 0 ]; then
+    COMMIT_HASH=$(git rev-parse HEAD)
+else
+    echo "Usage: $0 [commit_hash]"
+    exit 1
+fi
+
+# Perform git checkout to the specified commit
+echo -e "\nPerforming git checkout to commit: $COMMIT_HASH"
+git checkout "$COMMIT_HASH"

Review Comment:
   `git checkout "$COMMIT_HASH"` mutates the *live working tree* and then runs 
`mvnw clean install` in it. If the tree has uncommitted changes the checkout 
aborts; and if the script is killed (SIGKILL / crash / CI cancel) before the 
`EXIT` trap fires, the user is left on a detached old commit with an old build. 
Safer to build the source version in an isolated `git worktree` (or a throwaway 
clone) so the working tree is never touched.



##########
parent/pom.xml:
##########
@@ -110,6 +110,7 @@
         <spring.version>5.3.39</spring.version>
         <surefire.version>3.5.6</surefire.version>
         <tomcat.version>10.0.27</tomcat.version>
+        <testcontainers.version>2.0.5</testcontainers.version>

Review Comment:
   Two things on this new property: (1) `2.0.5` is a brand-new Testcontainers 
**major** — the mainstream line is `1.21.x`. Please confirm 2.0.x is 
intentional (its API differs from 1.x) and not a typo for `1.20.5`/`1.21.x`. 
(2) Minor: it's out of alphabetical order — `testcontainers` should come before 
`tomcat`.



##########
modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java:
##########
@@ -0,0 +1,296 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.compatibility.testframework.testcontainers;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.time.Duration;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.cluster.ClusterState;
+import 
org.apache.ignite.compatibility.testframework.plugins.TestCompatibilityPluginProvider;
+import org.apache.ignite.configuration.ClientConnectorConfiguration;
+import org.apache.ignite.internal.IgniteInterruptedCheckedException;
+import org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import static 
org.apache.ignite.compatibility.testframework.testcontainers.ContainerAddressResolver.EXT_ADDR_PROP_PREFIX;
+import static org.apache.ignite.testframework.GridTestUtils.waitForCondition;
+import static org.testcontainers.shaded.org.awaitility.Awaitility.await;
+import static org.testcontainers.utility.MountableFile.forClasspathResource;
+import static org.testcontainers.utility.MountableFile.forHostPath;
+
+/** Ignite container. */
+public class IgniteContainer extends GenericContainer<IgniteContainer> {
+    /** Property for local work directory. */
+    private static final String LOCAL_WORK_DIR_PROP = "local.work.dir";
+
+    /** Local work directory. */
+    public static final String LOCAL_WORK_DIR_PATH = 
System.getProperty(LOCAL_WORK_DIR_PROP,
+        System.getProperty("user.dir") + "/target/test-ignite-work");
+
+    /** Logger. */
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(IgniteContainer.class);
+
+    /** Ignite root directory in container. */
+    private static final String ROOT_DIR_PATH = "/opt/ignite/apache-ignite/";
+
+    /** Ignite work directory in container. */
+    private static final String WORK_DIR_PATH = ROOT_DIR_PATH + "work";
+
+    /** Config path in container. */
+    private static final String CFG_PATH = ROOT_DIR_PATH + 
"config/test-config.xml";
+
+    /** */
+    private static final String ENABLE_EXPERIMENTAL_FLAG = 
"--enable-experimental";
+
+    /** */
+    private static final Pattern CLUSTER_STATE_PATTERN = 
Pattern.compile("Cluster state: (ACTIVE|INACTIVE)");
+
+    /** Base host port for the published discovery port (node index added). 
Kept clear of the host-node ports. */
+    private static final int DISCO_HOST_PORT_BASE = 50500;
+
+    /** Base host port for the published communication port (node index 
added). */
+    private static final int COMM_HOST_PORT_BASE = 50100;
+
+    /** Base host port for the published thin-client port (node index added). 
*/
+    private static final int CLIENT_HOST_PORT_BASE = 50800;
+
+    /** Custom classes used by node in containers. */
+    private static final List<String> TEST_CLASSES = List.of(
+        ContainerAddressResolver.class.getName(),
+        TestCompatibilityPluginProvider.class.getName()
+    );
+
+    /** Jar holding {@link #TEST_CLASSES}, injected so the old image can load 
it. */
+    private static volatile File testClassesJar;
+
+    /** Hostname. */
+    private final String hostname;
+
+    /** Consistent ID. */
+    private final String consistentId;
+
+    /** Path to work directory. */
+    private final String workDirPath;
+
+    /** Constructor. */
+    public IgniteContainer(String commitHash, Network net, String hostname, 
String consistentId, int idx) throws IOException {
+        super(DockerImageName.parse("apacheignite/ignite:" + commitHash));
+
+        this.hostname = hostname;
+        this.consistentId = consistentId;
+        workDirPath = WORK_DIR_PATH + "/" + hostname;
+
+        int discoHostPort = DISCO_HOST_PORT_BASE + idx;
+        int commHostPort = COMM_HOST_PORT_BASE + idx;
+
+        withEnv("CONFIG_URI", "file://" + CFG_PATH);
+        withEnv("IGNITE_QUIET", "false");
+        withEnv("IGNITE_WORK_DIR", workDirPath);
+        withEnv("IGNITE_LOCAL_HOST", "0.0.0.0");
+        withEnv("TZ", ZoneId.systemDefault().toString());
+
+        // On macOS the host JVM cannot reach container-internal addresses, so 
each node advertises its
+        // host-published discovery/communication ports (127.0.0.1:hostPort) 
via ContainerAddressResolver.
+        // node.consistent.id pins the node's consistent id (and thus its 
persistence folder) to consistentId so the
+        // upgraded host node, started with the same consistent id, inherits 
this node's persisted data.
+        withEnv("JVM_OPTS", "-Xms512m -Xmx1g" + " -Dnode.consistent.id=" + 
consistentId
+            + " -D" + EXT_ADDR_PROP_PREFIX + TcpDiscoverySpi.DFLT_PORT + 
"=127.0.0.1:" + discoHostPort
+            + " -D" + EXT_ADDR_PROP_PREFIX + TcpCommunicationSpi.DFLT_PORT + 
"=127.0.0.1:" + commHostPort);
+
+        withFileSystemBind(LOCAL_WORK_DIR_PATH, WORK_DIR_PATH, 
BindMode.READ_WRITE);
+        
withCopyFileToContainer(forClasspathResource("docker/test-config.xml"), 
CFG_PATH);
+        
withCopyFileToContainer(forHostPath(testClassesJar().getAbsolutePath()), 
ROOT_DIR_PATH + "libs/test-classes.jar");
+
+        withNetwork(net);
+        withNetworkAliases(hostname);
+
+        // Stream container logs to stdout so they appear in the IDE test 
runner.
+        withLogConsumer(frame -> System.out.println("[" + consistentId + "] " 
+ frame.getUtf8String().trim()));
+
+        // Fixed host ports so the host JVM node can target each container 
deterministically.
+        addFixedExposedPort(CLIENT_HOST_PORT_BASE + idx, 
ClientConnectorConfiguration.DFLT_PORT);
+        addFixedExposedPort(commHostPort, TcpCommunicationSpi.DFLT_PORT);
+        addFixedExposedPort(discoHostPort, TcpDiscoverySpi.DFLT_PORT);

Review Comment:
   `addFixedExposedPort` pins host ports (50100/50500/50800 + idx). Fixed ports 
block parallel execution and cause flaky `port is already allocated` failures 
if a previous run didn't release them or anything else holds them — and with 
the 600s startup wait above, such failures are slow. If fixed ports are 
required for deterministic host-JVM targeting, please say so in a comment and 
consider a configurable base offset / retry; otherwise prefer Testcontainers' 
dynamic port mapping.



##########
modules/compatibility/src/test/resources/docker/build_docker_image.sh:
##########
@@ -0,0 +1,156 @@
+#!/bin/bash
+
+# Get the absolute path to the project root directory
+PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../../../" && pwd)"
+
+# Change to the project root directory
+cd "$PROJECT_ROOT" || exit 1
+
+# Save current git state (branch name and commit hash)
+ORIGINAL_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
+ORIGINAL_COMMIT=$(git rev-parse HEAD)
+
+# Track whether Dockerfiles were patched (for cleanup on exit)
+DOCKERFILES_PATCHED=0
+PATCHED_DOCKERFILES=""
+
+# Function to restore original Dockerfiles
+restore_dockerfiles() {
+    if [ "$DOCKERFILES_PATCHED" -eq 1 ] && [ -n "$PATCHED_DOCKERFILES" ]; then
+        echo -e "\nRestoring patched Dockerfiles"
+        git checkout "$ORIGINAL_COMMIT" -- $PATCHED_DOCKERFILES
+    fi
+}
+
+# Function to restore original git state
+restore_git_state() {
+    if [ -n "$ORIGINAL_BRANCH" ]; then
+        echo -e "\nRestoring git state to branch: $ORIGINAL_BRANCH"
+        git checkout "$ORIGINAL_BRANCH" 2>/dev/null
+    else
+        echo -e "\nRestoring git state to detached commit: $ORIGINAL_COMMIT"
+        git checkout "$ORIGINAL_COMMIT" 2>/dev/null
+    fi
+    restore_dockerfiles
+}
+
+# Set trap to restore git state on exit (success or failure)
+trap restore_git_state EXIT
+
+# Check that commit hash is provided, or use the latest commit in current 
branch
+if [ $# -eq 1 ]; then
+    COMMIT_HASH=$1
+elif [ $# -eq 0 ]; then
+    COMMIT_HASH=$(git rev-parse HEAD)
+else
+    echo "Usage: $0 [commit_hash]"
+    exit 1
+fi
+
+# Perform git checkout to the specified commit
+echo -e "\nPerforming git checkout to commit: $COMMIT_HASH"
+git checkout "$COMMIT_HASH"
+
+# Build the project (skip if distribution archive already exists)
+SOURCE_ARCHIVE="$PROJECT_ROOT/target/bin/apache-ignite-*.zip"
+
+if ls $SOURCE_ARCHIVE 1> /dev/null 2>&1; then
+    echo -e "\nDistribution archive already found, skipping build steps"
+else
+    echo -e "\nBuilding the project: ./mvnw clean install -T1C 
-Pall-java,licenses -DskipTests"
+    ./mvnw clean install -T1C -Pall-java,licenses -DskipTests
+
+    # Check success of previous command
+    if [ $? -ne 0 ]; then
+        echo -e "\nError during project build"
+        exit 1
+    fi
+
+    # Initialize the release
+    echo -e "\nInitializing release: ./mvnw initialize -Prelease"
+    ./mvnw initialize -Prelease
+
+    # Check success of previous command
+    if [ $? -ne 0 ]; then
+        echo -e "\nError during release initialization"
+        exit 1
+    fi
+fi
+
+# Copy and unpack the release archive
+echo -e "\nCopying and unpacking the release archive"
+# Detect CPU architecture
+ARCH=$(uname -m)
+case "$ARCH" in
+    arm64|aarch64)
+        TARGET_DIR="$PROJECT_ROOT/deliveries/docker/apache-ignite/arm64"
+        ;;
+    x86_64)
+        TARGET_DIR="$PROJECT_ROOT/deliveries/docker/apache-ignite/x86_64"
+        ;;
+    *)
+        echo "Unsupported architecture: $ARCH"
+        exit 1
+        ;;
+esac
+
+# Check if archive exists
+if [ ! -f $(echo $SOURCE_ARCHIVE) ]; then
+    echo -e "\nArchive not found: $SOURCE_ARCHIVE"
+    exit 1
+fi
+
+# Copy archive to target directory
+cp $(echo $SOURCE_ARCHIVE) "$TARGET_DIR/"
+
+# Unpack the archive
+cd "$TARGET_DIR"
+unzip -o "$(basename $(echo $SOURCE_ARCHIVE))"
+
+# Return to project root directory
+cd "$PROJECT_ROOT"
+
+# Copy the startup script
+echo -e "\nCopying startup script"
+cp "$PROJECT_ROOT/deliveries/docker/apache-ignite/run.sh" "$TARGET_DIR/"
+
+# Patch Dockerfiles: switch apk repos to HTTP (bypasses TLS cert issues in 
corporate networks)
+PATCHED_DOCKERFILES=$(find "$PROJECT_ROOT/deliveries/docker/apache-ignite" 
-name Dockerfile)
+for DF in $PATCHED_DOCKERFILES; do
+    echo -e "\nPatching $DF (Debian base image instead of Alpine)"

Review Comment:
   DEVNOTES says the Alpine→Debian / apk patch is applied only *“if your 
corporate network uses SSL inspection”*, but here it runs **unconditionally** 
on every build, so the produced image is always Debian-based regardless of 
environment. Either gate it behind a flag / TLS-failure detection, or update 
DEVNOTES to state the base image is always Debian.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to