This is an automated email from the ASF dual-hosted git repository.

petrov-mg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new e1803947e17 IGNITE-28916 Added support for Management API Client 
version validation (#13400)
e1803947e17 is described below

commit e1803947e17f87cbbe6ab725fad2674e0ac77fa1
Author: Mikhail Petrov <[email protected]>
AuthorDate: Mon Jul 27 13:21:34 2026 +0300

    IGNITE-28916 Added support for Management API Client version validation 
(#13400)
---
 .../internal/dto/IgniteDataTransferObject.java     |  20 +++-
 .../feature/IgniteCoreFeatureSet.java              |   2 +-
 .../ignite/internal/visor/VisorTaskArgument.java   |  53 ++++++++++
 .../internal/TestManagementVisorOneNodeTask.java   |  10 +-
 .../rollingupgrade/AbstractRollingUpgradeTest.java |  13 ++-
 .../ManagementApiVersionValidationTest.java        | 108 +++++++++++++++++++++
 .../ignite/testsuites/IgniteBasicTestSuite.java    |   2 +
 7 files changed, 192 insertions(+), 16 deletions(-)

diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/dto/IgniteDataTransferObject.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/dto/IgniteDataTransferObject.java
index 4d750a69cec..f24bba840b1 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/dto/IgniteDataTransferObject.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/dto/IgniteDataTransferObject.java
@@ -53,11 +53,7 @@ public abstract class IgniteDataTransferObject implements 
Externalizable {
     @Override public void writeExternal(ObjectOutput out) throws IOException {
         out.writeInt(MAGIC);
 
-        try (IgniteDataTransferObjectOutput dtout = new 
IgniteDataTransferObjectOutput(out)) {
-            IgniteDataTransferObjectSerializer serializer = 
IDTOSerializerFactory.getInstance().serializer(getClass());
-
-            serializer.writeExternal(this, dtout);
-        }
+        writeIgniteDataTransferObject(out);
     }
 
     /** {@inheritDoc} */
@@ -68,6 +64,20 @@ public abstract class IgniteDataTransferObject implements 
Externalizable {
             throw new IOException("Unexpected IgniteDataTransferObject header 
" +
                 "[actual=" + Integer.toHexString(hdr) + ", expected=" + 
Integer.toHexString(MAGIC) + "]");
 
+        readIgniteDataTransferObject(in);
+    }
+
+    /** */
+    protected void writeIgniteDataTransferObject(ObjectOutput out) throws 
IOException {
+        try (IgniteDataTransferObjectOutput dtout = new 
IgniteDataTransferObjectOutput(out)) {
+            IgniteDataTransferObjectSerializer serializer = 
IDTOSerializerFactory.getInstance().serializer(getClass());
+
+            serializer.writeExternal(this, dtout);
+        }
+    }
+
+    /** */
+    protected void readIgniteDataTransferObject(ObjectInput in) throws 
IOException, ClassNotFoundException {
         try (IgniteDataTransferObjectInput dtin = new 
IgniteDataTransferObjectInput(in)) {
             IgniteDataTransferObjectSerializer serializer = 
IDTOSerializerFactory.getInstance().serializer(getClass());
 
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/feature/IgniteCoreFeatureSet.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/feature/IgniteCoreFeatureSet.java
index 8c495d80825..f5173872ef0 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/feature/IgniteCoreFeatureSet.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/feature/IgniteCoreFeatureSet.java
@@ -29,7 +29,7 @@ public class IgniteCoreFeatureSet extends 
IgniteComponentFeatureSet {
     private static final long serialVersionUID = 0L;
 
     /** */
-    private static final IgniteCoreFeatureSet INSTANCE = new 
IgniteCoreFeatureSet(
+    static IgniteCoreFeatureSet INSTANCE = new IgniteCoreFeatureSet(
         IgniteVersionUtils.VER,
         IgniteFeatureSet.buildFrom(SupportedFeatureRegistry.class)
     );
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/visor/VisorTaskArgument.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/visor/VisorTaskArgument.java
index acb9ef559aa..55602bbb8f8 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/visor/VisorTaskArgument.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/visor/VisorTaskArgument.java
@@ -17,12 +17,17 @@
 
 package org.apache.ignite.internal.visor;
 
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
 import org.apache.ignite.internal.Order;
 import org.apache.ignite.internal.dto.IgniteDataTransferObject;
+import 
org.apache.ignite.internal.processors.metastorage.persistence.DistributedMetaStorageVersion;
+import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteCoreFeatureSet;
 import org.apache.ignite.internal.util.typedef.internal.S;
 
 /**
@@ -32,6 +37,19 @@ public class VisorTaskArgument<A> extends 
IgniteDataTransferObject {
     /** */
     private static final long serialVersionUID = 0L;
 
+    /**
+     * Magic header used for Management API argument serialization.
+     *
+     * <p>This value differs from the magic header defined in the parent class 
because some {@link IgniteDataTransferObject}
+     * implementations (for example, {@link DistributedMetaStorageVersion}) 
are already stored in the PDS, which
+     * requires full backward compatibility. A separate magic header is 
required to distinguish Management API arguments
+     * serialized by pre-Rolling Upgrade clients and handle them correctly.</p>
+     */
+    private static final int MAGIC = 0xBAA55F5E;
+
+    /** */
+    transient IgniteCoreFeatureSet cmdInitiatorFeatures = 
IgniteCoreFeatureSet.local();
+
     /** Node IDs task should be mapped to. */
     @Order(0)
     List<UUID> nodes;
@@ -119,6 +137,41 @@ public class VisorTaskArgument<A> extends 
IgniteDataTransferObject {
         return debug;
     }
 
+    /** {@inheritDoc} */
+    @Override public void writeExternal(ObjectOutput out) throws IOException {
+        out.writeInt(MAGIC);
+
+        cmdInitiatorFeatures.writeExternal(out);
+
+        writeIgniteDataTransferObject(out);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readExternal(ObjectInput in) throws IOException, 
ClassNotFoundException {
+        int hdr = in.readInt();
+
+        if ((hdr & MAGIC) != MAGIC) {
+            throw new IOException("Failed to deserialize the Ignite Management 
API command argument. Unexpected Ignite" +
+                " DTO message header. The input stream is malformed or was 
generated by an incompatible Ignite version" +
+                " [actual=" + Integer.toHexString(hdr) +
+                ", expected=" + Integer.toHexString(MAGIC) + ']'
+            );
+        }
+
+        cmdInitiatorFeatures = new IgniteCoreFeatureSet();
+        cmdInitiatorFeatures.readExternal(in);
+
+        if 
(!cmdInitiatorFeatures.isUpgradableTo(IgniteCoreFeatureSet.local())) {
+            throw new IOException("Failed to deserialize the Ignite Management 
API command argument. The data was" +
+                " serialized by an incompatible Ignite version" +
+                " [remoteVersion=" + cmdInitiatorFeatures.version() +
+                ", localVersion=" + IgniteCoreFeatureSet.local().version() + 
']'
+            );
+        }
+
+        readIgniteDataTransferObject(in);
+    }
+
     /** {@inheritDoc} */
     @Override public String toString() {
         return S.toString(VisorTaskArgument.class, this);
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/TestManagementVisorOneNodeTask.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/TestManagementVisorOneNodeTask.java
index 1af21e3ecb2..5eee5dac163 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/internal/TestManagementVisorOneNodeTask.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/TestManagementVisorOneNodeTask.java
@@ -26,12 +26,12 @@ import org.jetbrains.annotations.Nullable;
 /**
  *
  */
-public class TestManagementVisorOneNodeTask extends VisorOneNodeTask<String, 
Object> {
+public class TestManagementVisorOneNodeTask extends VisorOneNodeTask<Object, 
Object> {
     /** */
     private static final long serialVersionUID = 0L;
 
     /** {@inheritDoc} */
-    @Override protected VisorValidOneNodeJob job(String arg) {
+    @Override protected VisorValidOneNodeJob job(Object arg) {
         return new VisorValidOneNodeJob(arg, debug);
     }
 
@@ -43,7 +43,7 @@ public class TestManagementVisorOneNodeTask extends 
VisorOneNodeTask<String, Obj
     /**
      * Valid Management one node visor job.
      */
-    private static class VisorValidOneNodeJob extends VisorJob<String, Object> 
{
+    private static class VisorValidOneNodeJob extends VisorJob<Object, Object> 
{
         /** */
         private static final long serialVersionUID = 0L;
 
@@ -51,12 +51,12 @@ public class TestManagementVisorOneNodeTask extends 
VisorOneNodeTask<String, Obj
          * @param arg Argument.
          * @param debug Debug flag.
          */
-        protected VisorValidOneNodeJob(String arg, boolean debug) {
+        protected VisorValidOneNodeJob(Object arg, boolean debug) {
             super(arg, debug);
         }
 
         /** {@inheritDoc} */
-        @Override protected Object run(String arg) {
+        @Override protected Object run(Object arg) {
             return null;
         }
     }
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/rollingupgrade/AbstractRollingUpgradeTest.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/rollingupgrade/AbstractRollingUpgradeTest.java
index 8e7148f0d4c..1802069a95a 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/rollingupgrade/AbstractRollingUpgradeTest.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/rollingupgrade/AbstractRollingUpgradeTest.java
@@ -389,11 +389,7 @@ public abstract class AbstractRollingUpgradeTest extends 
GridCommonAbstractTest
     protected void checkPreviousClusterFeatures(@Nullable String expVer) 
throws Exception {
         TestVersions expVersions = expVer == null ? null : 
TestVersions.parse(expVer);
 
-        IgniteCoreFeatureSet expPrevCoreFeatures = expVersions != null
-            ? new IgniteCoreFeatureSet(
-                IgniteProductVersion.fromString(expVersions.coreVersion()),
-                
IgniteFeatureSet.buildFrom(readDeclaredCoreFeatures(expVersions.coreVersion())))
-            : null;
+        IgniteCoreFeatureSet expPrevCoreFeatures = expVersions != null ? 
createCoreFeatureSet(expVersions.coreVersion()) : null;
 
         IgnitePluginFeatureSet expPrevPluginFeatures = expVersions != null && 
expVersions.containsPlugin()
             ? new IgnitePluginFeatureSet(
@@ -417,6 +413,13 @@ public abstract class AbstractRollingUpgradeTest extends 
GridCommonAbstractTest
         }
     }
 
+    /** */
+    public static IgniteCoreFeatureSet createCoreFeatureSet(String ver) throws 
Exception {
+        return new IgniteCoreFeatureSet(
+            IgniteProductVersion.fromString(ver),
+            IgniteFeatureSet.buildFrom(readDeclaredCoreFeatures(ver)));
+    }
+
     /** */
     protected void checkVersionUpgradeInactive(String expVer) throws Exception 
{
         checkVersionUpgradeEnabledStatus(false);
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/rollingupgrade/feature/ManagementApiVersionValidationTest.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/rollingupgrade/feature/ManagementApiVersionValidationTest.java
new file mode 100644
index 00000000000..b8af58f9f7c
--- /dev/null
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/rollingupgrade/feature/ManagementApiVersionValidationTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.internal.processors.rollingupgrade.feature;
+
+import java.util.concurrent.Callable;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.client.ClientConnectionException;
+import org.apache.ignite.client.Config;
+import org.apache.ignite.client.IgniteClient;
+import org.apache.ignite.configuration.ClientConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.TestManagementVisorOneNodeTask;
+import 
org.apache.ignite.internal.processors.rollingupgrade.AbstractRollingUpgradeTest;
+import org.apache.ignite.internal.visor.VisorTaskArgument;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.apache.ignite.testframework.ListeningTestLogger;
+import org.apache.ignite.testframework.LogListener;
+import org.junit.Test;
+
+/** */
+public class ManagementApiVersionValidationTest extends 
AbstractRollingUpgradeTest {
+    /** */
+    public static final LogListener DESERIALIZATION_FAILED_LSNR = 
LogListener.builder().andMatches(
+        "Failed to deserialize the Ignite Management API command argument"
+    ).build();
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String 
igniteInstanceName, String ver) throws Exception {
+        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName, 
ver);
+
+        cfg.setGridLogger(new ListeningTestLogger(log, 
DESERIALIZATION_FAILED_LSNR));
+
+        return cfg;
+    }
+
+    /** */
+    @Test
+    public void testVersionValidation() throws Exception {
+        withCoreVersion("2.21.0", () -> {
+            startGrid(0);
+
+            checkCommandArgumentDeserializationFailed(0, "2.21.1");
+            checkCommandArgumentDeserializationFailed(0, "2.19.0");
+
+            executeCommand(createCommandArgument(0, "2.21.0"));
+            executeCommand(createCommandArgument(0, "2.20.0"));
+
+            return null;
+        });
+    }
+
+    /** */
+    private void checkCommandArgumentDeserializationFailed(int destNodeIdx, 
String ver) throws Exception {
+        DESERIALIZATION_FAILED_LSNR.reset();
+
+        GridTestUtils.assertThrowsAnyCause(
+            log,
+            () -> {
+                executeCommand(createCommandArgument(destNodeIdx, ver));
+
+                return null;
+            },
+            ClientConnectionException.class,
+            "Channel is closed");
+
+        DESERIALIZATION_FAILED_LSNR.check(getTestTimeout());
+    }
+
+    /** */
+    private void executeCommand(VisorTaskArgument<Object> arg) throws 
Exception {
+        try (IgniteClient cli = Ignition.startClient(new 
ClientConfiguration().setAddresses(Config.SERVER))) {
+            
cli.compute().execute(TestManagementVisorOneNodeTask.class.getName(), arg);
+        }
+    }
+
+    /** */
+    private VisorTaskArgument<Object> createCommandArgument(int destNodeIdx, 
String ver) throws Exception {
+        return withCoreVersion(ver, () -> new 
VisorTaskArgument<>(nodeId(destNodeIdx), "", false));
+    }
+
+    /** */
+    private static <R> R withCoreVersion(String ver, Callable<R> action) 
throws Exception {
+        IgniteCoreFeatureSet prev = IgniteCoreFeatureSet.INSTANCE;
+        IgniteCoreFeatureSet.INSTANCE = createCoreFeatureSet(ver);
+
+        try {
+            return action.call();
+        }
+        finally {
+            IgniteCoreFeatureSet.INSTANCE = prev;
+        }
+    }
+}
diff --git 
a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite.java
 
b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite.java
index a36175a09eb..a49d63953f1 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite.java
@@ -66,6 +66,7 @@ import 
org.apache.ignite.internal.processors.odbc.SqlListenerUtilsTest;
 import 
org.apache.ignite.internal.processors.rollingupgrade.CoreVersionRollingUpgradeTest;
 import 
org.apache.ignite.internal.processors.rollingupgrade.PluginVersionRollingUpgradeTest;
 import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteFeatureSetTest;
+import 
org.apache.ignite.internal.processors.rollingupgrade.feature.ManagementApiVersionValidationTest;
 import org.apache.ignite.internal.product.GridProductVersionSelfTest;
 import org.apache.ignite.internal.util.nio.IgniteExceptionInNioWorkerSelfTest;
 import org.apache.ignite.messaging.GridMessagingNoPeerClassLoadingSelfTest;
@@ -108,6 +109,7 @@ import org.junit.runners.Suite;
 
     CoreVersionRollingUpgradeTest.class,
     PluginVersionRollingUpgradeTest.class,
+    ManagementApiVersionValidationTest.class,
     GridProductVersionSelfTest.class,
     GridAffinityAssignmentV2Test.class,
     GridAffinityAssignmentV2TestNoOptimizations.class,

Reply via email to