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

davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new d7924510b6 [Feature] [Core] Implement config validation for Flink and 
Spark (#11434)
d7924510b6 is described below

commit d7924510b68aaac542ec872cce8f576d75a7c19e
Author: lifeng <[email protected]>
AuthorDate: Mon Aug 10 15:49:02 2026 +0800

    [Feature] [Core] Implement config validation for Flink and Spark (#11434)
---
 seatunnel-core/seatunnel-core-starter/pom.xml      |  22 +
 .../core/starter/utils/ConfigValidationUtils.java  | 448 +++++++++++++++++++++
 .../starter/utils/ConfigValidationUtilsTest.java   | 214 ++++++++++
 .../config/validation_fake_to_inmemory.json        |  33 ++
 .../config/validation_missing_input_table.json     |  26 ++
 .../config/validation_multiple_input_tables.json   |  35 ++
 .../config/validation_sink_assert_failure.json     |  28 ++
 .../seatunnel-flink-starter-common/pom.xml         |  20 +
 .../flink/command/FlinkConfValidateCommand.java    |  25 +-
 .../command/FlinkConfValidateCommandTest.java      |  75 ++++
 .../test/resources/config/fake_to_inmemory.json    |  26 ++
 .../resources/config/invalid_restart_strategy.json |  27 ++
 .../seatunnel-spark-starter-common/pom.xml         |  20 +
 .../spark/command/SparkConfValidateCommand.java    |  20 +-
 .../command/SparkConfValidateCommandTest.java      |  71 ++++
 .../test/resources/config/fake_to_inmemory.json    |  26 ++
 .../test/resources/config/missing_input_table.json |  26 ++
 17 files changed, 1140 insertions(+), 2 deletions(-)

diff --git a/seatunnel-core/seatunnel-core-starter/pom.xml 
b/seatunnel-core/seatunnel-core-starter/pom.xml
index 332a806bc8..73708ae5c4 100644
--- a/seatunnel-core/seatunnel-core-starter/pom.xml
+++ b/seatunnel-core/seatunnel-core-starter/pom.xml
@@ -67,5 +67,27 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>connector-fake</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>seatunnel-e2e-common</artifactId>
+            <version>${project.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>seatunnel-transforms-v2</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 </project>
diff --git 
a/seatunnel-core/seatunnel-core-starter/src/main/java/org/apache/seatunnel/core/starter/utils/ConfigValidationUtils.java
 
b/seatunnel-core/seatunnel-core-starter/src/main/java/org/apache/seatunnel/core/starter/utils/ConfigValidationUtils.java
new file mode 100644
index 0000000000..bc1f2f8b16
--- /dev/null
+++ 
b/seatunnel-core/seatunnel-core-starter/src/main/java/org/apache/seatunnel/core/starter/utils/ConfigValidationUtils.java
@@ -0,0 +1,448 @@
+/*
+ * 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.seatunnel.core.starter.utils;
+
+import org.apache.seatunnel.shade.com.typesafe.config.Config;
+
+import org.apache.seatunnel.api.common.JobContext;
+import org.apache.seatunnel.api.common.PluginIdentifier;
+import org.apache.seatunnel.api.configuration.Option;
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.configuration.util.RequiredOption;
+import org.apache.seatunnel.api.options.EnvCommonOptions;
+import org.apache.seatunnel.api.options.EnvOptionRule;
+import org.apache.seatunnel.api.sink.SeaTunnelSink;
+import org.apache.seatunnel.api.sink.SupportMultiTableSink;
+import org.apache.seatunnel.api.source.SeaTunnelSource;
+import org.apache.seatunnel.api.source.SourceSplit;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.catalog.TablePath;
+import org.apache.seatunnel.api.table.factory.Factory;
+import org.apache.seatunnel.api.table.factory.FactoryUtil;
+import org.apache.seatunnel.api.table.factory.TableSinkFactory;
+import org.apache.seatunnel.api.table.factory.TableSourceFactory;
+import org.apache.seatunnel.api.table.factory.TableTransformFactory;
+import org.apache.seatunnel.api.table.factory.TableTransformFactoryContext;
+import org.apache.seatunnel.api.transform.SeaTunnelTransform;
+import org.apache.seatunnel.common.Constants;
+import org.apache.seatunnel.common.config.CheckResult;
+import org.apache.seatunnel.common.config.Common;
+import org.apache.seatunnel.common.config.TypesafeConfigUtils;
+import org.apache.seatunnel.common.constants.EngineType;
+import org.apache.seatunnel.common.constants.PluginType;
+import org.apache.seatunnel.common.utils.ExceptionUtils;
+import org.apache.seatunnel.common.utils.ReflectionUtils;
+import org.apache.seatunnel.core.starter.exception.ConfigCheckException;
+import org.apache.seatunnel.core.starter.execution.RuntimeEnvironment;
+import 
org.apache.seatunnel.plugin.discovery.seatunnel.SeaTunnelFactoryDiscovery;
+import 
org.apache.seatunnel.plugin.discovery.seatunnel.SeaTunnelSinkPluginDiscovery;
+import 
org.apache.seatunnel.plugin.discovery.seatunnel.SeaTunnelSourcePluginDiscovery;
+
+import scala.Tuple2;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static 
org.apache.seatunnel.api.options.ConnectorCommonOptions.PLUGIN_INPUT;
+import static 
org.apache.seatunnel.api.options.ConnectorCommonOptions.PLUGIN_NAME;
+import static 
org.apache.seatunnel.api.options.ConnectorCommonOptions.PLUGIN_OUTPUT;
+import static 
org.apache.seatunnel.api.table.factory.FactoryUtil.ensureJobModeMatch;
+
+/** Utility methods for validating SeaTunnel job configuration without 
executing a job. */
+@SuppressWarnings({"rawtypes", "unchecked"})
+public final class ConfigValidationUtils {
+
+    private static final BiConsumer<ClassLoader, List<URL>> 
ADD_URL_TO_CLASSLOADER =
+            (classLoader, urls) -> {
+                if (classLoader instanceof URLClassLoader) {
+                    urls.forEach(url -> ReflectionUtils.invoke(classLoader, 
"addURL", url));
+                } else {
+                    try {
+                        Optional<Method> method =
+                                ReflectionUtils.getDeclaredMethod(
+                                        URLClassLoader.class, "addURL", 
URL.class);
+                        if (!method.isPresent()) {
+                            throw new IllegalStateException(
+                                    "Unable to find addURL method from 
URLClassLoader");
+                        }
+                        method.get().setAccessible(true);
+                        for (URL url : urls) {
+                            method.get().invoke(classLoader, url);
+                        }
+                    } catch (Exception e) {
+                        throw new RuntimeException(
+                                "Unsupported classloader: " + 
classLoader.getClass().getName(), e);
+                    }
+                }
+            };
+
+    private ConfigValidationUtils() {}
+
+    public static void validate(Config config) {
+        validate(config, CheckResult.success());
+    }
+
+    public static void validate(Config config, CheckResult checkResult) {
+        if (!checkResult.isSuccess()) {
+            throw new ConfigCheckException(checkResult.getMsg());
+        }
+
+        ClassLoader parentClassLoader = 
Thread.currentThread().getContextClassLoader();
+        ClassLoader validationClassLoader = prepareClassLoader(config, 
parentClassLoader);
+        Thread.currentThread().setContextClassLoader(validationClassLoader);
+        try {
+            validateEnvironmentConfig(config);
+            JobContext jobContext = new JobContext();
+            jobContext.setJobMode(RuntimeEnvironment.getJobMode(config));
+            
jobContext.setEnableCheckpoint(RuntimeEnvironment.getEnableCheckpoint(config));
+
+            List<TableInfo> sourceTables =
+                    validateSources(
+                            TypesafeConfigUtils.getConfigList(
+                                    config, Constants.SOURCE, 
Collections.emptyList()),
+                            jobContext);
+            if (sourceTables.isEmpty()) {
+                throw new ConfigCheckException("At least one source plugin 
must be configured.");
+            }
+
+            List<TableInfo> outputTables =
+                    validateTransforms(
+                            sourceTables,
+                            TypesafeConfigUtils.getConfigList(
+                                    config, Constants.TRANSFORM, 
Collections.emptyList()),
+                            jobContext);
+
+            validateSinks(
+                    outputTables,
+                    TypesafeConfigUtils.getConfigList(
+                            config, Constants.SINK, Collections.emptyList()),
+                    jobContext);
+        } catch (ConfigCheckException e) {
+            throw e;
+        } catch (Exception e) {
+            Throwable rootException = ExceptionUtils.getRootException(e);
+            String message = rootException.getMessage();
+            throw new ConfigCheckException(
+                    message == null || message.isEmpty() ? e.getMessage() : 
message, e);
+        } finally {
+            Thread.currentThread().setContextClassLoader(parentClassLoader);
+        }
+    }
+
+    private static ClassLoader prepareClassLoader(Config config, ClassLoader 
parentClassLoader) {
+        List<URL> additionalJars = new ArrayList<>();
+        if (config.hasPath(Constants.ENV)) {
+            Config envConfig = config.getConfig(Constants.ENV);
+            if (envConfig.hasPath(EnvCommonOptions.JARS.key())) {
+                additionalJars.addAll(
+                        
Common.getThirdPartyJars(envConfig.getString(EnvCommonOptions.JARS.key()))
+                                .stream()
+                                .map(Path::toUri)
+                                .map(ConfigValidationUtils::toUrl)
+                                .collect(Collectors.toList()));
+            }
+        }
+        additionalJars.addAll(
+                
Common.getPluginsJarDependenciesWithoutConnectorDependency().stream()
+                        .map(Path::toUri)
+                        .map(ConfigValidationUtils::toUrl)
+                        .collect(Collectors.toList()));
+        additionalJars.addAll(
+                Common.getLibJars().stream()
+                        .map(Path::toUri)
+                        .map(ConfigValidationUtils::toUrl)
+                        .collect(Collectors.toList()));
+        additionalJars = 
additionalJars.stream().distinct().collect(Collectors.toList());
+        if (additionalJars.isEmpty()) {
+            return parentClassLoader;
+        }
+
+        try {
+            ADD_URL_TO_CLASSLOADER.accept(parentClassLoader, additionalJars);
+            return parentClassLoader;
+        } catch (RuntimeException e) {
+            return new URLClassLoader(additionalJars.toArray(new URL[0]), 
parentClassLoader);
+        }
+    }
+
+    private static URL toUrl(java.net.URI uri) {
+        try {
+            return uri.toURL();
+        } catch (Exception e) {
+            throw new ConfigCheckException("Invalid plugin dependency path: " 
+ uri, e);
+        }
+    }
+
+    private static void validateEnvironmentConfig(Config config) {
+        if (!config.hasPath(Constants.ENV)) {
+            return;
+        }
+        ReadonlyConfig envConfig = 
ReadonlyConfig.fromConfig(config.getConfig(Constants.ENV));
+        OptionRule optionRule = new EnvOptionRule().optionRule();
+        validateOptionTypes(envConfig, optionRule);
+        
org.apache.seatunnel.api.configuration.util.ConfigValidator.of(envConfig)
+                .validate(optionRule);
+    }
+
+    private static void validateOptionTypes(ReadonlyConfig config, OptionRule 
rule) {
+        for (Option<?> option : rule.getOptionalOptions()) {
+            config.getOptional(option);
+        }
+        for (RequiredOption requiredOption : rule.getRequiredOptions()) {
+            for (Option<?> option : requiredOption.getOptions()) {
+                config.getOptional(option);
+            }
+        }
+    }
+
+    private static List<TableInfo> validateSources(
+            List<? extends Config> sourceConfigs, JobContext jobContext) {
+        ClassLoader classLoader = 
Thread.currentThread().getContextClassLoader();
+        SeaTunnelFactoryDiscovery factoryDiscovery =
+                new SeaTunnelFactoryDiscovery(
+                        
org.apache.seatunnel.api.table.factory.TableSourceFactory.class,
+                        ADD_URL_TO_CLASSLOADER);
+        SeaTunnelSourcePluginDiscovery sourcePluginDiscovery =
+                new SeaTunnelSourcePluginDiscovery(ADD_URL_TO_CLASSLOADER);
+        Function<PluginIdentifier, SeaTunnelSource> fallbackCreateSource =
+                sourcePluginDiscovery::createPluginInstance;
+
+        List<TableInfo> sourceTables = new ArrayList<>();
+        for (Config sourceConfig : sourceConfigs) {
+            PluginIdentifier pluginIdentifier =
+                    getPluginIdentifier(sourceConfig, PluginType.SOURCE);
+            TableSourceFactory sourceFactory =
+                    (TableSourceFactory)
+                            factoryDiscovery
+                                    
.createOptionalPluginInstance(pluginIdentifier)
+                                    .orElse(null);
+            if (sourceFactory != null) {
+                
org.apache.seatunnel.api.configuration.util.ConfigValidator.validateUnknownKeys(
+                        ReadonlyConfig.fromConfig(sourceConfig),
+                        sourceFactory.optionRule(),
+                        pluginIdentifier.getPluginName());
+            }
+            ClassLoader sourceClassLoader = 
getFactoryClassLoader(sourceFactory, classLoader);
+            Tuple2<SeaTunnelSource<Object, SourceSplit, Serializable>, 
List<CatalogTable>> source =
+                    FactoryUtil.createAndPrepareSource(
+                            ReadonlyConfig.fromConfig(sourceConfig),
+                            sourceClassLoader,
+                            pluginIdentifier.getPluginName(),
+                            fallbackCreateSource,
+                            sourceFactory,
+                            null);
+
+            source._1().setJobContext(jobContext);
+            ensureJobModeMatch(jobContext, source._1());
+            sourceTables.add(
+                    new TableInfo(
+                            source._2(),
+                            
ReadonlyConfig.fromConfig(sourceConfig).get(PLUGIN_OUTPUT)));
+        }
+        return sourceTables;
+    }
+
+    private static List<TableInfo> validateTransforms(
+            List<TableInfo> upstreamTables,
+            List<? extends Config> transformConfigs,
+            JobContext jobContext) {
+        if (transformConfigs.isEmpty()) {
+            return upstreamTables;
+        }
+
+        ClassLoader classLoader = 
Thread.currentThread().getContextClassLoader();
+        SeaTunnelFactoryDiscovery factoryDiscovery =
+                new SeaTunnelFactoryDiscovery(TableTransformFactory.class, 
ADD_URL_TO_CLASSLOADER);
+        TableInfo defaultInput = upstreamTables.get(0);
+        Map<String, TableInfo> outputTables =
+                upstreamTables.stream()
+                        .collect(
+                                Collectors.toMap(
+                                        TableInfo::getTableName,
+                                        Function.identity(),
+                                        (left, right) -> right,
+                                        LinkedHashMap::new));
+
+        for (Config transformConfig : transformConfigs) {
+            PluginIdentifier pluginIdentifier =
+                    getPluginIdentifier(transformConfig, PluginType.TRANSFORM);
+
+            TableInfo inputTable =
+                    resolveInputTable(
+                                    transformConfig,
+                                    new ArrayList<>(outputTables.values()),
+                                    "Multiple input tables are not supported 
in the current version")
+                            .orElse(defaultInput);
+
+            TableTransformFactory factory =
+                    (TableTransformFactory) 
factoryDiscovery.createPluginInstance(pluginIdentifier);
+            
org.apache.seatunnel.api.configuration.util.ConfigValidator.validateUnknownKeys(
+                    ReadonlyConfig.fromConfig(transformConfig),
+                    factory.optionRule(),
+                    pluginIdentifier.getPluginName());
+            ClassLoader transformClassLoader = getFactoryClassLoader(factory, 
classLoader);
+            TableTransformFactoryContext context =
+                    new TableTransformFactoryContext(
+                            inputTable.getCatalogTables(),
+                            ReadonlyConfig.fromConfig(transformConfig),
+                            transformClassLoader);
+            
org.apache.seatunnel.api.configuration.util.ConfigValidator.of(context.getOptions())
+                    .validate(factory.optionRule());
+            SeaTunnelTransform<?> transform = 
factory.createTransform(context).createTransform();
+            transform.setJobContext(jobContext);
+
+            String pluginOutputIdentifier =
+                    
ReadonlyConfig.fromConfig(transformConfig).get(PLUGIN_OUTPUT);
+            outputTables.put(
+                    pluginOutputIdentifier,
+                    new TableInfo(transform.getProducedCatalogTables(), 
pluginOutputIdentifier));
+        }
+        return new ArrayList<>(outputTables.values());
+    }
+
+    private static void validateSinks(
+            List<TableInfo> upstreamTables,
+            List<? extends Config> sinkConfigs,
+            JobContext jobContext) {
+        if (sinkConfigs.isEmpty()) {
+            throw new ConfigCheckException("At least one sink plugin must be 
configured.");
+        }
+
+        ClassLoader classLoader = 
Thread.currentThread().getContextClassLoader();
+        SeaTunnelFactoryDiscovery factoryDiscovery =
+                new SeaTunnelFactoryDiscovery(TableSinkFactory.class, 
ADD_URL_TO_CLASSLOADER);
+        SeaTunnelSinkPluginDiscovery sinkPluginDiscovery =
+                new SeaTunnelSinkPluginDiscovery(ADD_URL_TO_CLASSLOADER);
+        Function<PluginIdentifier, SeaTunnelSink> fallbackCreateSink =
+                sinkPluginDiscovery::createPluginInstance;
+
+        TableInfo defaultInput = upstreamTables.get(upstreamTables.size() - 1);
+        for (Config sinkConfig : sinkConfigs) {
+            PluginIdentifier pluginIdentifier = 
getPluginIdentifier(sinkConfig, PluginType.SINK);
+            TableSinkFactory sinkFactory =
+                    (TableSinkFactory)
+                            factoryDiscovery
+                                    
.createOptionalPluginInstance(pluginIdentifier)
+                                    .orElse(null);
+            if (sinkFactory != null) {
+                
org.apache.seatunnel.api.configuration.util.ConfigValidator.validateUnknownKeys(
+                        ReadonlyConfig.fromConfig(sinkConfig),
+                        sinkFactory.optionRule(),
+                        pluginIdentifier.getPluginName());
+            }
+            ClassLoader sinkClassLoader = getFactoryClassLoader(sinkFactory, 
classLoader);
+            TableInfo inputTable =
+                    resolveInputTable(
+                                    sinkConfig,
+                                    upstreamTables,
+                                    "Multiple input tables are not supported 
in the current version")
+                            .orElse(defaultInput);
+
+            Map<TablePath, SeaTunnelSink> sinks = new LinkedHashMap<>();
+            for (CatalogTable catalogTable : inputTable.getCatalogTables()) {
+                SeaTunnelSink sink =
+                        FactoryUtil.createAndPrepareSink(
+                                catalogTable,
+                                ReadonlyConfig.fromConfig(sinkConfig),
+                                sinkClassLoader,
+                                pluginIdentifier.getPluginName(),
+                                fallbackCreateSink,
+                                sinkFactory);
+                sink.setJobContext(jobContext);
+                sinks.put(catalogTable.getTableId().toTablePath(), sink);
+            }
+
+            if (!sinks.isEmpty()
+                    && 
sinks.values().stream().allMatch(SupportMultiTableSink.class::isInstance)) {
+                FactoryUtil.createMultiTableSink(
+                        sinks, ReadonlyConfig.fromConfig(sinkConfig), 
sinkClassLoader);
+            }
+        }
+    }
+
+    static ClassLoader getFactoryClassLoader(Factory factory, ClassLoader 
fallbackClassLoader) {
+        return factory == null ? fallbackClassLoader : 
factory.getClass().getClassLoader();
+    }
+
+    private static Optional<TableInfo> resolveInputTable(
+            Config pluginConfig, List<TableInfo> upstreamTables, String 
multiInputMessage) {
+        ReadonlyConfig readonlyConfig = 
ReadonlyConfig.fromConfig(pluginConfig);
+        if (!readonlyConfig.getOptional(PLUGIN_INPUT).isPresent()) {
+            return Optional.empty();
+        }
+
+        List<String> pluginInputIdentifiers = readonlyConfig.get(PLUGIN_INPUT);
+        if (pluginInputIdentifiers.isEmpty()) {
+            throw new ConfigCheckException("plugin_input must not be empty 
when configured");
+        }
+        if (pluginInputIdentifiers.size() > 1) {
+            throw new ConfigCheckException(multiInputMessage);
+        }
+
+        String pluginInputIdentifier = pluginInputIdentifiers.get(0);
+        return Optional.of(
+                upstreamTables.stream()
+                        .filter(info -> 
pluginInputIdentifier.equals(info.getTableName()))
+                        .findFirst()
+                        .orElseThrow(
+                                () ->
+                                        new ConfigCheckException(
+                                                String.format(
+                                                        "table %s not found",
+                                                        
pluginInputIdentifier))));
+    }
+
+    private static PluginIdentifier getPluginIdentifier(
+            Config pluginConfig, PluginType pluginType) {
+        return PluginIdentifier.of(
+                EngineType.SEATUNNEL.getEngine(),
+                pluginType.getType(),
+                pluginConfig.getString(PLUGIN_NAME.key()));
+    }
+
+    private static final class TableInfo {
+        private final List<CatalogTable> catalogTables;
+        private final String tableName;
+
+        private TableInfo(List<CatalogTable> catalogTables, String tableName) {
+            this.catalogTables = catalogTables;
+            this.tableName = tableName;
+        }
+
+        public List<CatalogTable> getCatalogTables() {
+            return catalogTables;
+        }
+
+        public String getTableName() {
+            return tableName;
+        }
+    }
+}
diff --git 
a/seatunnel-core/seatunnel-core-starter/src/test/java/org/apache/seatunnel/core/starter/utils/ConfigValidationUtilsTest.java
 
b/seatunnel-core/seatunnel-core-starter/src/test/java/org/apache/seatunnel/core/starter/utils/ConfigValidationUtilsTest.java
new file mode 100644
index 0000000000..1ef1b340fc
--- /dev/null
+++ 
b/seatunnel-core/seatunnel-core-starter/src/test/java/org/apache/seatunnel/core/starter/utils/ConfigValidationUtilsTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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.seatunnel.core.starter.utils;
+
+import org.apache.seatunnel.shade.com.typesafe.config.Config;
+import org.apache.seatunnel.shade.com.typesafe.config.ConfigValueFactory;
+
+import org.apache.seatunnel.api.table.factory.Factory;
+import org.apache.seatunnel.common.Constants;
+import org.apache.seatunnel.core.starter.exception.ConfigCheckException;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Proxy;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.stream.Collectors;
+
+class ConfigValidationUtilsTest {
+
+    @Test
+    void shouldUseFactoryClassLoader() throws Exception {
+        ClassLoader parentClassLoader = 
ConfigValidationUtilsTest.class.getClassLoader();
+        try (URLClassLoader factoryClassLoader =
+                new URLClassLoader(new URL[0], parentClassLoader)) {
+            Factory factory =
+                    (Factory)
+                            Proxy.newProxyInstance(
+                                    factoryClassLoader,
+                                    new Class<?>[] {Factory.class},
+                                    (proxy, method, args) -> null);
+
+            Assertions.assertSame(
+                    factoryClassLoader,
+                    ConfigValidationUtils.getFactoryClassLoader(factory, 
parentClassLoader));
+        }
+    }
+
+    @Test
+    void shouldUseFallbackClassLoaderWhenFactoryIsMissing() {
+        ClassLoader fallbackClassLoader = 
ConfigValidationUtilsTest.class.getClassLoader();
+
+        Assertions.assertSame(
+                fallbackClassLoader,
+                ConfigValidationUtils.getFactoryClassLoader(null, 
fallbackClassLoader));
+    }
+
+    @Test
+    void shouldRestoreContextClassLoaderAfterValidation() throws Exception {
+        Config config =
+                
ConfigBuilder.of(getResourcePath("/config/validation_fake_to_inmemory.json"));
+        ClassLoader originalClassLoader = 
Thread.currentThread().getContextClassLoader();
+        try (URLClassLoader validationClassLoader =
+                new URLClassLoader(new URL[0], originalClassLoader)) {
+            
Thread.currentThread().setContextClassLoader(validationClassLoader);
+
+            Assertions.assertDoesNotThrow(() -> 
ConfigValidationUtils.validate(config));
+            Assertions.assertSame(
+                    validationClassLoader, 
Thread.currentThread().getContextClassLoader());
+        } finally {
+            Thread.currentThread().setContextClassLoader(originalClassLoader);
+        }
+    }
+
+    @Test
+    void shouldFailWhenConnectorOptionIsUnknown() throws URISyntaxException {
+        Config config =
+                withPluginOption(
+                        ConfigBuilder.of(
+                                
getResourcePath("/config/validation_fake_to_inmemory.json")),
+                        Constants.SOURCE,
+                        0,
+                        "unknown_option",
+                        true);
+
+        ConfigCheckException exception =
+                Assertions.assertThrows(
+                        ConfigCheckException.class, () -> 
ConfigValidationUtils.validate(config));
+
+        
Assertions.assertTrue(exception.getMessage().contains("unknown_option"));
+    }
+
+    @Test
+    void shouldFailWhenPluginInputIsEmpty() throws URISyntaxException {
+        Config config =
+                withPluginOption(
+                        ConfigBuilder.of(
+                                
getResourcePath("/config/validation_fake_to_inmemory.json")),
+                        Constants.SINK,
+                        0,
+                        "plugin_input",
+                        java.util.Collections.emptyList());
+
+        ConfigCheckException exception =
+                Assertions.assertThrows(
+                        ConfigCheckException.class, () -> 
ConfigValidationUtils.validate(config));
+
+        Assertions.assertEquals(
+                "plugin_input must not be empty when configured", 
exception.getMessage());
+    }
+
+    @Test
+    void shouldValidateHappyPathConfig() throws URISyntaxException {
+        Config config =
+                
ConfigBuilder.of(getResourcePath("/config/validation_fake_to_inmemory.json"));
+        Assertions.assertDoesNotThrow(() -> 
ConfigValidationUtils.validate(config));
+    }
+
+    @Test
+    void shouldFailWhenSinkInputTableIsMissing() throws URISyntaxException {
+        Config config =
+                
ConfigBuilder.of(getResourcePath("/config/validation_missing_input_table.json"));
+
+        ConfigCheckException exception =
+                Assertions.assertThrows(
+                        ConfigCheckException.class, () -> 
ConfigValidationUtils.validate(config));
+
+        Assertions.assertEquals("table missing_table not found", 
exception.getMessage());
+    }
+
+    @Test
+    void shouldFailWhenSinkHasMultipleInputTables() throws URISyntaxException {
+        Config config =
+                
ConfigBuilder.of(getResourcePath("/config/validation_multiple_input_tables.json"));
+
+        ConfigCheckException exception =
+                Assertions.assertThrows(
+                        ConfigCheckException.class, () -> 
ConfigValidationUtils.validate(config));
+
+        Assertions.assertEquals(
+                "Multiple input tables are not supported in the current 
version",
+                exception.getMessage());
+    }
+
+    @Test
+    void shouldFailWhenSinkFactoryRejectsConfig() throws URISyntaxException {
+        Config config =
+                
ConfigBuilder.of(getResourcePath("/config/validation_sink_assert_failure.json"));
+
+        ConfigCheckException exception =
+                Assertions.assertThrows(
+                        ConfigCheckException.class, () -> 
ConfigValidationUtils.validate(config));
+
+        Assertions.assertTrue(exception.getMessage().contains("assert key and 
value not match"));
+    }
+
+    @Test
+    void shouldFailWhenSourceSectionIsMissing() throws URISyntaxException {
+        Config config =
+                
ConfigBuilder.of(getResourcePath("/config/validation_fake_to_inmemory.json"))
+                        .withoutPath(Constants.SOURCE);
+
+        ConfigCheckException exception =
+                Assertions.assertThrows(
+                        ConfigCheckException.class, () -> 
ConfigValidationUtils.validate(config));
+
+        Assertions.assertEquals(
+                "At least one source plugin must be configured.", 
exception.getMessage());
+    }
+
+    @Test
+    void shouldFailWhenSinkSectionIsMissing() throws URISyntaxException {
+        Config config =
+                
ConfigBuilder.of(getResourcePath("/config/validation_fake_to_inmemory.json"))
+                        .withoutPath(Constants.SINK);
+
+        ConfigCheckException exception =
+                Assertions.assertThrows(
+                        ConfigCheckException.class, () -> 
ConfigValidationUtils.validate(config));
+
+        Assertions.assertEquals(
+                "At least one sink plugin must be configured.", 
exception.getMessage());
+    }
+
+    private static Path getResourcePath(String resourcePath) throws 
URISyntaxException {
+        URL resource = 
ConfigValidationUtilsTest.class.getResource(resourcePath);
+        Assertions.assertNotNull(resource);
+        return Paths.get(resource.toURI());
+    }
+
+    private static Config withPluginOption(
+            Config config, String section, int index, String key, Object 
value) {
+        List<Object> pluginConfigs =
+                config.getConfigList(section).stream()
+                        .map(
+                                pluginConfig ->
+                                        pluginConfig
+                                                .withValue(
+                                                        key, 
ConfigValueFactory.fromAnyRef(value))
+                                                .root())
+                        .collect(Collectors.toList());
+        return config.withValue(section, 
ConfigValueFactory.fromIterable(pluginConfigs));
+    }
+}
diff --git 
a/seatunnel-core/seatunnel-core-starter/src/test/resources/config/validation_fake_to_inmemory.json
 
b/seatunnel-core/seatunnel-core-starter/src/test/resources/config/validation_fake_to_inmemory.json
new file mode 100644
index 0000000000..415174c623
--- /dev/null
+++ 
b/seatunnel-core/seatunnel-core-starter/src/test/resources/config/validation_fake_to_inmemory.json
@@ -0,0 +1,33 @@
+{
+  "env": {
+    "parallelism": 1,
+    "job.mode": "BATCH"
+  },
+  "source": [
+    {
+      "plugin_name": "FakeSource",
+      "plugin_output": "fake_table",
+      "row.num": 10,
+      "schema": {
+        "fields": {
+          "name": "string",
+          "age": "int"
+        }
+      }
+    }
+  ],
+  "transform": [
+    {
+      "plugin_name": "Filter",
+      "plugin_input": "fake_table",
+      "plugin_output": "filtered_table",
+      "include_fields": ["name"]
+    }
+  ],
+  "sink": [
+    {
+      "plugin_name": "InMemory",
+      "plugin_input": "filtered_table"
+    }
+  ]
+}
diff --git 
a/seatunnel-core/seatunnel-core-starter/src/test/resources/config/validation_missing_input_table.json
 
b/seatunnel-core/seatunnel-core-starter/src/test/resources/config/validation_missing_input_table.json
new file mode 100644
index 0000000000..c6d1043e02
--- /dev/null
+++ 
b/seatunnel-core/seatunnel-core-starter/src/test/resources/config/validation_missing_input_table.json
@@ -0,0 +1,26 @@
+{
+  "env": {
+    "parallelism": 1,
+    "job.mode": "BATCH"
+  },
+  "source": [
+    {
+      "plugin_name": "FakeSource",
+      "plugin_output": "fake_table",
+      "row.num": 10,
+      "schema": {
+        "fields": {
+          "name": "string",
+          "age": "int"
+        }
+      }
+    }
+  ],
+  "transform": [],
+  "sink": [
+    {
+      "plugin_name": "InMemory",
+      "plugin_input": "missing_table"
+    }
+  ]
+}
diff --git 
a/seatunnel-core/seatunnel-core-starter/src/test/resources/config/validation_multiple_input_tables.json
 
b/seatunnel-core/seatunnel-core-starter/src/test/resources/config/validation_multiple_input_tables.json
new file mode 100644
index 0000000000..3e3bddc305
--- /dev/null
+++ 
b/seatunnel-core/seatunnel-core-starter/src/test/resources/config/validation_multiple_input_tables.json
@@ -0,0 +1,35 @@
+{
+  "env": {
+    "parallelism": 1,
+    "job.mode": "BATCH"
+  },
+  "source": [
+    {
+      "plugin_name": "FakeSource",
+      "plugin_output": "first_table",
+      "row.num": 10,
+      "schema": {
+        "fields": {
+          "name": "string"
+        }
+      }
+    },
+    {
+      "plugin_name": "FakeSource",
+      "plugin_output": "second_table",
+      "row.num": 10,
+      "schema": {
+        "fields": {
+          "name": "string"
+        }
+      }
+    }
+  ],
+  "transform": [],
+  "sink": [
+    {
+      "plugin_name": "InMemory",
+      "plugin_input": ["first_table", "second_table"]
+    }
+  ]
+}
diff --git 
a/seatunnel-core/seatunnel-core-starter/src/test/resources/config/validation_sink_assert_failure.json
 
b/seatunnel-core/seatunnel-core-starter/src/test/resources/config/validation_sink_assert_failure.json
new file mode 100644
index 0000000000..7b9843f3e3
--- /dev/null
+++ 
b/seatunnel-core/seatunnel-core-starter/src/test/resources/config/validation_sink_assert_failure.json
@@ -0,0 +1,28 @@
+{
+  "env": {
+    "parallelism": 1,
+    "job.mode": "BATCH"
+  },
+  "source": [
+    {
+      "plugin_name": "FakeSource",
+      "plugin_output": "fake_table",
+      "row.num": 10,
+      "schema": {
+        "fields": {
+          "name": "string",
+          "age": "int"
+        }
+      }
+    }
+  ],
+  "transform": [],
+  "sink": [
+    {
+      "plugin_name": "InMemory",
+      "plugin_input": "fake_table",
+      "assert_options_key": "left",
+      "assert_options_value": "right"
+    }
+  ]
+}
diff --git 
a/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/pom.xml 
b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/pom.xml
index ee8481127f..cd68112bed 100644
--- 
a/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/pom.xml
+++ 
b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/pom.xml
@@ -74,6 +74,26 @@
             <version>${project.version}</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>connector-console</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>connector-fake</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.junit.platform</groupId>
+            <artifactId>junit-platform-launcher</artifactId>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 
 </project>
diff --git 
a/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/main/java/org/apache/seatunnel/core/starter/flink/command/FlinkConfValidateCommand.java
 
b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/main/java/org/apache/seatunnel/core/starter/flink/command/FlinkConfValidateCommand.java
index 78921e6980..a6de3fce74 100644
--- 
a/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/main/java/org/apache/seatunnel/core/starter/flink/command/FlinkConfValidateCommand.java
+++ 
b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/main/java/org/apache/seatunnel/core/starter/flink/command/FlinkConfValidateCommand.java
@@ -17,15 +17,26 @@
 
 package org.apache.seatunnel.core.starter.flink.command;
 
+import org.apache.seatunnel.shade.com.typesafe.config.Config;
+import org.apache.seatunnel.shade.com.typesafe.config.ConfigUtil;
+import org.apache.seatunnel.shade.com.typesafe.config.ConfigValueFactory;
+
+import org.apache.seatunnel.api.metalake.MetalakeConfigUtils;
+import org.apache.seatunnel.common.Constants;
 import org.apache.seatunnel.core.starter.command.Command;
 import org.apache.seatunnel.core.starter.exception.ConfigCheckException;
 import org.apache.seatunnel.core.starter.flink.args.FlinkCommandArgs;
+import org.apache.seatunnel.core.starter.flink.utils.EnvironmentUtil;
+import org.apache.seatunnel.core.starter.utils.ConfigBuilder;
+import org.apache.seatunnel.core.starter.utils.ConfigValidationUtils;
 import org.apache.seatunnel.core.starter.utils.FileUtils;
 
 import lombok.extern.slf4j.Slf4j;
 
 import java.nio.file.Path;
 
+import static 
org.apache.seatunnel.core.starter.utils.FileUtils.checkConfigExist;
+
 /** Use to validate the configuration of the SeaTunnel API. */
 @Slf4j
 public class FlinkConfValidateCommand implements Command<FlinkCommandArgs> {
@@ -39,6 +50,18 @@ public class FlinkConfValidateCommand implements 
Command<FlinkCommandArgs> {
     @Override
     public void execute() throws ConfigCheckException {
         Path configPath = FileUtils.getConfigPath(flinkCommandArgs);
-        // TODO: validate the config by new api
+        checkConfigExist(configPath);
+        Config config =
+                MetalakeConfigUtils.getMetalakeConfig(
+                        ConfigBuilder.of(configPath, 
flinkCommandArgs.getVariables()));
+        if (!flinkCommandArgs.getJobName().equals(Constants.LOGO)) {
+            config =
+                    config.withValue(
+                            ConfigUtil.joinPath("env", "job.name"),
+                            
ConfigValueFactory.fromAnyRef(flinkCommandArgs.getJobName()));
+        }
+        ConfigValidationUtils.validate(
+                config, 
EnvironmentUtil.checkRestartStrategy(config.getConfig("env")));
+        log.info("Config validation succeeded: {}", configPath);
     }
 }
diff --git 
a/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/test/java/org/apache/seatunnel/core/starter/flink/command/FlinkConfValidateCommandTest.java
 
b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/test/java/org/apache/seatunnel/core/starter/flink/command/FlinkConfValidateCommandTest.java
new file mode 100644
index 0000000000..9d596e2b07
--- /dev/null
+++ 
b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/test/java/org/apache/seatunnel/core/starter/flink/command/FlinkConfValidateCommandTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.seatunnel.core.starter.flink.command;
+
+import org.apache.seatunnel.common.config.DeployMode;
+import org.apache.seatunnel.core.starter.command.Command;
+import org.apache.seatunnel.core.starter.exception.ConfigCheckException;
+import org.apache.seatunnel.core.starter.flink.args.FlinkCommandArgs;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+
+class FlinkConfValidateCommandTest {
+
+    @Test
+    void shouldValidateFlinkConfig() throws URISyntaxException {
+        FlinkCommandArgs commandArgs = 
buildArgs("/config/fake_to_inmemory.json");
+        Command<?> command = commandArgs.buildCommand();
+
+        Assertions.assertInstanceOf(FlinkConfValidateCommand.class, command);
+        Assertions.assertDoesNotThrow(command::execute);
+    }
+
+    @Test
+    void shouldRejectInvalidRestartStrategy() throws URISyntaxException {
+        FlinkCommandArgs commandArgs = 
buildArgs("/config/invalid_restart_strategy.json");
+        Command<?> command = commandArgs.buildCommand();
+
+        Assertions.assertInstanceOf(FlinkConfValidateCommand.class, command);
+        ConfigCheckException exception =
+                Assertions.assertThrows(ConfigCheckException.class, 
command::execute);
+
+        Assertions.assertTrue(
+                exception
+                        .getMessage()
+                        .contains(
+                                "fixed-delay restart strategy must set 
[execution.restart.attempts],[execution.restart.delayBetweenAttempts]"));
+    }
+
+    private static FlinkCommandArgs buildArgs(String resourcePath) throws 
URISyntaxException {
+        FlinkCommandArgs commandArgs = new FlinkCommandArgs();
+        commandArgs.setDeployMode(DeployMode.RUN);
+        commandArgs.setCheckConfig(true);
+        commandArgs.setConfigFile(getResourcePath(resourcePath).toString());
+        commandArgs.setVariables(Collections.emptyList());
+        return commandArgs;
+    }
+
+    private static Path getResourcePath(String resourcePath) throws 
URISyntaxException {
+        URL resource = 
FlinkConfValidateCommandTest.class.getResource(resourcePath);
+        Assertions.assertNotNull(resource);
+        return Paths.get(resource.toURI());
+    }
+}
diff --git 
a/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/test/resources/config/fake_to_inmemory.json
 
b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/test/resources/config/fake_to_inmemory.json
new file mode 100644
index 0000000000..66fed43e3c
--- /dev/null
+++ 
b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/test/resources/config/fake_to_inmemory.json
@@ -0,0 +1,26 @@
+{
+  "env": {
+    "parallelism": 1,
+    "job.mode": "BATCH"
+  },
+  "source": [
+    {
+      "plugin_name": "FakeSource",
+      "plugin_output": "fake_table",
+      "row.num": 10,
+      "schema": {
+        "fields": {
+          "name": "string",
+          "age": "int"
+        }
+      }
+    }
+  ],
+  "transform": [],
+  "sink": [
+    {
+      "plugin_name": "Console",
+      "plugin_input": "fake_table"
+    }
+  ]
+}
diff --git 
a/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/test/resources/config/invalid_restart_strategy.json
 
b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/test/resources/config/invalid_restart_strategy.json
new file mode 100644
index 0000000000..872c497e90
--- /dev/null
+++ 
b/seatunnel-core/seatunnel-flink-starter/seatunnel-flink-starter-common/src/test/resources/config/invalid_restart_strategy.json
@@ -0,0 +1,27 @@
+{
+  "env": {
+    "parallelism": 1,
+    "job.mode": "BATCH",
+    "execution.restart.strategy": "fixed-delay"
+  },
+  "source": [
+    {
+      "plugin_name": "FakeSource",
+      "plugin_output": "fake_table",
+      "row.num": 10,
+      "schema": {
+        "fields": {
+          "name": "string",
+          "age": "int"
+        }
+      }
+    }
+  ],
+  "transform": [],
+  "sink": [
+    {
+      "plugin_name": "Console",
+      "plugin_input": "fake_table"
+    }
+  ]
+}
diff --git 
a/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/pom.xml 
b/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/pom.xml
index ec4df4bfa9..35bc36b12e 100644
--- 
a/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/pom.xml
+++ 
b/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/pom.xml
@@ -64,6 +64,26 @@
             <scope>${spark.scope}</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>connector-console</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>connector-fake</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.junit.platform</groupId>
+            <artifactId>junit-platform-launcher</artifactId>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 
 </project>
diff --git 
a/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/src/main/java/org/apache/seatunnel/core/starter/spark/command/SparkConfValidateCommand.java
 
b/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/src/main/java/org/apache/seatunnel/core/starter/spark/command/SparkConfValidateCommand.java
index c198dff3d0..eb948b6a8e 100644
--- 
a/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/src/main/java/org/apache/seatunnel/core/starter/spark/command/SparkConfValidateCommand.java
+++ 
b/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/src/main/java/org/apache/seatunnel/core/starter/spark/command/SparkConfValidateCommand.java
@@ -17,9 +17,17 @@
 
 package org.apache.seatunnel.core.starter.spark.command;
 
+import org.apache.seatunnel.shade.com.typesafe.config.Config;
+import org.apache.seatunnel.shade.com.typesafe.config.ConfigUtil;
+import org.apache.seatunnel.shade.com.typesafe.config.ConfigValueFactory;
+
+import org.apache.seatunnel.api.metalake.MetalakeConfigUtils;
+import org.apache.seatunnel.common.Constants;
 import org.apache.seatunnel.core.starter.command.Command;
 import org.apache.seatunnel.core.starter.exception.ConfigCheckException;
 import org.apache.seatunnel.core.starter.spark.args.SparkCommandArgs;
+import org.apache.seatunnel.core.starter.utils.ConfigBuilder;
+import org.apache.seatunnel.core.starter.utils.ConfigValidationUtils;
 
 import org.apache.spark.SparkFiles;
 
@@ -44,6 +52,16 @@ public class SparkConfValidateCommand implements 
Command<SparkCommandArgs> {
         Path configPath =
                 SparkTaskExecuteCommand.resolveConfigPath(sparkCommandArgs, 
SparkFiles::get);
         checkConfigExist(configPath);
-        // TODO: validate the config by new api
+        Config config =
+                MetalakeConfigUtils.getMetalakeConfig(
+                        ConfigBuilder.of(configPath, 
sparkCommandArgs.getVariables()));
+        if (!sparkCommandArgs.getJobName().equals(Constants.LOGO)) {
+            config =
+                    config.withValue(
+                            ConfigUtil.joinPath("env", "job.name"),
+                            
ConfigValueFactory.fromAnyRef(sparkCommandArgs.getJobName()));
+        }
+        ConfigValidationUtils.validate(config);
+        log.info("Config validation succeeded: {}", configPath);
     }
 }
diff --git 
a/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/src/test/java/org/apache/seatunnel/core/starter/spark/command/SparkConfValidateCommandTest.java
 
b/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/src/test/java/org/apache/seatunnel/core/starter/spark/command/SparkConfValidateCommandTest.java
new file mode 100644
index 0000000000..290b4568a2
--- /dev/null
+++ 
b/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/src/test/java/org/apache/seatunnel/core/starter/spark/command/SparkConfValidateCommandTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.seatunnel.core.starter.spark.command;
+
+import org.apache.seatunnel.common.config.DeployMode;
+import org.apache.seatunnel.core.starter.command.Command;
+import org.apache.seatunnel.core.starter.exception.ConfigCheckException;
+import org.apache.seatunnel.core.starter.spark.args.SparkCommandArgs;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+
+class SparkConfValidateCommandTest {
+
+    @Test
+    void shouldValidateSparkConfig() throws URISyntaxException {
+        SparkCommandArgs commandArgs = 
buildArgs("/config/fake_to_inmemory.json");
+        Command<?> command = commandArgs.buildCommand();
+
+        Assertions.assertInstanceOf(SparkConfValidateCommand.class, command);
+        Assertions.assertDoesNotThrow(command::execute);
+    }
+
+    @Test
+    void shouldRejectMissingSinkInputTable() throws URISyntaxException {
+        SparkCommandArgs commandArgs = 
buildArgs("/config/missing_input_table.json");
+        Command<?> command = commandArgs.buildCommand();
+
+        Assertions.assertInstanceOf(SparkConfValidateCommand.class, command);
+        ConfigCheckException exception =
+                Assertions.assertThrows(ConfigCheckException.class, 
command::execute);
+
+        Assertions.assertEquals("table missing_table not found", 
exception.getMessage());
+    }
+
+    private static SparkCommandArgs buildArgs(String resourcePath) throws 
URISyntaxException {
+        SparkCommandArgs commandArgs = new SparkCommandArgs();
+        commandArgs.setDeployMode(DeployMode.CLIENT);
+        commandArgs.setCheckConfig(true);
+        commandArgs.setConfigFile(getResourcePath(resourcePath).toString());
+        commandArgs.setVariables(Collections.emptyList());
+        return commandArgs;
+    }
+
+    private static Path getResourcePath(String resourcePath) throws 
URISyntaxException {
+        URL resource = 
SparkConfValidateCommandTest.class.getResource(resourcePath);
+        Assertions.assertNotNull(resource);
+        return Paths.get(resource.toURI());
+    }
+}
diff --git 
a/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/src/test/resources/config/fake_to_inmemory.json
 
b/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/src/test/resources/config/fake_to_inmemory.json
new file mode 100644
index 0000000000..66fed43e3c
--- /dev/null
+++ 
b/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/src/test/resources/config/fake_to_inmemory.json
@@ -0,0 +1,26 @@
+{
+  "env": {
+    "parallelism": 1,
+    "job.mode": "BATCH"
+  },
+  "source": [
+    {
+      "plugin_name": "FakeSource",
+      "plugin_output": "fake_table",
+      "row.num": 10,
+      "schema": {
+        "fields": {
+          "name": "string",
+          "age": "int"
+        }
+      }
+    }
+  ],
+  "transform": [],
+  "sink": [
+    {
+      "plugin_name": "Console",
+      "plugin_input": "fake_table"
+    }
+  ]
+}
diff --git 
a/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/src/test/resources/config/missing_input_table.json
 
b/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/src/test/resources/config/missing_input_table.json
new file mode 100644
index 0000000000..f21f963ddc
--- /dev/null
+++ 
b/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-starter-common/src/test/resources/config/missing_input_table.json
@@ -0,0 +1,26 @@
+{
+  "env": {
+    "parallelism": 1,
+    "job.mode": "BATCH"
+  },
+  "source": [
+    {
+      "plugin_name": "FakeSource",
+      "plugin_output": "fake_table",
+      "row.num": 10,
+      "schema": {
+        "fields": {
+          "name": "string",
+          "age": "int"
+        }
+      }
+    }
+  ],
+  "transform": [],
+  "sink": [
+    {
+      "plugin_name": "Console",
+      "plugin_input": "missing_table"
+    }
+  ]
+}

Reply via email to