Repository: cloud-custodian/cloud-custodian

Issue:
Define and use of integer variables (YAML-files) - c7n-org
### Describe the bug

I am not able to use defined variables as integer values in policy files.
The solution that was provided here https://github.com/cloud-custodian/cloud-custodian/issues/6734#issuecomment-867604128 is not working:

"{min-size}" --> "Parameter validation failed:\nInvalid type for parameter MinSize, value: 0, type: <class 'str'>, valid types: <class 'int'>"
{min-size} --> "Parameter validation failed:\nInvalid type for parameter MinSize, value: {'min-size': None}, type: <class 'dict'>, valid types: <class 'int'>"

### What did you expect to happen?

That the value pasted as integer.


### Cloud Provider

Amazon Web Services (AWS)

### Cloud Custodian version and dependency information

```shell
0.9.17
```


### Policy

```shell
policies:
  - name: asg-power-off-working-hours
    resource: asg
    mode:
      type: periodic
      schedule: "{lambda-schedule}"
      role: "{asg-lambda-role}"
    filters:
      - type: offhour
    actions:
      - type: resize
        min-size: "{asg_min_size}"
        max-size: "{asg_max_size}"
        desired-size: "{asg_desired_size}"
        save-options-tag: OffHoursPrevious
```


### Relevant log/traceback output

```shell
"{asg_min_size}" --> "Parameter validation failed:\nInvalid type for parameter MinSize, value: 0, type: <class 'str'>, valid types: <class 'int'>"
{asg_min_size} --> "Parameter validation failed:\nInvalid type for parameter MinSize, value: {'min-size': None}, type: <class 'dict'>, valid types: <class 'int'>"
```


### Extra information or context

I saw that there is the field "value_type" for the filter section, but it looks like it can’t be used in the action section.

Comments:

Patch:
diff --git a/c7n/policy.py b/c7n/policy.py
index f3cb71f34..155517ce8 100644
--- a/c7n/policy.py
+++ b/c7n/policy.py
@@ -24,6 +24,7 @@ from c7n.provider import clouds, get_resource_class
 from c7n import deprecated, utils
 from c7n.version import version
 from c7n.query import RetryPageIterator
+from c7n.varfmt import VarFormat
 
 log = logging.getLogger('c7n.policy')
 
@@ -1231,7 +1232,9 @@ class Policy:
         Updates the policy data in-place.
         """
         # format string values returns a copy
-        updated = utils.format_string_values(self.data, **variables)
+        var_fmt = VarFormat()
+        updated = utils.format_string_values(
+            self.data, formatter=var_fmt.format, **variables)
 
         # Several keys should only be expanded at runtime, perserve them.
         if 'member-role' in updated.get('mode', {}):
diff --git a/c7n/utils.py b/c7n/utils.py
index 3c0acf111..65c4ffb51 100644
--- a/c7n/utils.py
+++ b/c7n/utils.py
@@ -580,7 +580,7 @@ def set_value_from_jmespath(source, expression, value, is_first=True):
     source[current_key] = value
 
 
-def format_string_values(obj, err_fallback=(IndexError, KeyError), *args, **kwargs):
+def format_string_values(obj, err_fallback=(IndexError, KeyError), formatter=None, *args, **kwargs):
     """
     Format all string values in an object.
     Return the updated object
@@ -588,16 +588,19 @@ def format_string_values(obj, err_fallback=(IndexError, KeyError), *args, **kwar
     if isinstance(obj, dict):
         new = {}
         for key in obj.keys():
-            new[key] = format_string_values(obj[key], *args, **kwargs)
+            new[key] = format_string_values(obj[key], formatter=formatter, *args, **kwargs)
         return new
     elif isinstance(obj, list):
         new = []
         for item in obj:
-            new.append(format_string_values(item, *args, **kwargs))
+            new.append(format_string_values(item, formatter=formatter, *args, **kwargs))
         return new
     elif isinstance(obj, str):
         try:
-            return obj.format(*args, **kwargs)
+            if formatter:
+                return formatter(obj, *args, **kwargs)
+            else:
+                return obj.format(*args, **kwargs)
         except err_fallback:
             return obj
     else:
diff --git a/c7n/varfmt.py b/c7n/varfmt.py
new file mode 100644
index 000000000..bcf3c7c1b
--- /dev/null
+++ b/c7n/varfmt.py
@@ -0,0 +1,98 @@
+from string import Formatter
+
+
+class VarFormat(Formatter):
+    """Behaves exactly like the stdlib formatter, with one additional behavior.
+
+    when a string has no format_spec and only contains a single expression,
+    retain the type of the source object.
+
+    inspired by https://pypyr.io/docs/substitutions/format-string/
+    """
+
+    def _vformat(
+        self, format_string, args, kwargs, used_args, recursion_depth, auto_arg_index=0
+    ):
+        # This is mostly verbatim from stdlib format.Formatter._vformat
+        # https://github.com/python/cpython/blob/main/Lib/string.py
+        #
+        # we have to copy alot of std logic to override the str cast
+
+        if recursion_depth < 0:
+            raise ValueError('Max string recursion exceeded')
+        result = []
+        for literal_text, field_name, format_spec, conversion in self.parse(
+            format_string
+        ):
+
+            # output the literal text
+            if literal_text:
+                result.append((literal_text, True, None))
+
+            # if there's a field, output it
+            if field_name is not None:
+                # this is some markup, find the object and do
+                #  the formatting
+
+                # handle arg indexing when empty field_names are given.
+                if field_name == '':
+                    if auto_arg_index is False:
+                        raise ValueError(
+                            'cannot switch from manual field '
+                            'specification to automatic field '
+                            'numbering'
+                        )
+                    field_name = str(auto_arg_index)
+                    auto_arg_index += 1
+                elif field_name.isdigit():
+                    if auto_arg_index:
+                        raise ValueError(
+                            'cannot switch from manual field '
+                            'specification to automatic field '
+                            'numbering'
+                        )
+                    # disable auto arg incrementing, if it gets
+                    # used later on, then an exception will be raised
+                    auto_arg_index = False
+
+                # given the field_name, find the object it references
+                #  and the argument it came from
+                obj, arg_used = self.get_field(field_name, args, kwargs)
+                used_args.add(arg_used)
+
+                # do any conversion on the resulting object
+                obj = self.convert_field(obj, conversion)
+
+                # expand the format spec, if needed
+                format_spec, auto_arg_index = self._vformat(
+                    format_spec,
+                    args,
+                    kwargs,
+                    used_args,
+                    recursion_depth - 1,
+                    auto_arg_index=auto_arg_index,
+                )
+
+                # defer format
+                result.append((obj, False, format_spec))
+
+        # if input is a single expression (ie. '{expr}' don't cast
+        # source to string.
+        if len(result) == 1:
+            obj, is_literal, format_spec = result[0]
+            if is_literal:
+                return obj, auto_arg_index
+            if format_spec:
+                return self.format_field(obj, format_spec), auto_arg_index
+            else:
+                return obj, auto_arg_index
+        else:
+            return (
+                ''.join(
+                    [
+                        obj if is_literal else self.format_field(obj, format_spec)
+                        for obj, is_literal, format_spec in result
+                    ]
+                ),
+                auto_arg_index,
+            )


Test patch:
diff --git a/tests/test_varfmt.py b/tests/test_varfmt.py
new file mode 100644
index 000000000..0bb9a9271
--- /dev/null
+++ b/tests/test_varfmt.py
@@ -0,0 +1,78 @@
+import pytest
+
+from c7n.varfmt import VarFormat
+from c7n.utils import parse_date, format_string_values
+
+
+def test_format_mixed():
+    assert VarFormat().format("{x} abc {Y}", x=2, Y='a') == '2 abc a'
+
+
+def test_format_pass_list():
+    assert VarFormat().format("{x}", x=[1, 2, 3]) == [1, 2, 3]
+
+
+def test_format_pass_int():
+    assert VarFormat().format("{x}", x=2) == 2
+
+
+def test_format_pass_empty():
+    assert VarFormat().format("{x}", x=[]) == []
+    assert VarFormat().format("{x}", x=None) is None
+    assert VarFormat().format("{x}", x={}) == {}
+    assert VarFormat().format("{x}", x=0) == 0
+
+
+def test_format_string_values_empty():
+    formatter = VarFormat().format
+    assert format_string_values({'a': '{x}'}, x=None, formatter=formatter) == {
+        'a': None
+    }
+    assert format_string_values({'a': '{x}'}, x={}, formatter=formatter) == {'a': {}}
+    assert format_string_values({'a': '{x}'}, x=[], formatter=formatter) == {'a': []}
+    assert format_string_values({'a': '{x}'}, x=0, formatter=formatter) == {'a': 0}
+
+
+def test_format_manual_to_auto():
+    # coverage check for stdlib impl behavior
+    with pytest.raises(ValueError) as err:
+        VarFormat().format("{0} {}", 1, 2)
+    assert str(err.value) == (
+        'cannot switch from manual field specification to automatic field numbering'
+    )
+
+
+def test_format_auto_to_manual():
+    # coverage check for stdlib impl behavior
+    with pytest.raises(ValueError) as err:
+        VarFormat().format('{} {1}', 'a', 'b')
+    assert str(err.value) == (
+        'cannot switch from manual field specification to automatic field numbering'
+    )
+
+
+def test_format_date_fmt():
+    d = parse_date("2018-02-02 12:00")
+    assert VarFormat().format("{:%Y-%m-%d}", d, "2018-02-02")
+    assert VarFormat().format("{}", d) == d
+
+
+def test_load_policy_var_retain_type(test):
+    p = test.load_policy(
+        {
+            'name': 'x',
+            'resource': 'aws.sqs',
+            'filters': [
+                {'type': 'value', 'key': 'why', 'op': 'in', 'value': "{my_list}"},
+                {'type': 'value', 'key': 'why_not', 'value': "{my_int}"},
+                {'key': "{my_date:%Y-%m-%d}"},
+            ],
+        }
+    )
+
+    p.expand_variables(
+        dict(my_list=[1, 2, 3], my_int=22, my_date=parse_date('2022-02-01 12:00'))
+    )
+    test.assertJmes('filters[0].value', p.data, [1, 2, 3])
+    test.assertJmes('filters[1].value', p.data, 22)
+    test.assertJmes('filters[2].key', p.data, "2022-02-01")