This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/dev/pr-11967-5dc71549adb79a798f4d8a9de64523830d57de39 in repository https://gitbox.apache.org/repos/asf/seatunnel.git
commit 96d8e8d39dfdf25ca497d6414ec7d0bab2796edd Author: Goutam Adwant <[email protected]> AuthorDate: Thu Sep 3 04:58:38 2026 +0000 [Feature][Connector-V2] Add Google Cloud Storage file source (#11967) Signed-off-by: goutamadwant <[email protected]> Signed-off-by: Goutam Adwant <[email protected]> --- config/plugin_config | 1 + docs/en/connectors/changelog/connector-file-gcs.md | 6 + docs/en/connectors/source/GcsFile.md | 138 +++++++++++++++++ docs/zh/connectors/changelog/connector-file-gcs.md | 6 + docs/zh/connectors/source/GcsFile.md | 128 ++++++++++++++++ plugin-mapping.properties | 1 + .../seatunnel/file/config/FileSystemType.java | 3 +- .../connector-file/connector-file-gcs/pom.xml | 69 +++++++++ .../file/gcs/config/GcsFileBaseOptions.java | 49 ++++++ .../file/gcs/config/GcsFileSourceOptions.java} | 27 +--- .../seatunnel/file/gcs/config/GcsHadoopConf.java | 105 +++++++++++++ .../seatunnel/file/gcs/source/GcsFileSource.java | 45 ++++++ .../file/gcs/source/GcsFileSourceFactory.java | 164 +++++++++++++++++++++ .../gcs/source/config/GcsFileSourceConfig.java | 46 ++++++ .../config/MultipleTableGcsFileSourceConfig.java | 39 +++++ .../services/org.apache.hadoop.fs.FileSystem | 16 ++ .../file/gcs/GcsFileSourceFactoryTest.java | 95 ++++++++++++ .../file/gcs/config/GcsHadoopConfTest.java | 130 ++++++++++++++++ seatunnel-connectors-v2/connector-file/pom.xml | 1 + seatunnel-dist/pom.xml | 6 + seatunnel-dist/release-docs/NOTICE | 104 +++++++++++++ .../licenses/LICENSE-gcs-connector.txt | 1 + .../connector-file-gcs-e2e/pom.xml | 58 ++++++++ .../e2e/connector/file/gcs/GcsFileIT.java | 104 +++++++++++++ .../src/test/resources/gcs/data/e2e.json | 2 + .../src/test/resources/gcs/gcs_file_to_assert.conf | 84 +++++++++++ seatunnel-e2e/seatunnel-connector-v2-e2e/pom.xml | 1 + .../container/seatunnel/SeaTunnelContainer.java | 24 +++ .../SeaTunnelContainerThreadExemptionTest.java | 24 ++- 29 files changed, 1447 insertions(+), 30 deletions(-) diff --git a/config/plugin_config b/config/plugin_config index a2e8bd3cc7..6c858ed71a 100644 --- a/config/plugin_config +++ b/config/plugin_config @@ -46,6 +46,7 @@ connector-file-local connector-file-oss connector-file-jindo-oss connector-file-s3 +connector-file-gcs connector-file-sftp connector-file-obs connector-file-bos diff --git a/docs/en/connectors/changelog/connector-file-gcs.md b/docs/en/connectors/changelog/connector-file-gcs.md new file mode 100644 index 0000000000..97ff142816 --- /dev/null +++ b/docs/en/connectors/changelog/connector-file-gcs.md @@ -0,0 +1,6 @@ +<details><summary> Change Log </summary> + +| Change | Commit | Version | +|--------|--------|---------| + +</details> diff --git a/docs/en/connectors/source/GcsFile.md b/docs/en/connectors/source/GcsFile.md new file mode 100644 index 0000000000..ecedbfd720 --- /dev/null +++ b/docs/en/connectors/source/GcsFile.md @@ -0,0 +1,138 @@ +import ChangeLog from '../changelog/connector-file-gcs.md'; + +# GcsFile + +> Google Cloud Storage file source connector + +## Support Those Engines + +> Spark<br/> +> Flink<br/> +> SeaTunnel Zeta<br/> + +## Key Features + +- [x] [batch](../../introduction/concepts/connector-v2-features.md) +- [x] [exactly-once](../../introduction/concepts/connector-v2-features.md) +- [x] [column projection](../../introduction/concepts/connector-v2-features.md) +- [x] [parallelism](../../introduction/concepts/connector-v2-features.md) +- [x] [multimodal](../../introduction/concepts/connector-v2-features.md#multimodal) +- [x] multiple table source +- [x] file formats: `text`, `csv`, `parquet`, `orc`, `json`, `excel`, `xml`, `binary`, `markdown`, and `pdf` + +## Description + +Reads files from Google Cloud Storage through the Google Cloud Storage connector for Hadoop. The +connector reuses SeaTunnel's file source implementation for format parsing, schema discovery, +projection, splitting, and multiple-table jobs. + +Set `bucket` to a bucket URI such as `gs://my-bucket`. Set `path` to the object or prefix inside that +bucket, such as `/data/orders`. Do not include an object path in `bucket`. + +## Dependency + +The connector uses `com.google.cloud.bigdataoss:gcs-connector:hadoop3-2.2.33:shaded`, which is +Apache License 2.0 software and targets Java 8. The shaded GCS Hadoop library is packaged in the +`connector-file-gcs` connector JAR. Spark and Flink deployments must provide a compatible Hadoop 3 +runtime on every driver and worker. + +## Authentication + +The connector supports these authentication modes: + +1. **Application Default Credentials (ADC):** omit `service_account_key_file`. The Hadoop GCS + connector discovers credentials from `GOOGLE_APPLICATION_CREDENTIALS` or the service account + attached to the Google Cloud runtime. +2. **Service-account JSON file:** set `service_account_key_file` to a local path that exists at the + same location on every node that reads GCS. + +The explicit `service_account_key_file` option takes precedence over the corresponding entry in +`hadoop_gcs_properties`. + +## Options + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| path | string | yes | - | Object or prefix path inside `bucket`, for example `/data/orders`. | +| file_format_type | string | yes | - | File format: `text`, `csv`, `parquet`, `orc`, `json`, `excel`, `xml`, `binary`, `markdown`, or `pdf`. | +| bucket | string | yes | - | GCS bucket URI, for example `gs://my-bucket`. | +| service_account_key_file | string | no | - | Service-account JSON key file on every worker. When omitted, ADC is used. | +| hadoop_gcs_properties | map | no | - | Additional `fs.gs.*` Hadoop properties. Explicit connector options take precedence. | +| schema | config | conditional | - | Required for `text`, `json`, `excel`, `csv`, and `xml`. See [Schema Feature](../../introduction/concepts/schema-feature.md). | +| read_columns | list | no | - | Columns to project from the source. | +| field_delimiter | string | no | `\001` for text, `,` for CSV | Field delimiter for text and CSV files. `delimiter` is an alias. | +| row_delimiter | string | no | `\n` | Row delimiter for text files. | +| skip_header_row_number | long | no | `0` | Number of initial text or CSV rows to skip. | +| encoding | string | no | `UTF-8` | Character encoding for text, JSON, CSV, and XML files. | +| parse_partition_from_path | boolean | no | `true` | Adds partition values parsed from paths such as `/year=2026/month=08`. | +| recursive_file_scan | boolean | no | `true` | Whether to scan subdirectories recursively. | +| file_filter_pattern | string | no | - | File name filter pattern. | +| filename_extension | string | no | - | File extension filter, for example `csv` or `.json`. | +| compress_codec | string | no | `none` | Compression codec for a single compressed file. | +| archive_compress_codec | string | no | `none` | Archive compression codec. | +| enable_file_split | boolean | no | `false` | Enables logical splitting for uncompressed text, CSV, JSON, and Parquet files. | +| file_split_size | long | conditional | `134217728` | Split size in bytes when `enable_file_split=true`. | +| null_format | string | no | - | Text representation of null values. | +| quote_char | string | no | `"` | CSV quote character. | +| escape_char | string | no | - | CSV escape character. | +| sheet_name | string | no | - | Excel worksheet to read. | +| excel_engine | string | no | `POI` | Excel reader: `POI` or `EasyExcel`. | +| poi_excel_max_file_size | long | no | `52428800` | Maximum Excel file size in bytes for the POI engine. | +| xml_row_tag | string | conditional | - | XML element representing one row. | +| xml_use_attr_format | boolean | conditional | - | Whether XML values are read from attributes. | +| discovery_mode | string | no | `once` | `once` or `continuous`. Continuous mode currently requires update sync and binary format. | +| scan_interval | string | no | `10S` | Poll interval for continuous discovery. | +| start_mode | string | no | `earliest` | `earliest` processes existing files; `latest` starts with later changes. | +| sync_mode | string | no | `full` | `full` or `update`. Update mode currently supports binary format only. | +| target_path | string | conditional | - | Required for `sync_mode=update`; used to compare objects by relative path. | +| target_hadoop_conf | map | no | - | Hadoop configuration for the comparison target. | +| update_strategy | string | no | `distcp` | Update comparison strategy: `distcp` or `strict`. | +| compare_mode | string | no | `len_mtime` | `len_mtime` or `checksum`; checksum requires strict strategy. | +| update_compare_parallelism | int | no | `8` | Parallelism for target metadata lookups, from 1 through 64. | +| update_compare_bulk_threshold | int | no | `0` | Positive values enable bulk directory listing at the threshold; `0` disables it. | +| post_sync_action | string | no | `none` | Post-checkpoint action for continuous discovery: `none`, `delete`, or `backup`. | +| backup_path | string | conditional | - | Required for `post_sync_action=backup`. Must not overlap the source path. | +| retention_max_age | string | no | - | Maximum age for SeaTunnel backup objects. | +| retention_check_interval | string | no | `1H` | Backup retention scan interval. | +| common-options | | no | - | See [Source Common Options](../common-options/source-common-options.md). | + +## Example + +### Read Parquet With ADC + +```hocon +source { + GcsFile { + bucket = "gs://my-bucket" + path = "/warehouse/orders" + file_format_type = "parquet" + } +} +``` + +### Read CSV With a Service Account + +```hocon +source { + GcsFile { + bucket = "gs://my-bucket" + path = "/landing/customers" + file_format_type = "csv" + service_account_key_file = "/opt/seatunnel/keys/gcs-reader.json" + skip_header_row_number = 1 + schema { + fields { + id = long + name = string + } + } + hadoop_gcs_properties = { + "fs.gs.project.id" = "my-project" + } + } +} +``` + +## Changelog + +<ChangeLog /> diff --git a/docs/zh/connectors/changelog/connector-file-gcs.md b/docs/zh/connectors/changelog/connector-file-gcs.md new file mode 100644 index 0000000000..97ff142816 --- /dev/null +++ b/docs/zh/connectors/changelog/connector-file-gcs.md @@ -0,0 +1,6 @@ +<details><summary> Change Log </summary> + +| Change | Commit | Version | +|--------|--------|---------| + +</details> diff --git a/docs/zh/connectors/source/GcsFile.md b/docs/zh/connectors/source/GcsFile.md new file mode 100644 index 0000000000..dd58c949cd --- /dev/null +++ b/docs/zh/connectors/source/GcsFile.md @@ -0,0 +1,128 @@ +import ChangeLog from '../changelog/connector-file-gcs.md'; + +# GcsFile + +> Google Cloud Storage 文件 Source 连接器 + +## 支持的引擎 + +> Spark<br/> +> Flink<br/> +> SeaTunnel Zeta<br/> + +## 主要特性 + +- [x] [批处理](../../introduction/concepts/connector-v2-features.md) +- [x] [精确一次](../../introduction/concepts/connector-v2-features.md) +- [x] [列投影](../../introduction/concepts/connector-v2-features.md) +- [x] [并行度](../../introduction/concepts/connector-v2-features.md) +- [x] [多模态](../../introduction/concepts/connector-v2-features.md#multimodal) +- [x] 多表 Source +- [x] 文件格式:`text`、`csv`、`parquet`、`orc`、`json`、`excel`、`xml`、`binary`、`markdown` 和 `pdf` + +## 描述 + +通过 Google Cloud Storage Hadoop 连接器读取 GCS 文件。格式解析、Schema 发现、列投影、文件切分和多表任务复用 SeaTunnel File Source 的现有实现。 + +`bucket` 必须是 `gs://my-bucket` 形式的存储桶 URI。`path` 是存储桶内的对象或前缀,例如 `/data/orders`。不要把对象路径写入 `bucket`。 + +## 依赖 + +本连接器使用 `com.google.cloud.bigdataoss:gcs-connector:hadoop3-2.2.33:shaded`。该依赖采用 Apache License 2.0,并以 Java 8 为目标版本。shaded GCS Hadoop 库已打包到 `connector-file-gcs` 连接器 JAR 中。Spark 和 Flink 部署必须在驱动节点和所有工作节点提供兼容的 Hadoop 3 运行环境。 + +## 认证 + +连接器支持以下认证方式: + +1. **应用默认凭据(ADC)**:不配置 `service_account_key_file`。Hadoop GCS 连接器会从 `GOOGLE_APPLICATION_CREDENTIALS` 或 Google Cloud 运行环境绑定的服务账号获取凭据。 +2. **服务账号 JSON 文件**:配置 `service_account_key_file`。该本地路径必须以相同位置存在于每个读取 GCS 的节点上。 + +显式配置的 `service_account_key_file` 优先于 `hadoop_gcs_properties` 中的同名 Hadoop 属性。 + +## 配置项 + +| 名称 | 类型 | 是否必填 | 默认值 | 描述 | +|------|------|----------|--------|------| +| path | string | 是 | - | `bucket` 内的对象或前缀路径,例如 `/data/orders`。 | +| file_format_type | string | 是 | - | 文件格式:`text`、`csv`、`parquet`、`orc`、`json`、`excel`、`xml`、`binary`、`markdown` 或 `pdf`。 | +| bucket | string | 是 | - | GCS 存储桶 URI,例如 `gs://my-bucket`。 | +| service_account_key_file | string | 否 | - | 每个工作节点上的服务账号 JSON 文件。省略时使用 ADC。 | +| hadoop_gcs_properties | map | 否 | - | 额外的 `fs.gs.*` Hadoop 属性。显式连接器配置优先。 | +| schema | config | 条件必填 | - | `text`、`json`、`excel`、`csv` 和 `xml` 格式需要配置。参见 [Schema 功能](../../introduction/concepts/schema-feature.md)。 | +| read_columns | list | 否 | - | 从数据源投影的列。 | +| field_delimiter | string | 否 | text 为 `\001`,CSV 为 `,` | text 和 CSV 的字段分隔符,`delimiter` 是其别名。 | +| row_delimiter | string | 否 | `\n` | text 文件的行分隔符。 | +| skip_header_row_number | long | 否 | `0` | 跳过 text 或 CSV 文件开头的行数。 | +| encoding | string | 否 | `UTF-8` | text、JSON、CSV 和 XML 的字符编码。 | +| parse_partition_from_path | boolean | 否 | `true` | 从 `/year=2026/month=08` 形式的路径解析分区值。 | +| recursive_file_scan | boolean | 否 | `true` | 是否递归扫描子目录。 | +| file_filter_pattern | string | 否 | - | 文件名过滤模式。 | +| filename_extension | string | 否 | - | 文件扩展名过滤,例如 `csv` 或 `.json`。 | +| compress_codec | string | 否 | `none` | 单个压缩文件的压缩编码。 | +| archive_compress_codec | string | 否 | `none` | 归档压缩编码。 | +| enable_file_split | boolean | 否 | `false` | 为未压缩的 text、CSV、JSON 和 Parquet 文件启用逻辑切分。 | +| file_split_size | long | 条件必填 | `134217728` | `enable_file_split=true` 时的切分大小,单位为字节。 | +| null_format | string | 否 | - | 表示空值的文本。 | +| quote_char | string | 否 | `"` | CSV 引号字符。 | +| escape_char | string | 否 | - | CSV 转义字符。 | +| sheet_name | string | 否 | - | 要读取的 Excel 工作表。 | +| excel_engine | string | 否 | `POI` | Excel 读取器:`POI` 或 `EasyExcel`。 | +| poi_excel_max_file_size | long | 否 | `52428800` | POI 引擎可读取的最大 Excel 文件字节数。 | +| xml_row_tag | string | 条件必填 | - | 表示一行数据的 XML 元素。 | +| xml_use_attr_format | boolean | 条件必填 | - | 是否从 XML 属性读取值。 | +| discovery_mode | string | 否 | `once` | `once` 或 `continuous`。连续发现当前要求 update 同步和 binary 格式。 | +| scan_interval | string | 否 | `10S` | 连续发现的轮询间隔。 | +| start_mode | string | 否 | `earliest` | `earliest` 处理已有文件,`latest` 从后续变更开始。 | +| sync_mode | string | 否 | `full` | `full` 或 `update`。update 当前仅支持 binary 格式。 | +| target_path | string | 条件必填 | - | `sync_mode=update` 时必填,用于按相对路径比较对象。 | +| target_hadoop_conf | map | 否 | - | 比较目标文件系统的 Hadoop 配置。 | +| update_strategy | string | 否 | `distcp` | 更新比较策略:`distcp` 或 `strict`。 | +| compare_mode | string | 否 | `len_mtime` | `len_mtime` 或 `checksum`;checksum 要求 strict 策略。 | +| update_compare_parallelism | int | 否 | `8` | 目标元数据查询并行度,范围为 1 到 64。 | +| update_compare_bulk_threshold | int | 否 | `0` | 正数表示达到阈值后使用批量目录列举;`0` 表示禁用。 | +| post_sync_action | string | 否 | `none` | 连续发现对象完成 checkpoint 后的动作:`none`、`delete` 或 `backup`。 | +| backup_path | string | 条件必填 | - | `post_sync_action=backup` 时必填,且不能与源路径重叠。 | +| retention_max_age | string | 否 | - | SeaTunnel 备份对象的最大保留时间。 | +| retention_check_interval | string | 否 | `1H` | 备份保留扫描间隔。 | +| common-options | | 否 | - | 参见 [Source 通用配置](../common-options/source-common-options.md)。 | + +## 示例 + +### 使用 ADC 读取 Parquet + +```hocon +source { + GcsFile { + bucket = "gs://my-bucket" + path = "/warehouse/orders" + file_format_type = "parquet" + } +} +``` + +### 使用服务账号读取 CSV + +```hocon +source { + GcsFile { + bucket = "gs://my-bucket" + path = "/landing/customers" + file_format_type = "csv" + service_account_key_file = "/opt/seatunnel/keys/gcs-reader.json" + skip_header_row_number = 1 + schema { + fields { + id = long + name = string + } + } + hadoop_gcs_properties = { + "fs.gs.project.id" = "my-project" + } + } +} +``` + +## Changelog + +<ChangeLog /> diff --git a/plugin-mapping.properties b/plugin-mapping.properties index d0253dcb4d..e04731eb67 100644 --- a/plugin-mapping.properties +++ b/plugin-mapping.properties @@ -83,6 +83,7 @@ seatunnel.sink.Iceberg = connector-iceberg seatunnel.source.InfluxDB = connector-influxdb seatunnel.source.S3File = connector-file-s3 seatunnel.sink.S3File = connector-file-s3 +seatunnel.source.GcsFile = connector-file-gcs seatunnel.source.AmazonDynamodb = connector-amazondynamodb seatunnel.sink.AmazonDynamodb = connector-amazondynamodb seatunnel.source.AzureCosmosDB = connector-azurecosmosdb diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/config/FileSystemType.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/config/FileSystemType.java index 19c67151c0..0b74520c72 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/config/FileSystemType.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/config/FileSystemType.java @@ -29,7 +29,8 @@ public enum FileSystemType implements Serializable { SFTP("SftpFile"), S3("S3File"), OBS("ObsFile"), - BOS("BosFile"); + BOS("BosFile"), + GCS("GcsFile"); private final String fileSystemPluginName; diff --git a/seatunnel-connectors-v2/connector-file/connector-file-gcs/pom.xml b/seatunnel-connectors-v2/connector-file/connector-file-gcs/pom.xml new file mode 100644 index 0000000000..4d44fe58ca --- /dev/null +++ b/seatunnel-connectors-v2/connector-file/connector-file-gcs/pom.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.seatunnel</groupId> + <artifactId>connector-file</artifactId> + <version>${revision}</version> + </parent> + + <artifactId>connector-file-gcs</artifactId> + <name>SeaTunnel : Connectors V2 : File : GCS</name> + + <properties> + <gcs-connector.version>hadoop3-2.2.33</gcs-connector.version> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.seatunnel</groupId> + <artifactId>connector-file-base</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>org.apache.seatunnel</groupId> + <artifactId>seatunnel-shade-hadoop3-uber</artifactId> + <scope>provided</scope> + <exclusions> + <exclusion> + <groupId>org.apache.avro</groupId> + <artifactId>avro</artifactId> + </exclusion> + </exclusions> + </dependency> + + <dependency> + <groupId>com.google.cloud.bigdataoss</groupId> + <artifactId>gcs-connector</artifactId> + <version>${gcs-connector.version}</version> + <classifier>shaded</classifier> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> + </dependency> + </dependencies> + +</project> diff --git a/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/config/GcsFileBaseOptions.java b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/config/GcsFileBaseOptions.java new file mode 100644 index 0000000000..98fb0e5004 --- /dev/null +++ b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/config/GcsFileBaseOptions.java @@ -0,0 +1,49 @@ +/* + * 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.connectors.seatunnel.file.gcs.config; + +import org.apache.seatunnel.api.configuration.Option; +import org.apache.seatunnel.api.configuration.Options; +import org.apache.seatunnel.connectors.seatunnel.file.config.FileBaseSourceOptions; + +import java.util.Map; + +public class GcsFileBaseOptions extends FileBaseSourceOptions { + + public static final Option<String> BUCKET = + Options.key("bucket") + .stringType() + .noDefaultValue() + .withDescription("Google Cloud Storage bucket URI, for example gs://my-bucket"); + + public static final Option<String> SERVICE_ACCOUNT_KEY_FILE = + Options.key("service_account_key_file") + .stringType() + .noDefaultValue() + .withDescription( + "Path to a service account JSON key file available on every worker. " + + "When omitted, the GCS connector uses Application Default Credentials"); + + public static final Option<Map<String, String>> GCS_PROPERTIES = + Options.key("hadoop_gcs_properties") + .mapType() + .noDefaultValue() + .withDescription( + "Additional Hadoop GCS connector properties. Explicit connector " + + "options take precedence over entries in this map"); +} diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/config/FileSystemType.java b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/config/GcsFileSourceOptions.java similarity index 56% copy from seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/config/FileSystemType.java copy to seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/config/GcsFileSourceOptions.java index 19c67151c0..8a8b42199f 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/config/FileSystemType.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/config/GcsFileSourceOptions.java @@ -15,29 +15,6 @@ * limitations under the License. */ -package org.apache.seatunnel.connectors.seatunnel.file.config; +package org.apache.seatunnel.connectors.seatunnel.file.gcs.config; -import java.io.Serializable; - -public enum FileSystemType implements Serializable { - HDFS("HdfsFile"), - LOCAL("LocalFile"), - OSS("OssFile"), - OSS_JINDO("OssJindoFile"), - COS("CosFile"), - FTP("FtpFile"), - SFTP("SftpFile"), - S3("S3File"), - OBS("ObsFile"), - BOS("BosFile"); - - private final String fileSystemPluginName; - - FileSystemType(String fileSystemPluginName) { - this.fileSystemPluginName = fileSystemPluginName; - } - - public String getFileSystemPluginName() { - return fileSystemPluginName; - } -} +public class GcsFileSourceOptions extends GcsFileBaseOptions {} diff --git a/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/config/GcsHadoopConf.java b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/config/GcsHadoopConf.java new file mode 100644 index 0000000000..291147c35d --- /dev/null +++ b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/config/GcsHadoopConf.java @@ -0,0 +1,105 @@ +/* + * 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.connectors.seatunnel.file.gcs.config; + +import org.apache.seatunnel.shade.org.apache.commons.lang3.StringUtils; + +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.connectors.seatunnel.file.config.HadoopConf; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +/** Hadoop filesystem configuration for Google Cloud Storage. */ +public class GcsHadoopConf extends HadoopConf { + + static final String GCS_FILESYSTEM_IMPLEMENTATION = + "com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem"; + static final String GCS_SERVICE_ACCOUNT_KEY_FILE = "fs.gs.auth.service.account.json.keyfile"; + private static final String SCHEMA = "gs"; + + private GcsHadoopConf(String bucket) { + super(bucket); + } + + /** Builds a GCS Hadoop configuration from connector options. */ + public static HadoopConf buildWithReadonlyConfig(ReadonlyConfig config) { + String bucket = config.get(GcsFileBaseOptions.BUCKET); + validateBucket(bucket); + + Map<String, String> properties = new HashMap<>(); + config.getOptional(GcsFileBaseOptions.GCS_PROPERTIES).ifPresent(properties::putAll); + + config.getOptional(GcsFileBaseOptions.SERVICE_ACCOUNT_KEY_FILE) + .ifPresent(keyFile -> configureServiceAccount(properties, keyFile)); + + GcsHadoopConf hadoopConf = new GcsHadoopConf(bucket); + hadoopConf.setExtraOptions(properties); + return hadoopConf; + } + + @Override + public String getFsHdfsImpl() { + return GCS_FILESYSTEM_IMPLEMENTATION; + } + + @Override + public String getSchema() { + return SCHEMA; + } + + private static void validateBucket(String bucket) { + if (StringUtils.isBlank(bucket)) { + throw invalidBucket(bucket); + } + try { + URI uri = new URI(bucket); + String path = uri.getPath(); + if (!SCHEMA.equals(uri.getScheme()) + || StringUtils.isBlank(uri.getAuthority()) + || uri.getUserInfo() != null + || uri.getPort() != -1 + || uri.getQuery() != null + || uri.getFragment() != null + || (StringUtils.isNotEmpty(path) && !"/".equals(path))) { + throw invalidBucket(bucket); + } + } catch (URISyntaxException e) { + throw invalidBucket(bucket); + } + } + + private static IllegalArgumentException invalidBucket(String bucket) { + return new IllegalArgumentException( + String.format( + "The GCS bucket must be a bucket URI such as 'gs://my-bucket', but was '%s'. " + + "Configure object paths with the 'path' option.", + bucket)); + } + + private static void configureServiceAccount( + Map<String, String> properties, String serviceAccountKeyFile) { + if (StringUtils.isBlank(serviceAccountKeyFile)) { + throw new IllegalArgumentException( + "The GCS service_account_key_file option must not be blank"); + } + properties.put(GCS_SERVICE_ACCOUNT_KEY_FILE, serviceAccountKeyFile); + } +} diff --git a/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/source/GcsFileSource.java b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/source/GcsFileSource.java new file mode 100644 index 0000000000..518dde49b6 --- /dev/null +++ b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/source/GcsFileSource.java @@ -0,0 +1,45 @@ +/* + * 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.connectors.seatunnel.file.gcs.source; + +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.api.table.catalog.CatalogTable; +import org.apache.seatunnel.connectors.seatunnel.file.config.FileSystemType; +import org.apache.seatunnel.connectors.seatunnel.file.gcs.source.config.MultipleTableGcsFileSourceConfig; +import org.apache.seatunnel.connectors.seatunnel.file.source.BaseMultipleTableFileSource; + +import java.util.List; + +/** File source implementation for objects stored in Google Cloud Storage. */ +public class GcsFileSource extends BaseMultipleTableFileSource { + + /** Creates a GCS file source for the configured catalog tables. */ + public GcsFileSource( + ReadonlyConfig readonlyConfig, List<CatalogTable> catalogTablesFromConfig) { + this(new MultipleTableGcsFileSourceConfig(readonlyConfig, catalogTablesFromConfig)); + } + + private GcsFileSource(MultipleTableGcsFileSourceConfig sourceConfig) { + super(sourceConfig, initFileSplitStrategy(sourceConfig)); + } + + @Override + public String getPluginName() { + return FileSystemType.GCS.getFileSystemPluginName(); + } +} diff --git a/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/source/GcsFileSourceFactory.java b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/source/GcsFileSourceFactory.java new file mode 100644 index 0000000000..9f50be47ac --- /dev/null +++ b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/source/GcsFileSourceFactory.java @@ -0,0 +1,164 @@ +/* + * 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.connectors.seatunnel.file.gcs.source; + +import org.apache.seatunnel.api.configuration.util.OptionRule; +import org.apache.seatunnel.api.options.ConnectorCommonOptions; +import org.apache.seatunnel.api.source.SeaTunnelSource; +import org.apache.seatunnel.api.source.SourceSplit; +import org.apache.seatunnel.api.table.connector.TableSource; +import org.apache.seatunnel.api.table.factory.Factory; +import org.apache.seatunnel.api.table.factory.TableSourceFactory; +import org.apache.seatunnel.api.table.factory.TableSourceFactoryContext; +import org.apache.seatunnel.connectors.seatunnel.file.config.FileBaseSourceOptions; +import org.apache.seatunnel.connectors.seatunnel.file.config.FileFormat; +import org.apache.seatunnel.connectors.seatunnel.file.config.FilePostSyncAction; +import org.apache.seatunnel.connectors.seatunnel.file.config.FileSyncMode; +import org.apache.seatunnel.connectors.seatunnel.file.config.FileSystemType; +import org.apache.seatunnel.connectors.seatunnel.file.gcs.config.GcsFileSourceOptions; + +import com.google.auto.service.AutoService; + +import java.io.Serializable; +import java.util.Arrays; + +/** Creates Google Cloud Storage file sources from table factory configuration. */ +@AutoService(Factory.class) +public class GcsFileSourceFactory implements TableSourceFactory { + + @Override + public String factoryIdentifier() { + return FileSystemType.GCS.getFileSystemPluginName(); + } + + @Override + public <T, SplitT extends SourceSplit, StateT extends Serializable> + TableSource<T, SplitT, StateT> createSource(TableSourceFactoryContext context) { + return () -> + (SeaTunnelSource<T, SplitT, StateT>) + new GcsFileSource(context.getOptions(), discoverTableSchemas(context)); + } + + @Override + public OptionRule optionRule() { + return OptionRule.builder() + .required(GcsFileSourceOptions.FILE_PATH) + .required(GcsFileSourceOptions.FILE_FORMAT_TYPE) + .required(GcsFileSourceOptions.BUCKET) + .optional( + GcsFileSourceOptions.SERVICE_ACCOUNT_KEY_FILE, + GcsFileSourceOptions.GCS_PROPERTIES) + .conditional( + FileBaseSourceOptions.FILE_FORMAT_TYPE, + FileFormat.TEXT, + FileBaseSourceOptions.ROW_DELIMITER, + FileBaseSourceOptions.FIELD_DELIMITER, + FileBaseSourceOptions.SKIP_HEADER_ROW_NUMBER) + .conditional( + FileBaseSourceOptions.FILE_FORMAT_TYPE, + FileFormat.XML, + FileBaseSourceOptions.XML_ROW_TAG, + FileBaseSourceOptions.XML_USE_ATTR_FORMAT) + .conditional( + FileBaseSourceOptions.FILE_FORMAT_TYPE, + FileFormat.CSV, + FileBaseSourceOptions.SKIP_HEADER_ROW_NUMBER) + .conditional( + FileBaseSourceOptions.FILE_FORMAT_TYPE, + Arrays.asList( + FileFormat.TEXT, + FileFormat.JSON, + FileFormat.EXCEL, + FileFormat.CSV, + FileFormat.XML), + ConnectorCommonOptions.SCHEMA) + .conditional( + FileBaseSourceOptions.FILE_FORMAT_TYPE, + Arrays.asList( + FileFormat.TEXT, FileFormat.JSON, FileFormat.CSV, FileFormat.XML), + FileBaseSourceOptions.ENCODING) + .conditional( + FileBaseSourceOptions.FILE_FORMAT_TYPE, + Arrays.asList( + FileFormat.TEXT, + FileFormat.JSON, + FileFormat.CSV, + FileFormat.PARQUET), + FileBaseSourceOptions.ENABLE_FILE_SPLIT) + .conditional( + FileBaseSourceOptions.ENABLE_FILE_SPLIT, + Boolean.TRUE, + FileBaseSourceOptions.FILE_SPLIT_SIZE) + .optional(FileBaseSourceOptions.PARSE_PARTITION_FROM_PATH) + .optional(FileBaseSourceOptions.DATE_FORMAT_LEGACY) + .optional(FileBaseSourceOptions.DATETIME_FORMAT_LEGACY) + .optional(FileBaseSourceOptions.TIME_FORMAT_LEGACY) + .optional(FileBaseSourceOptions.FILE_FILTER_PATTERN) + .optional(FileBaseSourceOptions.COMPRESS_CODEC) + .optional(FileBaseSourceOptions.ARCHIVE_COMPRESS_CODEC) + .optional(FileBaseSourceOptions.NULL_FORMAT) + .optional(FileBaseSourceOptions.FILENAME_EXTENSION) + .optional(FileBaseSourceOptions.READ_COLUMNS) + .optional( + FileBaseSourceOptions.SHEET_NAME, + FileBaseSourceOptions.EXCEL_ENGINE, + FileBaseSourceOptions.POI_EXCEL_MAX_FILE_SIZE) + .conditional( + FileBaseSourceOptions.FILE_FORMAT_TYPE, + FileFormat.MARKDOWN, + FileBaseSourceOptions.MARKDOWN_RAG_METADATA_ENABLED) + .conditional( + FileBaseSourceOptions.FILE_FORMAT_TYPE, + FileFormat.PDF, + FileBaseSourceOptions.PDF_RAG_METADATA_ENABLED) + .optional(FileBaseSourceOptions.QUOTE_CHAR) + .optional(FileBaseSourceOptions.ESCAPE_CHAR) + .optional(ConnectorCommonOptions.METALAKE_TYPE) + .optional( + FileBaseSourceOptions.DISCOVERY_MODE, + FileBaseSourceOptions.SCAN_INTERVAL, + FileBaseSourceOptions.START_MODE) + .optional( + FileBaseSourceOptions.SYNC_MODE, + FileBaseSourceOptions.TARGET_HADOOP_CONF, + FileBaseSourceOptions.UPDATE_STRATEGY, + FileBaseSourceOptions.COMPARE_MODE, + FileBaseSourceOptions.UPDATE_COMPARE_PARALLELISM, + FileBaseSourceOptions.UPDATE_COMPARE_BULK_THRESHOLD) + .optional( + FileBaseSourceOptions.POST_SYNC_ACTION, + FileBaseSourceOptions.BACKUP_PATH, + FileBaseSourceOptions.RETENTION_MAX_AGE, + FileBaseSourceOptions.RETENTION_CHECK_INTERVAL) + .conditional( + FileBaseSourceOptions.SYNC_MODE, + FileSyncMode.UPDATE, + FileBaseSourceOptions.TARGET_PATH) + .conditional( + FileBaseSourceOptions.POST_SYNC_ACTION, + FilePostSyncAction.BACKUP, + FileBaseSourceOptions.BACKUP_PATH) + .optional(FileBaseSourceOptions.RECURSIVE_FILE_SCAN) + .build(); + } + + @Override + public Class<? extends SeaTunnelSource> getSourceClass() { + return GcsFileSource.class; + } +} diff --git a/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/source/config/GcsFileSourceConfig.java b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/source/config/GcsFileSourceConfig.java new file mode 100644 index 0000000000..e5cb57939c --- /dev/null +++ b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/source/config/GcsFileSourceConfig.java @@ -0,0 +1,46 @@ +/* + * 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.connectors.seatunnel.file.gcs.source.config; + +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.api.table.catalog.CatalogTable; +import org.apache.seatunnel.connectors.seatunnel.file.config.BaseFileSourceConfig; +import org.apache.seatunnel.connectors.seatunnel.file.config.FileSystemType; +import org.apache.seatunnel.connectors.seatunnel.file.config.HadoopConf; +import org.apache.seatunnel.connectors.seatunnel.file.gcs.config.GcsHadoopConf; + +/** Source configuration that supplies the Google Cloud Storage filesystem settings. */ +public class GcsFileSourceConfig extends BaseFileSourceConfig { + + private static final long serialVersionUID = 1L; + + /** Creates a source configuration for a single catalog table. */ + public GcsFileSourceConfig(ReadonlyConfig readonlyConfig, CatalogTable catalogTableFromConfig) { + super(readonlyConfig, catalogTableFromConfig); + } + + @Override + public HadoopConf getHadoopConfig() { + return GcsHadoopConf.buildWithReadonlyConfig(getBaseFileSourceConfig()); + } + + @Override + public String getPluginName() { + return FileSystemType.GCS.getFileSystemPluginName(); + } +} diff --git a/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/source/config/MultipleTableGcsFileSourceConfig.java b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/source/config/MultipleTableGcsFileSourceConfig.java new file mode 100644 index 0000000000..24089d2d69 --- /dev/null +++ b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/source/config/MultipleTableGcsFileSourceConfig.java @@ -0,0 +1,39 @@ +/* + * 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.connectors.seatunnel.file.gcs.source.config; + +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.api.table.catalog.CatalogTable; +import org.apache.seatunnel.connectors.seatunnel.file.config.BaseFileSourceConfig; +import org.apache.seatunnel.connectors.seatunnel.file.config.BaseMultipleTableFileSourceConfig; + +import java.util.List; + +public class MultipleTableGcsFileSourceConfig extends BaseMultipleTableFileSourceConfig { + + public MultipleTableGcsFileSourceConfig( + ReadonlyConfig rootConfig, List<CatalogTable> catalogTablesFromConfig) { + super(rootConfig, catalogTablesFromConfig); + } + + @Override + public BaseFileSourceConfig getBaseSourceConfig( + ReadonlyConfig readonlyConfig, CatalogTable catalogTableFromConfig) { + return new GcsFileSourceConfig(readonlyConfig, catalogTableFromConfig); + } +} diff --git a/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/resources/META-INF/services/org.apache.hadoop.fs.FileSystem b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/resources/META-INF/services/org.apache.hadoop.fs.FileSystem new file mode 100644 index 0000000000..1668b432e9 --- /dev/null +++ b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/main/resources/META-INF/services/org.apache.hadoop.fs.FileSystem @@ -0,0 +1,16 @@ +# 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. + +com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem diff --git a/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/GcsFileSourceFactoryTest.java b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/GcsFileSourceFactoryTest.java new file mode 100644 index 0000000000..677b3f0ebd --- /dev/null +++ b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/GcsFileSourceFactoryTest.java @@ -0,0 +1,95 @@ +/* + * 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.connectors.seatunnel.file.gcs; + +import org.apache.seatunnel.api.configuration.Option; +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.api.configuration.util.ConfigValidator; +import org.apache.seatunnel.api.configuration.util.OptionRule; +import org.apache.seatunnel.api.configuration.util.OptionValidationException; +import org.apache.seatunnel.connectors.seatunnel.file.config.FileBaseSourceOptions; +import org.apache.seatunnel.connectors.seatunnel.file.gcs.config.GcsFileSourceOptions; +import org.apache.seatunnel.connectors.seatunnel.file.gcs.source.GcsFileSource; +import org.apache.seatunnel.connectors.seatunnel.file.gcs.source.GcsFileSourceFactory; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GcsFileSourceFactoryTest { + + @Test + void shouldIdentifyGcsFileSource() { + GcsFileSourceFactory factory = new GcsFileSourceFactory(); + + assertEquals("GcsFile", factory.factoryIdentifier()); + assertEquals(GcsFileSource.class, factory.getSourceClass()); + } + + @Test + void shouldRequireOnlyCoreGcsSourceOptionsForBinaryFiles() { + OptionRule optionRule = new GcsFileSourceFactory().optionRule(); + Map<String, Object> config = sourceConfig(); + + assertDoesNotThrow(() -> validate(config, optionRule)); + + config.remove(GcsFileSourceOptions.BUCKET.key()); + assertThrows(OptionValidationException.class, () -> validate(config, optionRule)); + } + + @Test + void shouldExposeAuthenticationAndFileBaseOptions() { + OptionRule optionRule = new GcsFileSourceFactory().optionRule(); + + assertTrue( + optionRule + .getOptionalOptions() + .contains(GcsFileSourceOptions.SERVICE_ACCOUNT_KEY_FILE)); + assertTrue(optionRule.getOptionalOptions().contains(GcsFileSourceOptions.GCS_PROPERTIES)); + assertTrue(optionRule.getOptionalOptions().contains(FileBaseSourceOptions.DISCOVERY_MODE)); + assertTrue(optionRule.getOptionalOptions().contains(FileBaseSourceOptions.SYNC_MODE)); + assertTrue(optionRuleContains(optionRule, FileBaseSourceOptions.ENABLE_FILE_SPLIT)); + assertTrue(optionRuleContains(optionRule, FileBaseSourceOptions.FILE_SPLIT_SIZE)); + } + + private static Map<String, Object> sourceConfig() { + Map<String, Object> config = new HashMap<>(); + config.put(GcsFileSourceOptions.FILE_PATH.key(), "/source"); + config.put(GcsFileSourceOptions.FILE_FORMAT_TYPE.key(), "binary"); + config.put(GcsFileSourceOptions.BUCKET.key(), "gs://test-bucket"); + return config; + } + + private static boolean optionRuleContains(OptionRule optionRule, Option<?> option) { + if (optionRule.getOptionalOptions().contains(option)) { + return true; + } + return optionRule.getRequiredOptions().stream() + .anyMatch(requiredOption -> requiredOption.getOptions().contains(option)); + } + + private static void validate(Map<String, Object> config, OptionRule optionRule) { + ConfigValidator.of(ReadonlyConfig.fromMap(config)).validate(optionRule); + } +} diff --git a/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/config/GcsHadoopConfTest.java b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/config/GcsHadoopConfTest.java new file mode 100644 index 0000000000..5b16044120 --- /dev/null +++ b/seatunnel-connectors-v2/connector-file/connector-file-gcs/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/gcs/config/GcsHadoopConfTest.java @@ -0,0 +1,130 @@ +/* + * 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.connectors.seatunnel.file.gcs.config; + +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.connectors.seatunnel.file.config.HadoopConf; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileSystem; + +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GcsHadoopConfTest { + + @Test + void shouldConfigureGsFileSystemAndUseDefaultCredentialsWhenKeyFileIsAbsent() throws Exception { + HadoopConf hadoopConf = build(Collections.emptyMap()); + + assertEquals("gs", hadoopConf.getSchema()); + assertEquals("gs://test-bucket", hadoopConf.getHdfsNameKey()); + assertEquals(GcsHadoopConf.GCS_FILESYSTEM_IMPLEMENTATION, hadoopConf.getFsHdfsImpl()); + assertFalse( + hadoopConf + .getExtraOptions() + .containsKey(GcsHadoopConf.GCS_SERVICE_ACCOUNT_KEY_FILE)); + + Configuration configuration = hadoopConf.toConfiguration(); + hadoopConf.setExtraOptionsForConfiguration(configuration); + assertEquals("gs://test-bucket", configuration.get("fs.defaultFS")); + assertEquals(GcsHadoopConf.GCS_FILESYSTEM_IMPLEMENTATION, configuration.get("fs.gs.impl")); + assertTrue(configuration.getBoolean("fs.gs.impl.disable.cache", false)); + assertEquals( + GcsHadoopConf.GCS_FILESYSTEM_IMPLEMENTATION, + FileSystem.getFileSystemClass("gs", configuration).getName()); + } + + @Test + void shouldConfigureServiceAccountJsonKeyFile() { + Map<String, Object> options = new HashMap<>(); + options.put(GcsFileBaseOptions.SERVICE_ACCOUNT_KEY_FILE.key(), "/keys/gcs.json"); + + HadoopConf hadoopConf = build(options); + + assertEquals( + "/keys/gcs.json", + hadoopConf.getExtraOptions().get(GcsHadoopConf.GCS_SERVICE_ACCOUNT_KEY_FILE)); + } + + @Test + void shouldApplyAdditionalPropertiesAndPreferDedicatedKeyFileOption() { + Map<String, String> properties = new HashMap<>(); + properties.put("fs.gs.project.id", "test-project"); + properties.put(GcsHadoopConf.GCS_SERVICE_ACCOUNT_KEY_FILE, "/keys/from-map.json"); + + Map<String, Object> options = new HashMap<>(); + options.put(GcsFileBaseOptions.GCS_PROPERTIES.key(), properties); + options.put(GcsFileBaseOptions.SERVICE_ACCOUNT_KEY_FILE.key(), "/keys/explicit.json"); + + HadoopConf hadoopConf = build(options); + + assertEquals("test-project", hadoopConf.getExtraOptions().get("fs.gs.project.id")); + assertEquals( + "/keys/explicit.json", + hadoopConf.getExtraOptions().get(GcsHadoopConf.GCS_SERVICE_ACCOUNT_KEY_FILE)); + } + + @Test + void shouldRejectBlankServiceAccountKeyFile() { + Map<String, Object> options = new HashMap<>(); + options.put(GcsFileBaseOptions.SERVICE_ACCOUNT_KEY_FILE.key(), " "); + + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, () -> build(options)); + + assertTrue(exception.getMessage().contains("must not be blank")); + } + + @Test + void shouldRejectInvalidBucketUris() { + String[] invalidBuckets = { + "test-bucket", + "s3a://test-bucket", + "gs://test-bucket/path", + "gs://user@test-bucket", + "gs://test-bucket?query=value", + "gs://test-bucket#fragment", + "gs://" + }; + + for (String invalidBucket : invalidBuckets) { + Map<String, Object> options = new HashMap<>(); + options.put(GcsFileBaseOptions.BUCKET.key(), invalidBucket); + + assertThrows( + IllegalArgumentException.class, + () -> GcsHadoopConf.buildWithReadonlyConfig(ReadonlyConfig.fromMap(options)), + invalidBucket); + } + } + + private static HadoopConf build(Map<String, Object> options) { + Map<String, Object> config = new HashMap<>(options); + config.put(GcsFileBaseOptions.BUCKET.key(), "gs://test-bucket"); + return GcsHadoopConf.buildWithReadonlyConfig(ReadonlyConfig.fromMap(config)); + } +} diff --git a/seatunnel-connectors-v2/connector-file/pom.xml b/seatunnel-connectors-v2/connector-file/pom.xml index 44ee201599..bed8eaa1f2 100644 --- a/seatunnel-connectors-v2/connector-file/pom.xml +++ b/seatunnel-connectors-v2/connector-file/pom.xml @@ -38,6 +38,7 @@ <module>connector-file-base-hadoop</module> <module>connector-file-sftp</module> <module>connector-file-s3</module> + <module>connector-file-gcs</module> <module>connector-file-obs</module> <module>connector-file-jindo-oss</module> <module>connector-file-cos</module> diff --git a/seatunnel-dist/pom.xml b/seatunnel-dist/pom.xml index ae00d170db..15156d53f8 100644 --- a/seatunnel-dist/pom.xml +++ b/seatunnel-dist/pom.xml @@ -596,6 +596,12 @@ <version>${project.version}</version> <scope>provided</scope> </dependency> + <dependency> + <groupId>org.apache.seatunnel</groupId> + <artifactId>connector-file-gcs</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> <dependency> <groupId>org.apache.seatunnel</groupId> <artifactId>connector-amazondynamodb</artifactId> diff --git a/seatunnel-dist/release-docs/NOTICE b/seatunnel-dist/release-docs/NOTICE index fd355b4d9a..c9ebe8f4bb 100644 --- a/seatunnel-dist/release-docs/NOTICE +++ b/seatunnel-dist/release-docs/NOTICE @@ -874,6 +874,110 @@ The initial codebase was donated to the ASF by HugeGraph Authors, copyright 2017 + Copyright 2019-2024 Couchbase, Inc. + Licensed under the Apache License, Version 2.0 +========================================================================= + + +=============================================================================== + + Third-party dependencies for GCS file connector: + +=============================================================================== + + com.google.cloud.bigdataoss:gcs-connector (https://github.com/GoogleCloudDataproc/hadoop-connectors) + + Copyright 2013 Google Inc. + + Licensed under the Apache License, Version 2.0 + +The shaded GCS connector includes the following notices: + +src/test/org/apache/commons/codec/language/DoubleMetaphoneTest.java +contains test data from http://aspell.net/test/orig/batch0.tab. +Copyright (C) 2002 Kevin Atkinson ([email protected]) + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +Apache HttpCore +Copyright 2005-2021 The Apache Software Foundation + +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta ([email protected]), and has +been in development since 2007. +It is currently developed by a community of developers. + +## Licensing + +Jackson 2.x core and extension components are licensed under Apache License 2.0 +To find the details that apply to this artifact see the accompanying LICENSE file. + +## Credits + +A list of contributors may be found from CREDITS(-2.x) file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +Apache HttpClient +Copyright 1999-2020 The Apache Software Foundation + + The Netty Project + ================= + +Please visit the Netty web site for more information: + + * http://netty.io/ + +Copyright 2016 The Netty Project + +The Netty Project 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. + +------------------------------------------------------------------------------- +This product contains a forked and modified version of Tomcat Native + + * LICENSE: + * license/LICENSE.tomcat-native.txt (Apache License 2.0) + * HOMEPAGE: + * http://tomcat.apache.org/native-doc/ + * https://svn.apache.org/repos/asf/tomcat/native/ + +This product contains the Maven wrapper scripts from 'Maven Wrapper', that provides an easy way to ensure a user has everything necessary to run the Maven build. + + * LICENSE: + * license/LICENSE.mvn-wrapper.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/takari/maven-wrapper + +This product contains small piece of code to support AIX, taken from netbsd. + + * LICENSE: + * license/LICENSE.aix-netbsd.txt (OpenSSL License) + * HOMEPAGE: + * https://ftp.netbsd.org/pub/NetBSD/NetBSD-current/src/crypto/external/bsd/openssl/dist + +This product contains code from boringssl. + + * LICENSE (Combination ISC and OpenSSL license) + * license/LICENSE.boringssl.txt (Combination ISC and OpenSSL license) + * HOMEPAGE: + * https://boringssl.googlesource.com/boringssl/ + +Apache Commons Codec +Copyright 2002-2017 The Apache Software Foundation + +=============================================================================== + +The content of package org.apache.commons.codec.language.bm has been translated +from the original php source code available at http://stevemorse.org/phoneticinfo.htm +with permission from the original authors. +Original source copyright: +Copyright (c) 2008 Alexander Beider & Stephen P. Morse. + ========================================================================= +=============================================================================== diff --git a/seatunnel-dist/release-docs/licenses/LICENSE-gcs-connector.txt b/seatunnel-dist/release-docs/licenses/LICENSE-gcs-connector.txt new file mode 100644 index 0000000000..c33ad4945b --- /dev/null +++ b/seatunnel-dist/release-docs/licenses/LICENSE-gcs-connector.txt @@ -0,0 +1 @@ +License: {Name: The Apache Software License, Version 2.0, URL: https://www.apache.org/licenses/LICENSE-2.0.txt, Distribution: repo, Comments: Google Cloud Storage Connector for Hadoop (com.google.cloud.bigdataoss:gcs-connector) } diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-gcs-e2e/pom.xml b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-gcs-e2e/pom.xml new file mode 100644 index 0000000000..2413e795ab --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-gcs-e2e/pom.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.seatunnel</groupId> + <artifactId>seatunnel-connector-v2-e2e</artifactId> + <version>${revision}</version> + </parent> + + <artifactId>connector-file-gcs-e2e</artifactId> + <name>SeaTunnel : E2E : Connector V2 : File GCS</name> + + <dependencies> + <dependency> + <groupId>org.apache.seatunnel</groupId> + <artifactId>connector-file-gcs</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.seatunnel</groupId> + <artifactId>connector-assert</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.seatunnel</groupId> + <artifactId>seatunnel-shade-hadoop3-uber</artifactId> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>org.apache.avro</groupId> + <artifactId>avro</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers</artifactId> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-gcs-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/gcs/GcsFileIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-gcs-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/gcs/GcsFileIT.java new file mode 100644 index 0000000000..7bcfb40c1f --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-gcs-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/gcs/GcsFileIT.java @@ -0,0 +1,104 @@ +/* + * 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.e2e.connector.file.gcs; + +import org.apache.seatunnel.e2e.common.TestResource; +import org.apache.seatunnel.e2e.common.TestSuiteBase; +import org.apache.seatunnel.e2e.common.container.EngineType; +import org.apache.seatunnel.e2e.common.container.TestContainer; +import org.apache.seatunnel.e2e.common.container.TestContainerId; +import org.apache.seatunnel.e2e.common.container.seatunnel.SeaTunnelContainer; +import org.apache.seatunnel.e2e.common.junit.DisabledOnContainer; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestTemplate; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.lifecycle.Startables; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.DockerLoggerFactory; +import org.testcontainers.utility.MountableFile; + +import java.util.stream.Stream; + +@DisabledOnContainer( + value = {TestContainerId.SPARK_2_4}, + type = {EngineType.FLINK}, + disabledReason = "The GCS connector requires Hadoop 3, but these images use Hadoop 2.7") +public class GcsFileIT extends TestSuiteBase implements TestResource { + + private static final String FAKE_GCS_IMAGE = "fsouza/fake-gcs-server:1.56.1"; + private static final String FAKE_GCS_HOST = "fake-gcs"; + private static final int FAKE_GCS_PORT = 4443; + private static final String JOB_CONFIG = "/gcs/gcs_file_to_assert.conf"; + + private GenericContainer<?> fakeGcs; + + @BeforeAll + @Override + public void startUp() { + DockerImageName image = DockerImageName.parse(FAKE_GCS_IMAGE); + fakeGcs = + new GenericContainer<>(image) + .withCommand( + "-scheme", + "http", + "-port", + String.valueOf(FAKE_GCS_PORT), + "-external-url", + "http://" + FAKE_GCS_HOST + ":" + FAKE_GCS_PORT) + .withNetwork(NETWORK) + .withNetworkAliases(FAKE_GCS_HOST) + .withExposedPorts(FAKE_GCS_PORT) + .withCopyFileToContainer( + MountableFile.forClasspathResource("gcs/data/e2e.json"), + "/data/seatunnel-gcs-test/input/e2e.json") + .withLogConsumer( + new Slf4jLogConsumer( + DockerLoggerFactory.getLogger( + image.asCanonicalNameString()))) + .waitingFor( + Wait.forHttp("/storage/v1/b") + .forPort(FAKE_GCS_PORT) + .forStatusCode(200)); + Startables.deepStart(Stream.of(fakeGcs)).join(); + SeaTunnelContainer.enableGcsOpenCensusThreadExemption(); + } + + @AfterAll + @Override + public void tearDown() { + try { + if (fakeGcs != null) { + fakeGcs.close(); + } + } finally { + SeaTunnelContainer.disableGcsOpenCensusThreadExemption(); + } + } + + @TestTemplate + public void testReadJsonFromGcs(TestContainer container) throws Exception { + Container.ExecResult result = container.executeJob(JOB_CONFIG); + Assertions.assertEquals(0, result.getExitCode(), result.getStderr()); + } +} diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-gcs-e2e/src/test/resources/gcs/data/e2e.json b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-gcs-e2e/src/test/resources/gcs/data/e2e.json new file mode 100644 index 0000000000..30c388d30d --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-gcs-e2e/src/test/resources/gcs/data/e2e.json @@ -0,0 +1,2 @@ +{"id":1,"name":"alice"} +{"id":2,"name":"bob"} diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-gcs-e2e/src/test/resources/gcs/gcs_file_to_assert.conf b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-gcs-e2e/src/test/resources/gcs/gcs_file_to_assert.conf new file mode 100644 index 0000000000..f5ce0cf5a3 --- /dev/null +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-gcs-e2e/src/test/resources/gcs/gcs_file_to_assert.conf @@ -0,0 +1,84 @@ +# +# 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. +# + +env { + parallelism = 1 + job.mode = "BATCH" + + spark.app.name = "SeaTunnel" + spark.executor.instances = 1 + spark.executor.cores = 1 + spark.executor.memory = "1g" + spark.master = local +} + +source { + GcsFile { + bucket = "gs://seatunnel-gcs-test" + path = "/input" + file_format_type = "json" + hadoop_gcs_properties = { + "fs.gs.storage.root.url" = "http://fake-gcs:4443/" + "fs.gs.auth.service.account.enable" = "false" + "fs.gs.auth.null.enable" = "true" + "fs.gs.project.id" = "seatunnel-test" + } + schema = { + fields { + id = int + name = string + } + } + } +} + +sink { + Assert { + rules { + row_rules = [ + { + rule_type = MIN_ROW + rule_value = 2 + }, + { + rule_type = MAX_ROW + rule_value = 2 + } + ] + field_rules = [ + { + field_name = id + field_type = int + field_value = [ + { + rule_type = NOT_NULL + } + ] + }, + { + field_name = name + field_type = string + field_value = [ + { + rule_type = NOT_NULL + } + ] + } + ] + } + } +} diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/pom.xml b/seatunnel-e2e/seatunnel-connector-v2-e2e/pom.xml index 8b09ffffed..ec50d4656b 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/pom.xml +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/pom.xml @@ -46,6 +46,7 @@ <module>connector-file-sftp-e2e</module> <module>connector-file-oss-e2e</module> <module>connector-file-s3-e2e</module> + <module>connector-file-gcs-e2e</module> <module>connector-cassandra-e2e</module> <module>connector-neo4j-e2e</module> <module>connector-http-e2e</module> diff --git a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/SeaTunnelContainer.java b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/SeaTunnelContainer.java index 3764daebff..3fb09b187e 100644 --- a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/SeaTunnelContainer.java +++ b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/SeaTunnelContainer.java @@ -537,6 +537,16 @@ public class SeaTunnelContainer extends AbstractTestContainer { azureQueueE2eActive = false; } + /** Enables GCS OpenCensus thread exemptions while the GCS file E2E test is active. */ + public static void enableGcsOpenCensusThreadExemption() { + gcsE2eActive = true; + } + + /** Disables GCS OpenCensus thread exemptions after the GCS file E2E test completes. */ + public static void disableGcsOpenCensusThreadExemption() { + gcsE2eActive = false; + } + /** * {@code true} while the Couchbase E2E test ({@code CouchbaseIT}) is active. * @@ -551,6 +561,9 @@ public class SeaTunnelContainer extends AbstractTestContainer { /** {@code true} while the Azure Queue Storage E2E test is active. */ static volatile boolean azureQueueE2eActive = false; + /** {@code true} while the GCS file E2E test is active. */ + static volatile boolean gcsE2eActive = false; + /** The thread should be recycled but not, we should fix it in the future. */ protected boolean isIssueWeAlreadyKnow(String threadName) { // Couchbase SDK JVM-global static singleton threads. @@ -588,6 +601,11 @@ public class SeaTunnelContainer extends AbstractTestContainer { if (isAzureQueueReactorThreadExempt(threadName)) { return true; } + // The shaded GCS client's OpenCensus exporters are JVM-global daemon threads. Their names + // are shared by other OpenCensus users, so exempt them only for the GCS E2E lifecycle. + if (isGcsOpenCensusThreadExempt(threadName)) { + return true; + } // ClickHouse com.clickhouse.client.ClickHouseClientBuilder return threadName.startsWith("ClickHouseClientWorker") // InfluxDB okio.AsyncTimeout$Watchdog @@ -631,6 +649,12 @@ public class SeaTunnelContainer extends AbstractTestContainer { || (threadName.startsWith("boundedElastic-evictor-") && azureQueueE2eActive); } + static boolean isGcsOpenCensusThreadExempt(String threadName) { + return gcsE2eActive + && (threadName.startsWith("ExportComponent.ServiceExporterThread-") + || threadName.startsWith("OpenCensus.Disruptor-")); + } + @Override public Container.ExecResult savepointJob(String jobId) throws IOException, InterruptedException { diff --git a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/SeaTunnelContainerThreadExemptionTest.java b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/SeaTunnelContainerThreadExemptionTest.java index 8f382da40c..63bbf34ac3 100644 --- a/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/SeaTunnelContainerThreadExemptionTest.java +++ b/seatunnel-e2e/seatunnel-e2e-common/src/test/java/org/apache/seatunnel/e2e/common/container/seatunnel/SeaTunnelContainerThreadExemptionTest.java @@ -30,7 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Regression tests for the Couchbase SDK thread-exemption routing fix (Blocker 2). + * Regression tests for lifecycle-scoped third-party client thread exemptions. * * <p>Before the fix, four Couchbase-specific thread names were exempted inside the <em>global</em> * {@code isSystemThread()} method. This meant any Reactor-based connector that leaked a scheduler @@ -65,12 +65,13 @@ class SeaTunnelContainerThreadExemptionTest { private static SeaTunnelContainer containerMock; /** - * Resets the Couchbase lifecycle flag after every test that may have set it, so that tests - * remain isolated regardless of execution order. + * Resets lifecycle flags after every test that may have set one, so tests remain isolated + * regardless of execution order. */ @AfterEach - void resetCouchbaseFlag() { + void resetLifecycleFlags() { SeaTunnelContainer.disableCouchbaseParallelThreadExemption(); + SeaTunnelContainer.disableGcsOpenCensusThreadExemption(); } @BeforeAll @@ -219,6 +220,21 @@ class SeaTunnelContainerThreadExemptionTest { "Thread 'parallel-reactor' must not match the Couchbase numeric-only pattern"); } + @ParameterizedTest(name = "GCS OpenCensus thread {0} is scoped to the GCS E2E lifecycle") + @ValueSource(strings = {"ExportComponent.ServiceExporterThread-0", "OpenCensus.Disruptor-0"}) + void isIssueWeAlreadyKnow_gcsOpenCensusThreads_areLifecycleScoped(String threadName) + throws Exception { + assertFalse( + isIssueWeAlreadyKnow(threadName), + "GCS OpenCensus threads must not be exempted outside the GCS E2E lifecycle"); + + SeaTunnelContainer.enableGcsOpenCensusThreadExemption(); + + assertTrue( + isIssueWeAlreadyKnow(threadName), + "GCS OpenCensus threads must be exempted while the GCS E2E is active"); + } + // ------------------------------------------------------------------------- // Sanity — existing global system threads must still pass isSystemThread // -------------------------------------------------------------------------
