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 735cf43052 [Improve][Core] Download connector plugins over HTTPS
735cf43052 is described below
commit 735cf430525622f6f2d1c8cebf17598605aca232
Author: Daniel <[email protected]>
AuthorDate: Thu Jul 30 01:59:54 2026 +0800
[Improve][Core] Download connector plugins over HTTPS
Merge pull request #11555 from DanielLeens/davidzollo_install-plugin-https
---
bin/install-plugin.sh | 270 ++++++++-
docs/en/getting-started/locally/deployment.md | 9 +
.../introduction/concepts/incompatible-changes.md | 6 +
docs/zh/getting-started/locally/deployment.md | 9 +
.../introduction/concepts/incompatible-changes.md | 6 +
.../installer/InstallPluginScriptTest.java | 617 +++++++++++++++++++++
6 files changed, 897 insertions(+), 20 deletions(-)
diff --git a/bin/install-plugin.sh b/bin/install-plugin.sh
index 3059bb081d..77c146c8ce 100755
--- a/bin/install-plugin.sh
+++ b/bin/install-plugin.sh
@@ -16,37 +16,267 @@
# limitations under the License.
#
-#This script is used to download the connector plug-ins required during the
running process.
-#All are downloaded by default. You can also choose what you need.
-#You only need to configure the plug-in name in config/plugin_config.
+# This script downloads the connector plug-ins selected in
config/plugin_config.
-# get seatunnel home
-SEATUNNEL_HOME=$(cd $(dirname $0);cd ../;pwd)
+SEATUNNEL_HOME=$(cd "$(dirname "$0")"; cd ../; pwd)
-# connector default version is 3.0.0, you can also choose a custom version.
eg: 3.0.0: sh install-plugin.sh 3.0.0
+# Connector default version is 3.0.0. You can also choose a custom version.
version=3.0.0
-
if [ -n "$1" ]; then
version="$1"
fi
-echo "Install SeaTunnel connectors plugins, usage version is ${version}"
+download_method=${SEATUNNEL_PLUGIN_DOWNLOAD_METHOD:-https}
+maven_repository=${SEATUNNEL_MAVEN_REPOSITORY:-https://repo.maven.apache.org/maven2}
+maven_repository=${maven_repository%/}
+temporary_file=
+checksum_file=
+temporary_directory=
+
+cleanup_temporary_files() {
+ if [ -n "$temporary_file" ]; then
+ rm -f "$temporary_file"
+ fi
+ if [ -n "$checksum_file" ]; then
+ rm -f "$checksum_file"
+ fi
+ if [ -n "$temporary_directory" ]; then
+ rmdir "$temporary_directory" 2>/dev/null || true
+ fi
+}
+
+trap cleanup_temporary_files EXIT
+trap 'exit 1' HUP INT TERM
+
+case "$download_method" in
+ https | maven)
+ ;;
+ *)
+ echo "Error: SEATUNNEL_PLUGIN_DOWNLOAD_METHOD must be 'https' or
'maven'." >&2
+ exit 1
+ ;;
+esac
+
+# Maven resolves unique snapshots, dynamic versions, mirrors, credentials, and
repository policies.
+case "$version" in
+ *-SNAPSHOT | LATEST | RELEASE | *"["* | *"]"* | *"("* | *")"* | *","* |
*"+"*)
+ download_method=maven
+ ;;
+esac
-# create the connectors directory
-if [ ! -d ${SEATUNNEL_HOME}/connectors ];
- then
- mkdir ${SEATUNNEL_HOME}/connectors
- echo "create connectors directory"
+if [ "$download_method" = "https" ]; then
+ case "$version" in
+ *[!A-Za-z0-9._-]* | "")
+ echo "Error: invalid connector release version '${version}'." >&2
+ exit 1
+ ;;
+ esac
+ case "$maven_repository" in
+ https://*)
+ ;;
+ *)
+ echo "Error: SEATUNNEL_MAVEN_REPOSITORY must use HTTPS." >&2
+ exit 1
+ ;;
+ esac
+ if ! command -v curl >/dev/null 2>&1; then
+ echo "Error: curl is required to download connector plugins over
HTTPS." >&2
+ exit 1
+ fi
+ if ! command -v mktemp >/dev/null 2>&1; then
+ echo "Error: mktemp is required to create secure temporary files." >&2
+ exit 1
+ fi
+ if ! command -v sha512sum >/dev/null 2>&1 &&
+ ! command -v sha1sum >/dev/null 2>&1 &&
+ ! command -v shasum >/dev/null 2>&1 &&
+ ! command -v openssl >/dev/null 2>&1; then
+ echo "Error: a SHA-512 or SHA-1 checksum tool is required." >&2
+ exit 1
+ fi
fi
-while read line; do
- first_char=$(echo "$line" | cut -c 1)
+echo "Install SeaTunnel connector plugins, version: ${version}, method:
${download_method}"
+
+if [ ! -d "${SEATUNNEL_HOME}/connectors" ]; then
+ mkdir -p "${SEATUNNEL_HOME}/connectors"
+ echo "Create connectors directory"
+fi
+
+download_https() {
+ source_url=$1
+ output_file=$2
+ allow_not_found=${3:-false}
+
+ http_status=$(
+ curl --fail --location --retry 3 --connect-timeout 10 \
+ --proto '=https' --proto-redir '=https' \
+ --write-out '%{http_code}' \
+ --output "$output_file" "$source_url"
+ )
+ curl_exit_code=$?
+ if [ "$curl_exit_code" -eq 0 ]; then
+ return 0
+ fi
+
+ if [ "$allow_not_found" = "true" ] && [ "$http_status" = "404" ]; then
+ return 44
+ fi
+ return "$curl_exit_code"
+}
+
+calculate_checksum() {
+ checksum_calculation_algorithm=$1
+ checksum_input_file=$2
- if [ "$first_char" != "-" ] && [ "$first_char" != "#" ] && [ ! -z
$first_char ]
- then
- echo "install connector : " $line
- ${SEATUNNEL_HOME}/mvnw dependency:get -Dtransitive=false
-DgroupId=org.apache.seatunnel -DartifactId=${line} -Dversion=${version}
-Ddest=${SEATUNNEL_HOME}/connectors
+ case "$checksum_calculation_algorithm" in
+ sha512)
+ if command -v sha512sum >/dev/null 2>&1; then
+ sha512sum "$checksum_input_file" | awk '{print $1}'
+ elif command -v shasum >/dev/null 2>&1; then
+ shasum -a 512 "$checksum_input_file" | awk '{print $1}'
+ elif command -v openssl >/dev/null 2>&1; then
+ openssl dgst -sha512 "$checksum_input_file" | awk '{print $NF}'
+ else
+ return 1
+ fi
+ ;;
+ sha1)
+ if command -v sha1sum >/dev/null 2>&1; then
+ sha1sum "$checksum_input_file" | awk '{print $1}'
+ elif command -v shasum >/dev/null 2>&1; then
+ shasum -a 1 "$checksum_input_file" | awk '{print $1}'
+ elif command -v openssl >/dev/null 2>&1; then
+ openssl dgst -sha1 "$checksum_input_file" | awk '{print $NF}'
+ else
+ return 1
+ fi
+ ;;
+ esac
+}
+
+supports_checksum_algorithm() {
+ checksum_support_algorithm=$1
+
+ case "$checksum_support_algorithm" in
+ sha512)
+ command -v sha512sum >/dev/null 2>&1 ||
+ command -v shasum >/dev/null 2>&1 ||
+ command -v openssl >/dev/null 2>&1
+ ;;
+ sha1)
+ command -v sha1sum >/dev/null 2>&1 ||
+ command -v shasum >/dev/null 2>&1 ||
+ command -v openssl >/dev/null 2>&1
+ ;;
+ esac
+}
+
+verify_checksum() {
+ checksum_artifact_file=$1
+ checksum_sidecar_path=$2
+ checksum_verification_algorithm=$3
+
+ expected_checksum=$(awk 'NR == 1 {print $1}' "$checksum_sidecar_path" | tr
'A-F' 'a-f')
+ actual_checksum=$(
+ calculate_checksum "$checksum_verification_algorithm"
"$checksum_artifact_file"
+ ) || return 1
+ actual_checksum=$(printf '%s' "$actual_checksum" | tr 'A-F' 'a-f')
+
+ case "$checksum_verification_algorithm:$expected_checksum" in
+
sha512:????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
| \
+ sha1:????????????????????????????????????????)
+ ;;
+ *)
+ return 1
+ ;;
+ esac
+
+ [ "$expected_checksum" = "$actual_checksum" ]
+}
+
+validate_jar() {
+ artifact_file=$1
+ magic_bytes=$(od -An -tx1 -N2 "$artifact_file" 2>/dev/null | tr -d
'[:space:]')
+ [ "$magic_bytes" = "504b" ]
+}
+
+download_release_plugin() {
+ artifact_id=$1
+ artifact_name="${artifact_id}-${version}.jar"
+
artifact_url="${maven_repository}/org/apache/seatunnel/${artifact_id}/${version}/${artifact_name}"
+ target_file="${SEATUNNEL_HOME}/connectors/${artifact_name}"
+ temporary_directory=$(
+ mktemp -d "${SEATUNNEL_HOME}/connectors/.install-plugin.XXXXXX"
+ ) || return 1
+ temporary_file="${temporary_directory}/${artifact_name}"
+ checksum_file="${temporary_directory}/${artifact_name}.checksum"
+
+ echo "Install connector: ${artifact_id}"
+ download_https "$artifact_url" "$temporary_file" || return 1
+
+ if supports_checksum_algorithm sha512; then
+ download_https "${artifact_url}.sha512" "$checksum_file" true
2>/dev/null
+ sha512_download_result=$?
+ else
+ sha512_download_result=44
fi
-done < ${SEATUNNEL_HOME}/config/plugin_config
+ if [ "$sha512_download_result" -eq 0 ]; then
+ checksum_algorithm=sha512
+ elif [ "$sha512_download_result" -eq 44 ] &&
+ supports_checksum_algorithm sha1 &&
+ download_https "${artifact_url}.sha1" "$checksum_file"; then
+ checksum_algorithm=sha1
+ else
+ echo "Error: no usable checksum is available for '${artifact_id}'." >&2
+ return 1
+ fi
+
+ if ! verify_checksum "$temporary_file" "$checksum_file"
"$checksum_algorithm"; then
+ echo "Error: checksum verification failed for '${artifact_id}'." >&2
+ return 1
+ fi
+ if ! validate_jar "$temporary_file"; then
+ echo "Error: downloaded file for '${artifact_id}' is not a JAR
archive." >&2
+ return 1
+ fi
+ mv "$temporary_file" "$target_file" || return 1
+ rm -f "$checksum_file"
+ rmdir "$temporary_directory" || return 1
+ temporary_file=
+ checksum_file=
+ temporary_directory=
+}
+
+download_plugin_with_maven() {
+ artifact_id=$1
+ echo "Install connector with Maven: ${artifact_id}"
+ "${SEATUNNEL_HOME}/mvnw" dependency:get \
+ -Dtransitive=false \
+ -DgroupId=org.apache.seatunnel \
+ -DartifactId="$artifact_id" \
+ -Dversion="$version" \
+ -Ddest="${SEATUNNEL_HOME}/connectors"
+}
+
+while IFS= read -r line || [ -n "$line" ]; do
+ first_char=$(printf '%s' "$line" | cut -c 1)
+
+ if [ "$first_char" != "-" ] && [ "$first_char" != "#" ] && [ -n
"$first_char" ]; then
+ case "$line" in
+ *[!A-Za-z0-9._-]*)
+ echo "Error: invalid connector artifact ID '${line}'." >&2
+ exit 1
+ ;;
+ esac
+
+ if [ "$download_method" = "maven" ]; then
+ download_plugin_with_maven "$line" || exit 1
+ elif ! download_release_plugin "$line"; then
+ echo "Error: failed to download connector '${line}'." >&2
+ exit 1
+ fi
+ fi
+done < "${SEATUNNEL_HOME}/config/plugin_config"
diff --git a/docs/en/getting-started/locally/deployment.md
b/docs/en/getting-started/locally/deployment.md
index a0fd3ca14b..3468578765 100644
--- a/docs/en/getting-started/locally/deployment.md
+++ b/docs/en/getting-started/locally/deployment.md
@@ -41,6 +41,15 @@ If you need a specific connector version, taking 3.0.0 as an
example, you need t
sh bin/install-plugin.sh 3.0.0
```
+For released connector versions, `install-plugin.sh` downloads JARs and their
checksums directly over HTTPS, so Maven is not required on Linux and macOS.
This path requires `curl`, `mktemp`, and one of `sha512sum`, `sha1sum`,
`shasum`, or `openssl`. The Windows `install-plugin.cmd` script continues to
use the bundled Maven Wrapper. To use an HTTPS Maven-compatible mirror with
`install-plugin.sh`, set `SEATUNNEL_MAVEN_REPOSITORY` to its base URL:
+
+```bash
+SEATUNNEL_MAVEN_REPOSITORY=https://repo.example.com/maven2 \
+ sh bin/install-plugin.sh 3.0.0
+```
+
+The direct download path supports immutable release versions from repositories
that publish `.sha512` or `.sha1` checksum files. `SNAPSHOT`, `LATEST`,
`RELEASE`, and version ranges automatically use the bundled Maven wrapper
because Maven metadata must be resolved. You can also set
`SEATUNNEL_PLUGIN_DOWNLOAD_METHOD=maven` to preserve Maven `settings.xml`
behavior such as mirrors, authenticated repositories, proxies, and custom TLS
policies.
+
Typically, you do not need all the connector plugins. You can specify the
required plugins by configuring `config/plugin_config`. For example, if you
want the sample application to work properly, you will need the
`connector-console` and `connector-fake` plugins. You can modify the
`plugin_config` configuration file as follows:
```plugin_config
diff --git a/docs/en/introduction/concepts/incompatible-changes.md
b/docs/en/introduction/concepts/incompatible-changes.md
index 8903c03ca9..5cce8d2a8d 100644
--- a/docs/en/introduction/concepts/incompatible-changes.md
+++ b/docs/en/introduction/concepts/incompatible-changes.md
@@ -78,6 +78,12 @@ You need to check this document before you upgrade to
related version.
### Configuration Changes
+- **Breaking Change: Released connector installation defaults to direct HTTPS
downloads**
+ - **Affected component**: `bin/install-plugin.sh` on Linux and macOS
+ - **Description**: Fixed release versions are now downloaded directly from
Maven Central over HTTPS and verified with a published SHA-512 or SHA-1
checksum. Previously, every connector was resolved through the bundled Maven
Wrapper.
+ - **Impact**: Existing environments that depend on Maven `settings.xml` for
mirrors, authenticated repositories, proxies, or custom TLS policies may no
longer install released connectors with the default command.
+ - **Migration Guide**: Set `SEATUNNEL_PLUGIN_DOWNLOAD_METHOD=maven` when
running `install-plugin.sh` to preserve the previous Maven resolution behavior.
Alternatively, set `SEATUNNEL_MAVEN_REPOSITORY` to an HTTPS Maven-compatible
mirror that publishes connector checksum files.
+
- **Breaking Change: CatalogFactory creation path now validates
`optionRule()`**
- **Affected component**: `seatunnel-api` —
`FactoryUtil.createOptionalCatalog()`
- **Description**: The `FactoryUtil.createOptionalCatalog()` method now
calls `ConfigValidator.validate(catalogFactory.optionRule())` before creating a
catalog instance. Previously, no validation was performed on the catalog
factory's option rules during catalog creation.
diff --git a/docs/zh/getting-started/locally/deployment.md
b/docs/zh/getting-started/locally/deployment.md
index 1ecad85398..d6773b4e85 100644
--- a/docs/zh/getting-started/locally/deployment.md
+++ b/docs/zh/getting-started/locally/deployment.md
@@ -41,6 +41,15 @@ sh bin/install-plugin.sh
sh bin/install-plugin.sh 3.0.0
```
+对于正式发布的连接器版本,`install-plugin.sh` 通过 HTTPS 直接下载 JAR 及其校验文件,因此 Linux 和 macOS 不需要
Maven。该方式需要 `curl`、`mktemp`,以及 `sha512sum`、`sha1sum`、`shasum` 或 `openssl`
中的任意一个。Windows 的 `install-plugin.cmd` 仍使用发行包内置的 Maven Wrapper。如果需要为
`install-plugin.sh` 使用 Maven 兼容的 HTTPS 镜像,可以通过 `SEATUNNEL_MAVEN_REPOSITORY`
指定仓库根地址:
+
+```bash
+SEATUNNEL_MAVEN_REPOSITORY=https://repo.example.com/maven2 \
+ sh bin/install-plugin.sh 3.0.0
+```
+
+直接下载仅支持提供 `.sha512` 或 `.sha1` 校验文件的不可变正式版本。`SNAPSHOT`、`LATEST`、`RELEASE`
和版本范围需要解析 Maven 元数据,因此脚本会自动使用发行包内置的 Maven Wrapper。如果需要继续使用 Maven `settings.xml`
中的镜像、认证仓库、代理或自定义 TLS 策略,也可以设置 `SEATUNNEL_PLUGIN_DOWNLOAD_METHOD=maven`。
+
通常情况下,你不需要所有的连接器插件。你可以通过配置`config/plugin_config`来指定所需的插件。例如,如果你想让示例应用程序正常工作,你将需要`connector-console`和`connector-fake`插件。你可以修改`plugin_config`配置文件,如下所示:
```plugin_config
diff --git a/docs/zh/introduction/concepts/incompatible-changes.md
b/docs/zh/introduction/concepts/incompatible-changes.md
index fe261ff287..caddbf3172 100644
--- a/docs/zh/introduction/concepts/incompatible-changes.md
+++ b/docs/zh/introduction/concepts/incompatible-changes.md
@@ -77,6 +77,12 @@
### 配置变更
+- **破坏性变更:正式发布版本的连接器默认改为通过 HTTPS 直接下载**
+ - **影响范围**:Linux 和 macOS 上的 `bin/install-plugin.sh`
+ - **变更说明**:对于固定的正式发布版本,脚本现在默认从 Maven Central 通过 HTTPS 直接下载连接器,并使用仓库发布的
SHA-512 或 SHA-1 校验文件验证完整性。此前所有连接器都通过发行包内置的 Maven Wrapper 解析。
+ - **影响**:如果现有环境依赖 Maven `settings.xml` 中配置的镜像、认证仓库、代理或自定义 TLS
策略,升级后使用默认命令安装正式版本连接器可能失败。
+ - **迁移指南**:运行 `install-plugin.sh` 时设置
`SEATUNNEL_PLUGIN_DOWNLOAD_METHOD=maven`,即可保留原有的 Maven 解析行为。也可以通过
`SEATUNNEL_MAVEN_REPOSITORY` 指定一个发布连接器校验文件的 HTTPS Maven 兼容镜像。
+
- **破坏性变更:CatalogFactory 创建路径现在会校验 `optionRule()`**
- **影响范围**:`seatunnel-api` — `FactoryUtil.createOptionalCatalog()`
- **变更说明**:`FactoryUtil.createOptionalCatalog()` 方法现在在创建 catalog 实例之前会调用
`ConfigValidator.validate(catalogFactory.optionRule())` 进行校验。此前,catalog 创建路径不会对
catalog factory 的 option rules 执行任何校验。
diff --git
a/seatunnel-dist/src/test/java/org/apache/seatunnel/installer/InstallPluginScriptTest.java
b/seatunnel-dist/src/test/java/org/apache/seatunnel/installer/InstallPluginScriptTest.java
new file mode 100644
index 0000000000..fd19f20008
--- /dev/null
+++
b/seatunnel-dist/src/test/java/org/apache/seatunnel/installer/InstallPluginScriptTest.java
@@ -0,0 +1,617 @@
+/*
+ * 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.installer;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFilePermission;
+import java.security.MessageDigest;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.stream.Stream;
+
+/**
+ * Verifies the connector installer download, integrity, cleanup, and Maven
compatibility paths
+ * without relying on an external artifact repository.
+ */
+@EnabledOnOs({OS.LINUX, OS.MAC})
+public class InstallPluginScriptTest {
+
+ private static final byte[] JAR_CONTENT =
+ new byte[] {0x50, 0x4B, 0x03, 0x04, 'f', 'i', 'x', 't', 'u', 'r',
'e'};
+
+ @TempDir private Path temporaryDirectory;
+
+ /**
+ * Verifies that a release artifact is installed only after its checksum
and JAR header pass.
+ */
+ @Test
+ public void testReleaseDownloadWithChecksum() throws Exception {
+ Path distribution = createDistribution("connector-fake");
+ Path fakeBinaryDirectory = createFakeCurl(sha1Hex(JAR_CONTENT), false,
404);
+
+ int exitCode = runInstaller(distribution, fakeBinaryDirectory,
"2.3.13", null);
+
+ Assertions.assertEquals(0, exitCode);
+ Path installedJar =
distribution.resolve("connectors").resolve("connector-fake-2.3.13.jar");
+ Assertions.assertArrayEquals(JAR_CONTENT,
Files.readAllBytes(installedJar));
+ Assertions.assertEquals(
+ EnumSet.of(
+ PosixFilePermission.OWNER_READ,
+ PosixFilePermission.OWNER_WRITE,
+ PosixFilePermission.GROUP_READ,
+ PosixFilePermission.OTHERS_READ),
+ Files.getPosixFilePermissions(installedJar));
+ assertNoTemporaryFiles(distribution.resolve("connectors"));
+ }
+
+ /**
+ * Verifies that an environment with only SHA-1 support requests the SHA-1
sidecar directly,
+ * even when the repository would publish SHA-512.
+ */
+ @Test
+ public void testSha1OnlyEnvironmentUsesSha1Checksum() throws Exception {
+ Path distribution = createDistribution("connector-fake");
+ Path fakeBinaryDirectory = createFakeCurl(sha1Hex(JAR_CONTENT), true,
200);
+ createIsolatedSha1Tools(fakeBinaryDirectory, sha1Hex(JAR_CONTENT));
+
+ int exitCode = runInstaller(distribution, fakeBinaryDirectory,
"2.3.13", null, true);
+
+ Assertions.assertEquals(0, exitCode);
+ Path installedJar =
distribution.resolve("connectors").resolve("connector-fake-2.3.13.jar");
+ Assertions.assertArrayEquals(JAR_CONTENT,
Files.readAllBytes(installedJar));
+ assertNoTemporaryFiles(distribution.resolve("connectors"));
+ }
+
+ /**
+ * Verifies that the preferred SHA-512 checksum path installs a valid
release artifact without
+ * falling back to SHA-1.
+ */
+ @Test
+ public void testReleaseDownloadWithSha512Checksum() throws Exception {
+ Path distribution = createDistribution("connector-fake");
+ Path fakeBinaryDirectory = createFakeCurl(sha512Hex(JAR_CONTENT),
true, 200);
+
+ int exitCode = runInstaller(distribution, fakeBinaryDirectory,
"2.3.13", null);
+
+ Assertions.assertEquals(0, exitCode);
+ Path installedJar =
distribution.resolve("connectors").resolve("connector-fake-2.3.13.jar");
+ Assertions.assertArrayEquals(JAR_CONTENT,
Files.readAllBytes(installedJar));
+ assertNoTemporaryFiles(distribution.resolve("connectors"));
+ }
+
+ /**
+ * Verifies that a checksum mismatch leaves an existing connector
untouched and cleans staging
+ * files.
+ */
+ @Test
+ public void testChecksumFailurePreservesExistingConnector() throws
Exception {
+ Path distribution = createDistribution("connector-fake");
+ Path connectorsDirectory = distribution.resolve("connectors");
+ Files.createDirectories(connectorsDirectory);
+ Path installedJar =
connectorsDirectory.resolve("connector-fake-2.3.13.jar");
+ byte[] existingContent =
"existing-connector".getBytes(StandardCharsets.UTF_8);
+ Files.write(installedJar, existingContent);
+ Path fakeBinaryDirectory = createFakeCurl(repeat("0", 40), false, 404);
+
+ int exitCode = runInstaller(distribution, fakeBinaryDirectory,
"2.3.13", null);
+
+ Assertions.assertNotEquals(0, exitCode);
+ Assertions.assertArrayEquals(existingContent,
Files.readAllBytes(installedJar));
+ assertNoTemporaryFiles(connectorsDirectory);
+ }
+
+ /**
+ * Verifies that a SHA-512 server error fails closed instead of silently
falling back to SHA-1.
+ */
+ @Test
+ public void testSha512ServerFailureDoesNotFallbackToSha1() throws
Exception {
+ Path distribution = createDistribution("connector-fake");
+ Path fakeBinaryDirectory = createFakeCurl(sha1Hex(JAR_CONTENT), false,
500);
+
+ int exitCode = runInstaller(distribution, fakeBinaryDirectory,
"2.3.13", null);
+
+ Assertions.assertNotEquals(0, exitCode);
+ Assertions.assertFalse(
+ Files.exists(
+
distribution.resolve("connectors").resolve("connector-fake-2.3.13.jar")));
+ assertNoTemporaryFiles(distribution.resolve("connectors"));
+ }
+
+ /**
+ * Verifies that an HTTP failure cannot replace an existing connector and
leaves no staging
+ * files behind.
+ */
+ @Test
+ public void testDownloadFailurePreservesExistingConnector() throws
Exception {
+ Path distribution = createDistribution("connector-fake");
+ Path connectorsDirectory = distribution.resolve("connectors");
+ Files.createDirectories(connectorsDirectory);
+ Path installedJar =
connectorsDirectory.resolve("connector-fake-2.3.13.jar");
+ byte[] existingContent =
"existing-connector".getBytes(StandardCharsets.UTF_8);
+ Files.write(installedJar, existingContent);
+ Path fakeBinaryDirectory = createFailingCurl();
+
+ int exitCode = runInstaller(distribution, fakeBinaryDirectory,
"2.3.13", null);
+
+ Assertions.assertNotEquals(0, exitCode);
+ Assertions.assertArrayEquals(existingContent,
Files.readAllBytes(installedJar));
+ assertNoTemporaryFiles(connectorsDirectory);
+ }
+
+ /**
+ * Verifies that a checksum-matching response is still rejected when it
does not have a JAR
+ * archive header.
+ */
+ @Test
+ public void testNonJarPayloadIsRejected() throws Exception {
+ byte[] nonJarContent = "not-a-jar".getBytes(StandardCharsets.UTF_8);
+ Path distribution = createDistribution("connector-fake");
+ Path fakeBinaryDirectory =
+ createFakeCurl(sha1Hex(nonJarContent), false, 404, "printf
'not-a-jar'");
+
+ int exitCode = runInstaller(distribution, fakeBinaryDirectory,
"2.3.13", null);
+
+ Assertions.assertNotEquals(0, exitCode);
+ Assertions.assertFalse(
+ Files.exists(
+
distribution.resolve("connectors").resolve("connector-fake-2.3.13.jar")));
+ assertNoTemporaryFiles(distribution.resolve("connectors"));
+ }
+
+ /**
+ * Verifies that snapshot versions retain Maven metadata resolution
instead of using a direct
+ * release URL.
+ */
+ @Test
+ public void testSnapshotUsesMavenFallback() throws Exception {
+ Path distribution = createDistribution("connector-fake");
+ Path fakeBinaryDirectory = createFakeCurl(repeat("0", 40), false, 404);
+ Path capturedArguments =
temporaryDirectory.resolve("maven-arguments.txt");
+ createFakeMavenWrapper(distribution, capturedArguments);
+
+ int exitCode =
+ runInstaller(
+ distribution, fakeBinaryDirectory, "3.0.0-SNAPSHOT",
capturedArguments);
+
+ Assertions.assertEquals(0, exitCode);
+ String arguments =
+ new String(Files.readAllBytes(capturedArguments),
StandardCharsets.UTF_8);
+ Assertions.assertTrue(arguments.contains("-Dversion=3.0.0-SNAPSHOT"));
+
Assertions.assertTrue(arguments.contains("-DartifactId=connector-fake"));
+ }
+
+ /**
+ * Verifies that a fixed release can explicitly retain Maven resolution
behavior for existing
+ * mirror, proxy, and authentication policies.
+ */
+ @Test
+ public void testReleaseUsesExplicitMavenFallback() throws Exception {
+ Path distribution = createDistribution("connector-fake");
+ Path fakeBinaryDirectory = createFakeCurl(repeat("0", 40), false, 404);
+ Path capturedArguments =
temporaryDirectory.resolve("release-maven-arguments.txt");
+ createFakeMavenWrapper(distribution, capturedArguments);
+
+ int exitCode =
+ runInstaller(
+ distribution,
+ fakeBinaryDirectory,
+ "2.3.13",
+ capturedArguments,
+ false,
+ "maven",
+ "https://repo.example.test/maven2");
+
+ Assertions.assertEquals(0, exitCode);
+ String arguments =
+ new String(Files.readAllBytes(capturedArguments),
StandardCharsets.UTF_8);
+ Assertions.assertTrue(arguments.contains("-Dversion=2.3.13"));
+
Assertions.assertTrue(arguments.contains("-DartifactId=connector-fake"));
+ }
+
+ /**
+ * Verifies that unsafe release version text is rejected before a download
is attempted or a
+ * connectors directory is created.
+ */
+ @Test
+ public void testInvalidReleaseVersionIsRejected() throws Exception {
+ Path distribution = createDistribution("connector-fake");
+ Path fakeBinaryDirectory = createFakeCurl(sha1Hex(JAR_CONTENT), false,
404);
+
+ int exitCode = runInstaller(distribution, fakeBinaryDirectory,
"../2.3.13", null);
+
+ Assertions.assertNotEquals(0, exitCode);
+
Assertions.assertFalse(Files.exists(distribution.resolve("connectors")));
+ }
+
+ /**
+ * Verifies that an unsafe connector artifact ID is rejected before any
artifact download is
+ * attempted.
+ */
+ @Test
+ public void testInvalidArtifactIdIsRejected() throws Exception {
+ Path distribution = createDistribution("../connector-fake");
+ Path fakeBinaryDirectory = createFakeCurl(sha1Hex(JAR_CONTENT), false,
404);
+
+ int exitCode = runInstaller(distribution, fakeBinaryDirectory,
"2.3.13", null);
+
+ Assertions.assertNotEquals(0, exitCode);
+ assertNoTemporaryFiles(distribution.resolve("connectors"));
+ }
+
+ /**
+ * Verifies that the direct-download repository cannot use plaintext HTTP
as its base protocol.
+ */
+ @Test
+ public void testHttpRepositoryIsRejected() throws Exception {
+ Path distribution = createDistribution("connector-fake");
+ Path fakeBinaryDirectory = createFakeCurl(sha1Hex(JAR_CONTENT), false,
404);
+
+ int exitCode =
+ runInstaller(
+ distribution,
+ fakeBinaryDirectory,
+ "2.3.13",
+ null,
+ false,
+ null,
+ "http://repo.example.test/maven2");
+
+ Assertions.assertNotEquals(0, exitCode);
+
Assertions.assertFalse(Files.exists(distribution.resolve("connectors")));
+ }
+
+ /**
+ * Creates a minimal SeaTunnel distribution containing the installer and
selected plugin
+ * configuration.
+ *
+ * @param pluginConfig plugin artifact ID written to the installer
configuration
+ * @return path to the minimal distribution
+ */
+ private Path createDistribution(String pluginConfig) throws Exception {
+ Path distribution = temporaryDirectory.resolve("SeaTunnel home with
spaces");
+ Path binDirectory = distribution.resolve("bin");
+ Path configDirectory = distribution.resolve("config");
+ Files.createDirectories(binDirectory);
+ Files.createDirectories(configDirectory);
+ Files.copy(locateFile("bin/install-plugin.sh"),
binDirectory.resolve("install-plugin.sh"));
+ Assertions.assertTrue(
+
binDirectory.resolve("install-plugin.sh").toFile().setExecutable(true));
+ Files.write(
+ configDirectory.resolve("plugin_config"),
+ (pluginConfig +
System.lineSeparator()).getBytes(StandardCharsets.UTF_8));
+ return distribution;
+ }
+
+ /**
+ * Creates a deterministic curl replacement that serves a JAR fixture and
the requested checksum
+ * responses.
+ *
+ * @param checksum checksum response returned by the fake repository
+ * @param sha512Available whether the fake repository publishes SHA-512
+ * @param sha512Status HTTP status returned for the SHA-512 request
+ * @return directory containing the fake curl executable
+ */
+ private Path createFakeCurl(String checksum, boolean sha512Available, int
sha512Status)
+ throws Exception {
+ return createFakeCurl(
+ checksum, sha512Available, sha512Status, "printf
'\\120\\113\\003\\004fixture'");
+ }
+
+ /**
+ * Creates a deterministic curl replacement with a configurable artifact
response.
+ *
+ * @param checksum checksum response returned by the fake repository
+ * @param sha512Available whether the fake repository publishes SHA-512
+ * @param sha512Status HTTP status returned for the SHA-512 request
+ * @param artifactCommand shell command that writes artifact bytes to
standard output
+ * @return directory containing the fake curl executable
+ */
+ private Path createFakeCurl(
+ String checksum, boolean sha512Available, int sha512Status, String
artifactCommand)
+ throws Exception {
+ Path fakeBinaryDirectory =
+ temporaryDirectory.resolve("fake-bin-" + checksum.substring(0,
8));
+ Files.createDirectories(fakeBinaryDirectory);
+ Path fakeCurl = fakeBinaryDirectory.resolve("curl");
+ String script =
+ "#!/bin/sh\n"
+ + "output=\n"
+ + "url=\n"
+ + "secure_proto=false\n"
+ + "secure_redirect=false\n"
+ + "while [ \"$#\" -gt 0 ]; do\n"
+ + " case \"$1\" in\n"
+ + " --output) output=$2; shift 2 ;;\n"
+ + " --proto) [ \"$2\" = '=https' ] &&
secure_proto=true; shift 2 ;;\n"
+ + " --proto-redir) [ \"$2\" = '=https' ] &&
secure_redirect=true;"
+ + " shift 2 ;;\n"
+ + " --retry|--connect-timeout|--write-out)"
+ + " shift 2 ;;\n"
+ + " --fail|--location) shift ;;\n"
+ + " *) url=$1; shift ;;\n"
+ + " esac\n"
+ + "done\n"
+ + "[ \"$secure_proto\" = true ] || exit 90\n"
+ + "[ \"$secure_redirect\" = true ] || exit 91\n"
+ + "case \"$url\" in\n"
+ + " *.jar.sha512)\n"
+ + (sha512Available
+ ? " printf '%s\\n' '" + checksum + "' >
\"$output\"\n"
+ : " printf '%s' '" + sha512Status + "';
exit 22\n")
+ + " ;;\n"
+ + " *.jar.sha1) printf '%s\\n' '"
+ + checksum
+ + "' > \"$output\" ;;\n"
+ + " *.jar) "
+ + artifactCommand
+ + " > \"$output\" ;;\n"
+ + " *) exit 22 ;;\n"
+ + "esac\n"
+ + "printf '%s' '200'\n";
+ Files.write(fakeCurl, script.getBytes(StandardCharsets.UTF_8));
+ Assertions.assertTrue(fakeCurl.toFile().setExecutable(true));
+ return fakeBinaryDirectory;
+ }
+
+ /**
+ * Creates a curl replacement that simulates an HTTP failure for every
request.
+ *
+ * @return directory containing the failing curl executable
+ */
+ private Path createFailingCurl() throws Exception {
+ Path fakeBinaryDirectory =
temporaryDirectory.resolve("failing-curl-bin");
+ Files.createDirectories(fakeBinaryDirectory);
+ Path fakeCurl = fakeBinaryDirectory.resolve("curl");
+ Files.write(fakeCurl, "#!/bin/sh\nexit
22\n".getBytes(StandardCharsets.UTF_8));
+ Assertions.assertTrue(fakeCurl.toFile().setExecutable(true));
+ return fakeBinaryDirectory;
+ }
+
+ /**
+ * Adds only a SHA-1 implementation and the POSIX utilities needed by the
installer to an
+ * isolated PATH.
+ *
+ * @param fakeBinaryDirectory isolated executable directory
+ * @param checksum checksum returned by the fake SHA-1 implementation
+ */
+ private void createIsolatedSha1Tools(Path fakeBinaryDirectory, String
checksum)
+ throws Exception {
+ for (String tool :
+ new String[] {
+ "awk", "cut", "dirname", "mkdir", "mktemp", "mv", "od",
"rm", "rmdir", "tr"
+ }) {
+ Path source = locateSystemTool(tool);
+ Files.createSymbolicLink(fakeBinaryDirectory.resolve(tool),
source);
+ }
+
+ Path fakeSha1sum = fakeBinaryDirectory.resolve("sha1sum");
+ String script = "#!/bin/sh\n" + "printf '%s %s\\n' '" + checksum + "'
\"$1\"\n";
+ Files.write(fakeSha1sum, script.getBytes(StandardCharsets.UTF_8));
+ Assertions.assertTrue(fakeSha1sum.toFile().setExecutable(true));
+ }
+
+ /**
+ * Locates a standard POSIX utility for an isolated test PATH.
+ *
+ * @param tool executable name
+ * @return absolute executable path
+ */
+ private Path locateSystemTool(String tool) {
+ for (String directory : new String[] {"/usr/bin", "/bin"}) {
+ Path candidate = Paths.get(directory, tool);
+ if (Files.isExecutable(candidate)) {
+ return candidate;
+ }
+ }
+ throw new IllegalStateException("Required POSIX test utility is
unavailable: " + tool);
+ }
+
+ /**
+ * Creates a Maven wrapper replacement that records dependency resolution
arguments.
+ *
+ * @param distribution minimal SeaTunnel distribution
+ * @param capturedArguments file that receives Maven arguments
+ */
+ private void createFakeMavenWrapper(Path distribution, Path
capturedArguments)
+ throws Exception {
+ Path fakeMavenWrapper = distribution.resolve("mvnw");
+ String script = "#!/bin/sh\n" + "printf '%s\\n' \"$@\" > \"" +
capturedArguments + "\"\n";
+ Files.write(fakeMavenWrapper, script.getBytes(StandardCharsets.UTF_8));
+ Assertions.assertTrue(fakeMavenWrapper.toFile().setExecutable(true));
+ }
+
+ /**
+ * Runs the installer with the fake download tools and a fixed HTTPS
repository.
+ *
+ * @param distribution minimal SeaTunnel distribution
+ * @param fakeBinaryDirectory directory prepended to PATH
+ * @param version connector version passed to the installer
+ * @param capturedArguments optional Maven argument capture file
+ * @return installer process exit code
+ */
+ private int runInstaller(
+ Path distribution, Path fakeBinaryDirectory, String version, Path
capturedArguments)
+ throws Exception {
+ return runInstaller(distribution, fakeBinaryDirectory, version,
capturedArguments, false);
+ }
+
+ /**
+ * Runs the installer with optional isolation from host checksum
executables.
+ *
+ * @param distribution minimal SeaTunnel distribution
+ * @param fakeBinaryDirectory directory used as PATH
+ * @param version connector version passed to the installer
+ * @param capturedArguments optional Maven argument capture file
+ * @param isolatePath whether PATH should exclude host checksum executables
+ * @return installer process exit code
+ */
+ private int runInstaller(
+ Path distribution,
+ Path fakeBinaryDirectory,
+ String version,
+ Path capturedArguments,
+ boolean isolatePath)
+ throws Exception {
+ return runInstaller(
+ distribution,
+ fakeBinaryDirectory,
+ version,
+ capturedArguments,
+ isolatePath,
+ null,
+ "https://repo.example.test/maven2");
+ }
+
+ /**
+ * Runs the installer with explicit method and repository environment
values.
+ *
+ * @param distribution minimal SeaTunnel distribution
+ * @param fakeBinaryDirectory directory used as PATH
+ * @param version connector version passed to the installer
+ * @param capturedArguments optional Maven argument capture file
+ * @param isolatePath whether PATH should exclude host checksum executables
+ * @param downloadMethod optional installer download method
+ * @param mavenRepository repository base URL
+ * @return installer process exit code
+ */
+ private int runInstaller(
+ Path distribution,
+ Path fakeBinaryDirectory,
+ String version,
+ Path capturedArguments,
+ boolean isolatePath,
+ String downloadMethod,
+ String mavenRepository)
+ throws Exception {
+ Path output = temporaryDirectory.resolve("installer-output-" +
version.hashCode() + ".txt");
+ ProcessBuilder processBuilder =
+ new ProcessBuilder(
+ "/bin/sh",
+ "-c",
+ "umask 022; exec /bin/sh \"$1\" \"$2\"",
+ "install-plugin-test",
+
distribution.resolve("bin").resolve("install-plugin.sh").toString(),
+ version);
+ processBuilder.redirectErrorStream(true);
+ processBuilder.redirectOutput(output.toFile());
+ Map<String, String> environment = processBuilder.environment();
+ String path = fakeBinaryDirectory.toString();
+ if (!isolatePath) {
+ path += System.getProperty("path.separator") +
environment.getOrDefault("PATH", "");
+ }
+ environment.put("PATH", path);
+ environment.put("SEATUNNEL_MAVEN_REPOSITORY", mavenRepository);
+ if (downloadMethod == null) {
+ environment.remove("SEATUNNEL_PLUGIN_DOWNLOAD_METHOD");
+ } else {
+ environment.put("SEATUNNEL_PLUGIN_DOWNLOAD_METHOD",
downloadMethod);
+ }
+ if (capturedArguments != null) {
+ environment.put("CAPTURE_FILE", capturedArguments.toString());
+ }
+ Process process = processBuilder.start();
+ return process.waitFor();
+ }
+
+ /**
+ * Confirms that failed or completed installs leave no uniquely named
staging files.
+ *
+ * @param connectorsDirectory connector installation directory
+ */
+ private void assertNoTemporaryFiles(Path connectorsDirectory) throws
Exception {
+ try (Stream<Path> files = Files.list(connectorsDirectory)) {
+ Assertions.assertFalse(
+ files.anyMatch(
+ path ->
path.getFileName().toString().contains(".install-plugin.")));
+ }
+ }
+
+ /**
+ * Resolves a repository file when tests run from either the root project
or seatunnel-dist.
+ *
+ * @param relativePath repository-relative file path
+ * @return resolved file path
+ */
+ private Path locateFile(String relativePath) {
+ Path rootPath = Paths.get(relativePath);
+ if (Files.exists(rootPath)) {
+ return rootPath;
+ }
+ return Paths.get("..").resolve(relativePath);
+ }
+
+ /**
+ * Calculates the lowercase SHA-1 digest used by the fake Maven repository.
+ *
+ * @param content bytes to hash
+ * @return lowercase hexadecimal digest
+ */
+ private String sha1Hex(byte[] content) throws Exception {
+ return digestHex("SHA-1", content);
+ }
+
+ /**
+ * Calculates the lowercase SHA-512 digest used by the fake Maven
repository.
+ *
+ * @param content bytes to hash
+ * @return lowercase hexadecimal digest
+ */
+ private String sha512Hex(byte[] content) throws Exception {
+ return digestHex("SHA-512", content);
+ }
+
+ /**
+ * Calculates a lowercase hexadecimal digest.
+ *
+ * @param algorithm message digest algorithm
+ * @param content bytes to hash
+ * @return lowercase hexadecimal digest
+ */
+ private String digestHex(String algorithm, byte[] content) throws
Exception {
+ byte[] digest = MessageDigest.getInstance(algorithm).digest(content);
+ StringBuilder value = new StringBuilder(digest.length * 2);
+ for (byte element : digest) {
+ value.append(String.format("%02x", element & 0xff));
+ }
+ return value.toString();
+ }
+
+ /**
+ * Repeats a string to construct fixed-length invalid checksum fixtures.
+ *
+ * @param value value to repeat
+ * @param count number of repetitions
+ * @return repeated value
+ */
+ private String repeat(String value, int count) {
+ StringBuilder result = new StringBuilder(value.length() * count);
+ for (int index = 0; index < count; index++) {
+ result.append(value);
+ }
+ return result.toString();
+ }
+}