Repository: cloud-custodian/cloud-custodian (Python)

Issue:
"{now}" Generating Exception When Running With 0.9.19
### Describe the bug

We utilize a policy to track Cloud Custodian evaluation of resources, essentially to give a buffer upstream before an action is taken again. To track this, we use the `{now}` clause as a value for a tracking tag. See attached policy.

When running this policy with v0.9.19, a JSON-related exception is generated. This behavior **does not** occur when running with 0.9.18, so it appears to be caused by a change in 0.9.19 specifically.

### What did you expect to happen?

C7n runs as expected, without generating an exception.

### Cloud Provider

Amazon Web Services (AWS)

### Cloud Custodian version and dependency information

```shell
Custodian:   0.9.19
Python:      3.9.13 (main, May 24 2022, 21:28:31)
             [Clang 13.1.6 (clang-1316.0.21.2)]
Platform:    posix.uname_result(sysname='Darwin', nodename='jcarlson-pro', release='21.6.0', version='Darwin Kernel Version 21.6.0: Mon Aug 22 20:17:10 PDT 2022; root:xnu-8020.140.49~2/RELEASE_X86_64', machine='x86_64')
Using venv:  True
Docker: False
Installed:

PyYAML==6.0
Pygments==2.13.0
argcomplete==2.0.0
attrs==22.1.0
aws-xray-sdk==2.10.0
bleach==5.0.1
boto3==1.24.87
botocore==1.27.87
certifi==2022.9.24
charset-normalizer==2.1.1
click==8.1.3
colorama==0.4.5
coverage==6.5.0
docutils==0.17.1
execnet==1.9.0
flake8==3.9.2
freezegun==1.2.2
idna==3.4
importlib-metadata==4.13.0
importlib-resources==5.9.0
iniconfig==1.1.1
jaraco-classes==3.2.3
jaraco.classes==3.2.3
jmespath==1.0.1
jsonpatch==1.32
jsonpointer==2.3
jsonschema==4.16.0
keyring==23.9.3
mccabe==0.6.1
mock==4.0.3
more-itertools==8.14.0
multidict==6.0.2
packaging==21.3
pkginfo==1.8.3
pkgutil-resolve-name==1.3.10
placebo==0.9.0
pluggy==1.0.0
portalocker==2.5.1
psutil==5.9.2
py==1.11.0
pycodestyle==2.7.0
pyflakes==2.3.1
pygments==2.13.0
pyparsing==3.0.9
pyrsistent==0.18.1
pytest==7.1.3
pytest-cov==3.0.0
pytest-forked==1.4.0
pytest-recording==0.12.1
pytest-sugar==0.9.5
pytest-terraform==0.6.4
pytest-xdist==2.5.0
python-dateutil==2.8.2
pyyaml==6.0
readme-renderer==37.2
requests==2.28.1
requests-toolbelt==0.9.1
rfc3986==2.0.0
s3transfer==0.6.0
six==1.16.0
tabulate==0.8.10
termcolor==2.0.1
tomli==2.0.1
tqdm==4.64.1
twine==3.8.0
typing-extensions==4.3.0
urllib3==1.26.12
vcrpy==4.2.1
webencodings==0.5.1
wrapt==1.14.1
yarl==1.8.1
zipp==3.8.1
```


### Policy

```shell
- name: update-mfa-for-user-accts-action
    description: |
      If needed, update the last evaluation timestamp for the MFA enforcement check.
    resource: iam-user
    conditions:
      - region: us-east-1
      - type: value
        key: account_id
        value_type: integer
        value: *target_account
    filters:
      - <<: *exception_tag
      - type: value
        key: tag:user_acct_mfa
        op: gt
        value: 14
        value_type: age
    actions:
      - type: tag
        key: "user_acct_mfa"
        value: "{now}"
```


### Relevant log/traceback output

```shell
Traceback (most recent call last):
  File "/Users/jcarlson/Code/cloud-custodian/venv/bin/c7n-org", line 8, in <module>
    sys.exit(cli())
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/c7n_org/cli.py", line 693, in run
    futures[w.submit(
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/c7n/executor.py", line 28, in submit
    return MainThreadFuture(func(*args, **kw))
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/c7n_org/cli.py", line 613, in run_account
    resources = p.run()
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/c7n/policy.py", line 1294, in __call__
    resources = PullMode(self).run()
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/c7n/policy.py", line 327, in run
    return []
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/c7n/ctx.py", line 95, in __exit__
    self.output.write_file('metadata.json', dumps(self.get_metadata(), indent=2))
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/c7n/utils.py", line 96, in dumps
    return json.dumps(data, cls=DateTimeEncoder, indent=indent)
  File "/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/__init__.py", line 234, in dumps
    return cls(
  File "/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/encoder.py", line 201, in encode
    chunks = list(chunks)
  File "/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/encoder.py", line 431, in _iterencode
    yield from _iterencode_dict(o, _current_indent_level)
  File "/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/encoder.py", line 405, in _iterencode_dict
    yield from chunks
  File "/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/encoder.py", line 405, in _iterencode_dict
    yield from chunks
  File "/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/encoder.py", line 325, in _iterencode_list
    yield from chunks
  File "/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/encoder.py", line 405, in _iterencode_dict
    yield from chunks
  File "/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/encoder.py", line 438, in _iterencode
    o = _default(o)
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/c7n/utils.py", line 216, in default
    return json.JSONEncoder.default(self, obj)
  File "/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type FormatDate is not JSON serializable
```


### Extra information or context

_No response_


Discussion:

Patch:
diff --git a/c7n/utils.py b/c7n/utils.py
index 500795674ee..0ca7a876f9a 100644
--- a/c7n/utils.py
+++ b/c7n/utils.py
@@ -213,6 +213,8 @@ class DateTimeEncoder(json.JSONEncoder):
     def default(self, obj):
         if isinstance(obj, datetime):
             return obj.isoformat()
+        if isinstance(obj, FormatDate):
+            return obj.datetime.isoformat()
         return json.JSONEncoder.default(self, obj)
 
 


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 0722f45a64c..163eb5ad1d0 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -224,6 +224,10 @@ def test_format_date(self):
 
         self.assertEqual("{:+5M%M}".format(utils.FormatDate(d)), "05")
 
+        self.assertEqual(json.dumps(utils.FormatDate(d),
+                                    cls=utils.DateTimeEncoder, indent=2),
+                         '"2018-02-02T12:00:00"')
+
     def test_group_by(self):
         items = [{}, {"Type": "a"}, {"Type": "a"}, {"Type": "b"}]
         self.assertEqual(list(utils.group_by(items, "Type").keys()), [None, "a", "b"])