This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new ff5a90c6f79 Support setting an S3 object ACL for remote task logs
(#70394)
ff5a90c6f79 is described below
commit ff5a90c6f79ec2ff141085c938150d4b4668e188
Author: PoAn Yang <[email protected]>
AuthorDate: Sun Aug 2 01:34:31 2026 +0900
Support setting an S3 object ACL for remote task logs (#70394)
* Support setting an S3 object ACL for remote task logs
Signed-off-by: PoAn Yang <[email protected]>
* Record the S3 log ACL option against the release that ships it
Amazon provider 9.33.0 is already tagged and released, so it cannot
contain this option.
---------
Signed-off-by: PoAn Yang <[email protected]>
Co-authored-by: Jarek Potiuk <[email protected]>
---
providers/amazon/docs/logging/s3-task-handler.rst | 20 ++++++++
providers/amazon/provider.yaml | 15 ++++++
.../providers/amazon/aws/log/s3_task_handler.py | 7 +++
.../airflow/providers/amazon/get_provider_info.py | 7 +++
.../unit/amazon/aws/log/test_s3_task_handler.py | 53 ++++++++++++++++++++++
5 files changed, 102 insertions(+)
diff --git a/providers/amazon/docs/logging/s3-task-handler.rst
b/providers/amazon/docs/logging/s3-task-handler.rst
index 577a1d3f38f..9870f4ea554 100644
--- a/providers/amazon/docs/logging/s3-task-handler.rst
+++ b/providers/amazon/docs/logging/s3-task-handler.rst
@@ -43,6 +43,26 @@ To enable this feature, ``airflow.cfg`` must be configured
as follows:
In the above example, Airflow will try to use
``S3Hook(aws_conn_id='my_s3_conn')``.
+Setting an object ACL for cross-account logging
+'''''''''''''''''''''''''''''''''''''''''''''''
+
+When Airflow runs under one AWS account but writes logs to a bucket owned by a
different
+account, S3 makes the writing account the owner of each uploaded log object.
As a result the
+bucket owner cannot read or manage the logs. Applying the
``bucket-owner-full-control`` ACL
+on upload grants the bucket owner full control over the log objects.
+
+Set the ACL with the ``[aws] s3_task_handler_acl_policy`` option:
+
+.. code-block:: ini
+
+ [aws]
+ # ACL applied to every task log object uploaded to S3, e.g. for
cross-account buckets.
+ s3_task_handler_acl_policy = bucket-owner-full-control
+
+When unset, no ACL is sent and the bucket's default object ownership applies.
The same value can
+also be supplied through ``[logging] remote_task_handler_kwargs``, for example
+``{"acl_policy": "bucket-owner-full-control"}``.
+
You can also use `LocalStack <https://localstack.cloud/>`_ to emulate Amazon
S3 locally.
To configure it, you must additionally set the endpoint url to point to your
local stack.
You can do this via the Connection Extra ``endpoint_url`` field.
diff --git a/providers/amazon/provider.yaml b/providers/amazon/provider.yaml
index 343398db658..fecd3a9b236 100644
--- a/providers/amazon/provider.yaml
+++ b/providers/amazon/provider.yaml
@@ -1169,6 +1169,21 @@ config:
version_added: 8.7.2
example:
airflow.providers.amazon.aws.log.cloudwatch_task_handler.json_serialize
default:
airflow.providers.amazon.aws.log.cloudwatch_task_handler.json_serialize_legacy
+ s3_task_handler_acl_policy:
+ description: |
+ The ACL applied to task log objects uploaded to S3 by the S3 remote
log handler,
+ for example ``bucket-owner-full-control``.
+
+ This is primarily useful for cross-account remote logging: when
Airflow runs under one AWS
+ account but writes logs to a bucket owned by another account, S3
makes the writing account
+ the object owner, so the bucket owner cannot read or manage the log
objects. Setting
+ ``bucket-owner-full-control`` grants the bucket owner full control
over the uploaded logs.
+
+ When unset, no ACL is sent and the bucket's default object ownership
applies.
+ type: string
+ version_added: 9.34.0
+ example: bucket-owner-full-control
+ default: ~
aws_batch_executor:
description: |
This section only applies if you are using the AwsBatchExecutor in
diff --git
a/providers/amazon/src/airflow/providers/amazon/aws/log/s3_task_handler.py
b/providers/amazon/src/airflow/providers/amazon/aws/log/s3_task_handler.py
index 6ef82f301da..68a88b679eb 100644
--- a/providers/amazon/src/airflow/providers/amazon/aws/log/s3_task_handler.py
+++ b/providers/amazon/src/airflow/providers/amazon/aws/log/s3_task_handler.py
@@ -44,6 +44,7 @@ class S3RemoteLogIO(LoggingMixin): # noqa: D101
remote_base: str
base_log_folder: pathlib.Path = attrs.field(converter=pathlib.Path)
delete_local_copy: bool
+ acl_policy: str | None = None
processors = ()
@@ -68,6 +69,7 @@ class S3RemoteLogIO(LoggingMixin): # noqa: D101
"base_log_folder":
os.path.expanduser(conf.get_mandatory_value("logging", "base_log_folder")),
"remote_base": conf.get_mandatory_value("logging",
"remote_base_log_folder"),
"delete_local_copy": conf.getboolean("logging",
"delete_local_logs"),
+ "acl_policy": conf.get("aws", "s3_task_handler_acl_policy",
fallback=None) or None,
}
| io_kwargs,
)
@@ -161,6 +163,8 @@ class S3RemoteLogIO(LoggingMixin): # noqa: D101
extra_args = {}
if conf.getboolean("logging", "ENCRYPT_S3_LOGS"):
extra_args["ServerSideEncryption"] = "AES256"
+ if self.acl_policy:
+ extra_args["ACL"] = self.acl_policy
# Default to a single retry attempt because s3 upload failures are
# rare but occasionally occur. Multiple retry attempts are unlikely
@@ -235,6 +239,9 @@ class S3TaskHandler(FileTaskHandler, LoggingMixin):
delete_local_copy=kwargs.get(
"delete_local_copy", conf.getboolean("logging",
"delete_local_logs")
),
+ acl_policy=kwargs.get(
+ "acl_policy", conf.get("aws", "s3_task_handler_acl_policy",
fallback=None) or None
+ ),
)
def set_context(self, ti: TaskInstance, *, identifier: str | None = None)
-> None:
diff --git a/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
b/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
index a3e609aa5a8..8e249e606fe 100644
--- a/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
+++ b/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
@@ -1283,6 +1283,13 @@ def get_provider_info():
"example":
"airflow.providers.amazon.aws.log.cloudwatch_task_handler.json_serialize",
"default":
"airflow.providers.amazon.aws.log.cloudwatch_task_handler.json_serialize_legacy",
},
+ "s3_task_handler_acl_policy": {
+ "description": "The ACL applied to task log objects
uploaded to S3 by the S3 remote log handler,\nfor example
``bucket-owner-full-control``.\n\nThis is primarily useful for cross-account
remote logging: when Airflow runs under one AWS\naccount but writes logs to a
bucket owned by another account, S3 makes the writing account\nthe object
owner, so the bucket owner cannot read or manage the log objects.
Setting\n``bucket-owner-full-control`` grants the bucket owner [...]
+ "type": "string",
+ "version_added": "9.34.0",
+ "example": "bucket-owner-full-control",
+ "default": None,
+ },
},
},
"aws_batch_executor": {
diff --git a/providers/amazon/tests/unit/amazon/aws/log/test_s3_task_handler.py
b/providers/amazon/tests/unit/amazon/aws/log/test_s3_task_handler.py
index 0576756c196..5fcbedf8dda 100644
--- a/providers/amazon/tests/unit/amazon/aws/log/test_s3_task_handler.py
+++ b/providers/amazon/tests/unit/amazon/aws/log/test_s3_task_handler.py
@@ -132,6 +132,50 @@ class TestS3RemoteLogIOFromConfig:
assert conn_id == "aws_default"
legacy_discover.assert_not_called()
+ @conf_vars(
+ {
+ ("logging", "remote_base_log_folder"):
"s3://bucket/remote/log/location",
+ ("aws", "s3_task_handler_acl_policy"): "bucket-owner-full-control",
+ }
+ )
+ def test_from_config_reads_acl_policy(self):
+ subject = S3RemoteLogIO.from_config()
+
+ assert subject.acl_policy == "bucket-owner-full-control"
+
+ @conf_vars({("logging", "remote_base_log_folder"):
"s3://bucket/remote/log/location"})
+ def test_from_config_acl_policy_defaults_to_none(self):
+ subject = S3RemoteLogIO.from_config()
+
+ assert subject.acl_policy is None
+
+ @conf_vars(
+ {
+ ("logging", "remote_base_log_folder"):
"s3://bucket/remote/log/location",
+ ("logging", "remote_task_handler_kwargs"): '{"acl_policy":
"bucket-owner-full-control"}',
+ }
+ )
+ def test_from_config_acl_policy_via_remote_task_handler_kwargs(self):
+ subject = S3RemoteLogIO.from_config()
+
+ assert subject.acl_policy == "bucket-owner-full-control"
+
+
+class TestS3TaskHandlerInit:
+ @conf_vars({("aws", "s3_task_handler_acl_policy"):
"bucket-owner-full-control"})
+ def test_init_reads_acl_policy_from_conf(self):
+ handler = S3TaskHandler("/tmp/local",
"s3://bucket/remote/log/location")
+
+ assert handler.io.acl_policy == "bucket-owner-full-control"
+
+ @conf_vars({("aws", "s3_task_handler_acl_policy"):
"bucket-owner-full-control"})
+ def test_init_acl_policy_kwarg_overrides_conf(self):
+ handler = S3TaskHandler(
+ "/tmp/local", "s3://bucket/remote/log/location",
acl_policy="bucket-owner-read"
+ )
+
+ assert handler.io.acl_policy == "bucket-owner-read"
+
@pytest.mark.db_test
class TestS3RemoteLogIO:
@@ -280,6 +324,15 @@ class TestS3RemoteLogIO:
body = boto3.resource("s3").Object("bucket",
self.remote_log_key).get()["Body"].read()
assert body == b"text"
+ def test_write_with_acl_policy(self):
+ self.subject.acl_policy = "bucket-owner-full-control"
+ conn = self.subject.hook.get_conn()
+ with mock.patch.object(conn, "put_object", wraps=conn.put_object) as
mock_put_object:
+ self.subject.write("text", self.remote_log_location)
+ assert mock_put_object.call_args.kwargs["ACL"] ==
"bucket-owner-full-control"
+ body = boto3.resource("s3").Object("bucket",
self.remote_log_key).get()["Body"].read()
+ assert body == b"text"
+
def test_upload_repeated_appends_no_duplication(self):
"""Simulate reschedule-mode sensor: each cycle appends to the local
log, then uploads.