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

tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/main by this push:
     new 27e8518ae1 TIKA-5009: Make projectId optional in the GCS plugin (#3445)
27e8518ae1 is described below

commit 27e8518ae167a1545c009b0e78e8efc290f206b0
Author: Bartek Ciszkowski <[email protected]>
AuthorDate: Wed Oct 28 15:47:26 2026 -0300

    TIKA-5009: Make projectId optional in the GCS plugin (#3445)
    
    * TIKA-5009: Make projectId optional in the GCS plugin
    
    The GCS fetcher, emitter and pipes iterator all fail to build without a
    projectId, but none of their calls need one. They read, write and list
    objects in a specified bucket.
    
    The java client also does not require projectId, and a comment within it
    states "Project ID is only required for creating buckets, so we don't
    requite it for creating the service.":
    
    
https://github.com/googleapis/google-cloud-java/blob/123f97c3f843dc7a3ff7b4ef0596c1425502a275/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java#L326
    
    The docs also state projectId picks the project billed for API calls,
    but storage access is billed to the project that owns the bucket; this
    has been corrected.
    
    * fixup! TIKA-5009: Make projectId optional in the GCS plugin
---
 CHANGES.txt                                        |  3 +
 docs/modules/ROOT/pages/pipes/plugins/gcs.adoc     | 14 ++--
 .../apache/tika/pipes/emitter/gcs/GCSEmitter.java  |  9 ++-
 .../tika/pipes/emitter/gcs/GCSEmitterConfig.java   |  3 -
 .../apache/tika/pipes/fetcher/gcs/GCSFetcher.java  | 13 ++--
 .../tika/pipes/iterator/gcs/GCSPipesIterator.java  | 14 ++--
 .../apache/tika/pipes/gcs/ConfigExamplesTest.java  |  4 +-
 .../apache/tika/pipes/gcs/GCSProjectIdTest.java    | 86 ++++++++++++++++++++++
 .../resources/config-examples/gcs-fetcher.json     |  1 -
 9 files changed, 116 insertions(+), 31 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index bef8eef13b..2500a89594 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -22,6 +22,9 @@ Release 4.3.0 - ???
      for EKS IAM roles for service accounts (IRSA); the plugin now bundles the 
AWS STS
      module it needs (TIKA-5006).
 
+   * The GCS fetcher, emitter and pipes iterator no longer require projectId. 
When it is
+     unset, the client resolves a project from the environment, as it does for 
its
+     credentials (TIKA-5009).
    * The Kafka emitter reports success only once Kafka has acknowledged every 
record
      (per acks); a rejected or timed-out record fails the emit instead of 
being lost
      silently, and the producer is flushed and closed on shutdown. (TIKA-5008).
diff --git a/docs/modules/ROOT/pages/pipes/plugins/gcs.adoc 
b/docs/modules/ROOT/pages/pipes/plugins/gcs.adoc
index dbfcfa8259..e32ed8386d 100644
--- a/docs/modules/ROOT/pages/pipes/plugins/gcs.adoc
+++ b/docs/modules/ROOT/pages/pipes/plugins/gcs.adoc
@@ -47,7 +47,7 @@ The GCS plugin relies on Google's Application Default 
Credentials chain — ther
 * Setting the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to the 
path of a service-account JSON key.
 * Running `gcloud auth application-default login` for local development.
 
-The `projectId` field in each component selects which GCP project to bill the 
API calls against; the service account or user must have storage access to the 
named bucket.
+`projectId` is optional. Reading, writing and listing objects needs no 
project, and storage access is billed to the project that owns the bucket. When 
unset, the client resolves a project from the environment 
(`GOOGLE_CLOUD_PROJECT`, the credentials file, the gcloud config or the GCE 
metadata server). The service account or user must have storage access to the 
named bucket.
 
 [#gcs-fetcher]
 == GCS Fetcher (`gcs-fetcher`)
@@ -66,8 +66,8 @@ include::example$pipes-gcs-fetcher.json[]
 |Field |Default |Description
 
 |`projectId`
-|_required_
-|GCP project ID for billing/authentication.
+|_none_
+|Optional GCP project ID. See <<credentials>>.
 
 |`bucket`
 |_required_
@@ -99,8 +99,8 @@ include::example$pipes-gcs-emitter.json[]
 |Field |Default |Description
 
 |`projectId`
-|_required_
-|GCP project ID (validated non-blank).
+|_none_
+|Optional GCP project ID. See <<credentials>>.
 
 |`bucket`
 |_required_
@@ -138,8 +138,8 @@ In addition to the required `fetcherId` / `emitterId` (see 
xref:pipes/iterators.
 |GCS bucket to enumerate.
 
 |`projectId`
-|`""`
-|GCP project ID for the listing API call.
+|_none_
+|Optional GCP project ID. See <<credentials>>.
 
 |`prefix`
 |`""`
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/emitter/gcs/GCSEmitter.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/emitter/gcs/GCSEmitter.java
index e3a9080e5d..e0bf0b0909 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/emitter/gcs/GCSEmitter.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/emitter/gcs/GCSEmitter.java
@@ -82,10 +82,11 @@ public class GCSEmitter extends AbstractStreamEmitter {
     }
 
     private static Storage buildStorage(GCSEmitterConfig config) {
-        return StorageOptions.newBuilder()
-                .setProjectId(config.projectId())
-                .build()
-                .getService();
+        StorageOptions.Builder builder = StorageOptions.newBuilder();
+        if (!StringUtils.isBlank(config.projectId())) {
+            builder.setProjectId(config.projectId());
+        }
+        return builder.build().getService();
     }
 
     @Override
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/emitter/gcs/GCSEmitterConfig.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/emitter/gcs/GCSEmitterConfig.java
index a7408ba4b3..15a3264a55 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/emitter/gcs/GCSEmitterConfig.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/emitter/gcs/GCSEmitterConfig.java
@@ -34,9 +34,6 @@ public record GCSEmitterConfig(
     }
 
     public void validate() throws TikaConfigException {
-        if (projectId == null || projectId.isBlank()) {
-            throw new TikaConfigException("'projectId' must not be empty");
-        }
         if (bucket == null || bucket.isBlank()) {
             throw new TikaConfigException("'bucket' must not be empty");
         }
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/fetcher/gcs/GCSFetcher.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/fetcher/gcs/GCSFetcher.java
index 1f45615b41..82fc436d07 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/fetcher/gcs/GCSFetcher.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/fetcher/gcs/GCSFetcher.java
@@ -38,9 +38,10 @@ import org.apache.tika.pipes.api.fetcher.Fetcher;
 import org.apache.tika.pipes.fetcher.gcs.config.GCSFetcherConfig;
 import org.apache.tika.plugins.AbstractTikaExtension;
 import org.apache.tika.plugins.ExtensionConfig;
+import org.apache.tika.utils.StringUtils;
 
 /**
- * Fetches files from google cloud storage. Must set projectId and bucket via 
the config.
+ * Fetches files from google cloud storage. Must set bucket via the config; 
projectId is optional.
  */
 public class GCSFetcher extends AbstractTikaExtension implements Fetcher {
 
@@ -64,12 +65,12 @@ public class GCSFetcher extends AbstractTikaExtension 
implements Fetcher {
 
     private void initialize() throws TikaConfigException {
         ConfigValidator.mustNotBeEmpty("bucket", config.getBucket());
-        ConfigValidator.mustNotBeEmpty("projectId", config.getProjectId());
 
-        storage = StorageOptions.newBuilder()
-                .setProjectId(config.getProjectId())
-                .build()
-                .getService();
+        StorageOptions.Builder builder = StorageOptions.newBuilder();
+        if (!StringUtils.isBlank(config.getProjectId())) {
+            builder.setProjectId(config.getProjectId());
+        }
+        storage = builder.build().getService();
     }
 
     @Override
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/iterator/gcs/GCSPipesIterator.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/iterator/gcs/GCSPipesIterator.java
index 8fb6df1547..45804e3c89 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/iterator/gcs/GCSPipesIterator.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/main/java/org/apache/tika/pipes/iterator/gcs/GCSPipesIterator.java
@@ -50,16 +50,12 @@ public class GCSPipesIterator extends PipesIteratorBase {
         if (StringUtils.isBlank(config.getBucket())) {
             throw new TikaConfigException("bucket must not be empty");
         }
-        if (StringUtils.isBlank(config.getProjectId())) {
-            throw new TikaConfigException("projectId must not be empty");
-        }
 
-        // Initialize the GCS client
-        this.storage = StorageOptions
-                .newBuilder()
-                .setProjectId(config.getProjectId())
-                .build()
-                .getService();
+        StorageOptions.Builder builder = StorageOptions.newBuilder();
+        if (!StringUtils.isBlank(config.getProjectId())) {
+            builder.setProjectId(config.getProjectId());
+        }
+        this.storage = builder.build().getService();
     }
 
     public static GCSPipesIterator build(ExtensionConfig extensionConfig) 
throws IOException, TikaConfigException {
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/test/java/org/apache/tika/pipes/gcs/ConfigExamplesTest.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/test/java/org/apache/tika/pipes/gcs/ConfigExamplesTest.java
index 52ce0a1fe5..eab655c387 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/test/java/org/apache/tika/pipes/gcs/ConfigExamplesTest.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/test/java/org/apache/tika/pipes/gcs/ConfigExamplesTest.java
@@ -17,6 +17,7 @@
 package org.apache.tika.pipes.gcs;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import org.junit.jupiter.api.Test;
@@ -41,7 +42,8 @@ public class ConfigExamplesTest extends 
AbstractConfigExamplesTest {
         JsonNode inner = innerComponent(readExample("gcs-fetcher.json"),
                 "fetchers", "gcsf", "gcs-fetcher");
         GCSFetcherConfig config = GCSFetcherConfig.load(inner.toString());
-        assertEquals("my-gcp-project", config.getProjectId());
+        // The fetcher example leaves projectId out to show it is optional.
+        assertNull(config.getProjectId());
         assertEquals("my-tika-input", config.getBucket());
     }
 
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/test/java/org/apache/tika/pipes/gcs/GCSProjectIdTest.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/test/java/org/apache/tika/pipes/gcs/GCSProjectIdTest.java
new file mode 100644
index 0000000000..cb7ba0c140
--- /dev/null
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/test/java/org/apache/tika/pipes/gcs/GCSProjectIdTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.tika.pipes.gcs;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.pipes.emitter.gcs.GCSEmitter;
+import org.apache.tika.pipes.fetcher.gcs.GCSFetcher;
+import org.apache.tika.pipes.iterator.gcs.GCSPipesIterator;
+import org.apache.tika.plugins.ExtensionConfig;
+
+/**
+ * projectId is optional: reading, writing and listing objects need no 
project, so each
+ * component builds without one. Building makes no request, so these need no 
credentials.
+ */
+public class GCSProjectIdTest {
+
+    private static final String BUCKET_ONLY = "{\"bucket\": \"bucket\"}";
+    private static final String WITH_PROJECT = "{\"bucket\": \"bucket\", 
\"projectId\": \"project\"}";
+
+    @Test
+    public void testFetcherWithoutProjectId() throws Exception {
+        assertNotNull(GCSFetcher.build(new ExtensionConfig("gcs", 
"gcs-fetcher", BUCKET_ONLY)));
+    }
+
+    @Test
+    public void testFetcherWithProjectId() throws Exception {
+        assertNotNull(GCSFetcher.build(new ExtensionConfig("gcs", 
"gcs-fetcher", WITH_PROJECT)));
+    }
+
+    @Test
+    public void testFetcherRequiresBucket() {
+        assertThrows(TikaConfigException.class,
+                () -> GCSFetcher.build(new ExtensionConfig("gcs", 
"gcs-fetcher", "{}")));
+    }
+
+    @Test
+    public void testEmitterWithoutProjectId() throws Exception {
+        assertNotNull(GCSEmitter.build(new ExtensionConfig("gcs", 
"gcs-emitter", BUCKET_ONLY)));
+    }
+
+    @Test
+    public void testEmitterWithProjectId() throws Exception {
+        assertNotNull(GCSEmitter.build(new ExtensionConfig("gcs", 
"gcs-emitter", WITH_PROJECT)));
+    }
+
+    @Test
+    public void testEmitterRequiresBucket() {
+        assertThrows(TikaConfigException.class,
+                () -> GCSEmitter.build(new ExtensionConfig("gcs", 
"gcs-emitter", "{}")));
+    }
+
+    @Test
+    public void testIteratorWithoutProjectId() throws Exception {
+        assertNotNull(GCSPipesIterator.build(new ExtensionConfig("gcs", 
"gcs-pipes-iterator", BUCKET_ONLY)));
+    }
+
+    @Test
+    public void testIteratorWithProjectId() throws Exception {
+        assertNotNull(GCSPipesIterator.build(new ExtensionConfig("gcs", 
"gcs-pipes-iterator", WITH_PROJECT)));
+    }
+
+    @Test
+    public void testIteratorRequiresBucket() {
+        assertThrows(TikaConfigException.class,
+                () -> GCSPipesIterator.build(new ExtensionConfig("gcs", 
"gcs-pipes-iterator", "{}")));
+    }
+}
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/test/resources/config-examples/gcs-fetcher.json
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/test/resources/config-examples/gcs-fetcher.json
index 89ab85eed3..177445ac28 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/test/resources/config-examples/gcs-fetcher.json
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-gcs/src/test/resources/config-examples/gcs-fetcher.json
@@ -2,7 +2,6 @@
   "fetchers": {
     "gcsf": {
       "gcs-fetcher": {
-        "projectId": "my-gcp-project",
         "bucket": "my-tika-input",
         "extractUserMetadata": true,
         "spoolToTemp": true

Reply via email to