Repository: cloud-custodian/cloud-custodian

Issue:
`AttributeError: region` since 0.9.22.0
### Describe the bug

Reported via Slack...

> Any ideas why I keep getting this error. This happens when copying the logs to S3 and it happens in 0.9.24 and the two versions before

### Cloud Provider

Amazon Web Services (AWS)

### Cloud Custodian version and dependency information

```shell
0.9.22.0 - 0.9.24.0
```


### Policy

```shell
mode_trail: &cloudtrail
  type: cloudtrail
  execution-options:
    region: us-west-2
    metrics: 'aws://?namespace=test'
    log_group: 'test'
    output_dir: 's3://test-infs-artifacts-nonprod-pdx/logs/{account_id}/{policy_name}/{now:%Y-%m-%d-%H-%M}'
  timeout: 200
  function-prefix: 'test-pol-'
  role: 'arn:aws:iam::{account_id}:role/{lambda_role}'
  tags:
    <<: *lambda_tags
```


### Relevant log/traceback output

```shell
[ERROR]	2023-03-22T04:03:07.268Z	99ce7cbe-33e2-4a5b-b383-26b4129ec402	error during policy execution
Traceback (most recent call last):
  File "/var/task/c7n/config.py", line 12, in __getattr__
    return self[k]
KeyError: 'region'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/var/task/c7n/handler.py", line 165, in dispatch_event
    p.push(event, context)
  File "/var/task/c7n/policy.py", line 1290, in push
    return mode.run(event, lambda_ctx)
  File "/var/task/c7n/policy.py", line 504, in run
    return self.run_resource_set(event, resources)
  File "/var/task/c7n/policy.py", line 539, in run_resource_set
    ctx.output.write_file("action-%s" % action.name, utils.dumps(results))
  File "/var/task/c7n/ctx.py", line 103, in __exit__
    self.output.__exit__(exc_type, exc_value, exc_traceback)
  File "/var/task/c7n/output.py", line 524, in __exit__
    self.upload()
  File "/var/task/c7n/output.py", line 534, in upload
    self.upload_file(os.path.join(root, f), key)
  File "/var/task/c7n/resources/aws.py", line 702, in upload_file
    self.transfer.upload_file(
  File "/var/task/c7n/resources/aws.py", line 696, in transfer
    bucket_region = self.config.region or None
  File "/var/task/c7n/config.py", line 14, in __getattr__
    raise AttributeError(k)
AttributeError: region
[ERROR] 2023-03-22T04:03:07.268Z 99ce7cbe-33e2-4a5b-b383-26b4129ec402 error during policy execution Traceback (most recent call last): File "/var/task/c7n/config.py", line 12, in __getattr__ return self[k] KeyError: 'region' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/var/task/c7n/handler.py", line 165, in dispatch_event p.push(event, context) File "/var/task/c7n/policy.py", line 1290, in push return mode.run(event, lambda_ctx) File "/var/task/c7n/policy.py", line 504, in run return self.run_resource_set(event, resources) File "/var/task/c7n/policy.py", line 539, in run_resource_set ctx.output.write_file("action-%s" % action.name, utils.dumps(results)) File "/var/task/c7n/ctx.py", line 103, in __exit__ self.output.__exit__(exc_type, exc_value, exc_traceback) File "/var/task/c7n/output.py", line 524, in __exit__ self.upload() File "/var/task/c7n/output.py", line 534, in upload self.upload_file(os.path.join(root, f), key) File "/var/task/c7n/resources/aws.py", line 702, in upload_file self.transfer.upload_file( File "/var/task/c7n/resources/aws.py", line 696, in transfer bucket_region = self.config.region or None File "/var/task/c7n/config.py", line 14, in __getattr__ raise AttributeError(k) AttributeError: region
```


### Extra information or context

_No response_


Discussion:
> I was able to figure it out - when I passed ?region=us-west-2, I got an access denied message to the S3 Bucket KMS Key. The original message was just not useful.

That suggests this is more of an error-handling issue.
hey @ajkerrigan - we recently upgraded to c7n 0.9.24 and I think we're seeing similar a error when trying to upload logs to s3. I'd have to check to see what version we upgraded from. Wondering what I can do to help you dig further and get more info on this.

```
[ERROR]	2023-04-06T14:00:07.813Z	64b9a9ac-0d32-4972-a515-e201b7a94b4c	error during policy execution
Traceback (most recent call last):
  File "/var/task/c7n/config.py", line 12, in __getattr__
    return self[k]
KeyError: 'region'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/var/task/c7n/handler.py", line 165, in dispatch_event
    p.push(event, context)
  File "/var/task/c7n/policy.py", line 1290, in push
    return mode.run(event, lambda_ctx)
  File "/var/task/c7n/policy.py", line 584, in run
    return PullMode.run(self)
  File "/var/task/c7n/policy.py", line 355, in run
    return []
  File "/var/task/c7n/ctx.py", line 103, in __exit__
    self.output.__exit__(exc_type, exc_value, exc_traceback)
  File "/var/task/c7n/output.py", line 524, in __exit__
    self.upload()
  File "/var/task/c7n/output.py", line 534, in upload
    self.upload_file(os.path.join(root, f), key)
  File "/var/task/c7n/resources/aws.py", line 702, in upload_file
    self.transfer.upload_file(
  File "/var/task/c7n/resources/aws.py", line 696, in transfer
    bucket_region = self.config.region or None
  File "/var/task/c7n/config.py", line 14, in __getattr__
    raise AttributeError(k)
AttributeError: region
```
confirmed that reverting to `0.9.22` resolves this issue. Here's a policy that we're deploying: 

```
vars:
  run_mode: &run_mode
    type: periodic
    # daily at 14:00 UTC
    schedule: "cron(0 14 * * ? *)"
    tags:
      app: "c7n"
      account: "{account_id}"
    execution-options:
      output_dir: "{s3_bucket}/logs/{account_id}/{region}/"
    memory: 1024
  run_conditions: &run_conditions
    conditions:
      - type: value
        key: account_id
        op: in
        value:
          - "123123123123"

policies:
  - name: ami-app-snapshots-cleanup
    resource: ami
    <<: *run_conditions
    filters:
      - "tag:app": present
      - "tag:releaseStatus": "snapshot"
      - type: reduce
        sort-by: "CreationDate"
        group-by: "tag:app"
        order: desc
        discard: 20
    mode:
      <<: *run_mode
    actions:
      - type: deregister
        delete-snapshots: true

```

Resolving patch:
diff --git a/c7n/handler.py b/c7n/handler.py
--- a/c7n/handler.py
+++ b/c7n/handler.py
@@ -10,6 +10,7 @@
 from c7n.config import Config
 from c7n.structure import StructureParser
 from c7n.resources import load_resources
+from c7n.resources.aws import AWS
 from c7n.policy import PolicyCollection
 from c7n.utils import format_event, get_account_id_from_sts, local_session
 
@@ -128,7 +129,8 @@ def init_config(policy_config):
        and exec_options['metrics_enabled']:
         exec_options['metrics_enabled'] = 'aws'
 
-    return Config.empty(**exec_options)
+    config = Config.empty(**exec_options)
+    return AWS().initialize(config)
 
 
 # One time initilization of global environment settings
diff --git a/c7n/resources/aws.py b/c7n/resources/aws.py
--- a/c7n/resources/aws.py
+++ b/c7n/resources/aws.py
@@ -808,7 +808,14 @@ def join_output(output_dir, suffix):
         return output_dir.rstrip('/')
     if output_dir.endswith('://'):
         return output_dir + suffix
-    return output_dir.rstrip('/') + '/%s' % suffix
+    output_url_parts = urlparse.urlparse(output_dir)
+    # for output urls, the end of the url may be a
+    # query string. make sure we add a suffix to
+    # the path component.
+    output_url_parts = output_url_parts._replace(
+        path = output_url_parts.path.rstrip('/') + '/%s' % suffix
+    )
+    return urlparse.urlunparse(output_url_parts)
 
 
 def fake_session():


Test patch:
diff --git a/tests/test_aws.py b/tests/test_aws.py
--- a/tests/test_aws.py
+++ b/tests/test_aws.py
@@ -469,6 +469,11 @@ def test_default_bucket_region_with_explicit_region():
     assert output_dir == conf.output_dir
 
 
+def test_join_output():
+    output_dir = aws.join_output("s3://aws?region=xyz", "suffix")
+    assert output_dir == "s3://aws/suffix?region=xyz"
+
+
 @vcr.use_cassette(
     'tests/data/vcr_cassettes/test_output/default_bucket_region_public.yaml')
 def test_default_bucket_region_is_public():
diff --git a/tests/test_handler.py b/tests/test_handler.py
--- a/tests/test_handler.py
+++ b/tests/test_handler.py
@@ -41,7 +41,10 @@ def test_init_config_exec_option_merge(self):
              'tracer': 'xray',
              'account_id': '007',
              'region': 'us-east-1',
-             'output_dir': 's3://mybucket/output',
+             # S3 uploads use a session in the output bucket's home region.
+             # When initiating a policy config we expect to identify the
+             # output bucket region so it's available at upload time.
+             'output_dir': 's3://mybucket/output?region=us-east-1',
 
              # defaults
              'external_id': None,
@@ -49,7 +52,7 @@ def test_init_config_exec_option_merge(self):
              'profile': None,
              'authorization_file': None,
              'cache': '',
-             'regions': (),
+             'regions': ['us-east-1'],
              'cache_period': 0,
              'log_group': None,
              'metrics': None})