Skip to content

Commit

Permalink
core - don't expand {now} placeholder during provisioning (#8509)
Browse files Browse the repository at this point in the history
  • Loading branch information
ajkerrigan committed May 10, 2023
1 parent 3c2a5ac commit 4f15613
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 9 deletions.
7 changes: 6 additions & 1 deletion c7n/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1262,7 +1262,12 @@ def get_variables(self, variables=None):
'op': '{op}',
'action_date': '{action_date}',
# tag action pyformat-date handling
'now': utils.FormatDate(datetime.utcnow()),
# defer expansion until runtime for serverless modes
'now': (
utils.DeferredFormatString('now')
if isinstance(self.get_execution_mode(), ServerlessExecutionMode)
else utils.FormatDate(datetime.utcnow())
),
# account increase limit action
'service': '{service}',
# s3 set logging action :-( see if we can revisit this one.
Expand Down
11 changes: 11 additions & 0 deletions c7n/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright The Cloud Custodian Authors.
# SPDX-License-Identifier: Apache-2.0
import copy
from collections import UserString
from datetime import datetime, timedelta
from dateutil.tz import tzutc
import json
Expand Down Expand Up @@ -680,6 +681,16 @@ def get_proxy_url(url):
return None


class DeferredFormatString(UserString):
"""A string that returns itself when formatted
Let any format spec pass through. This lets us selectively defer
expansion of runtime variables without losing format spec details.
"""
def __format__(self, format_spec):
return "".join(("{", self.data, f":{format_spec}" if format_spec else "", "}"))


class FormatDate:
"""a datetime wrapper with extended pyformat syntax"""

Expand Down
3 changes: 2 additions & 1 deletion c7n/varfmt.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from string import Formatter
from c7n.utils import DeferredFormatString


class VarFormat(Formatter):
Expand Down Expand Up @@ -82,7 +83,7 @@ def _vformat(
obj, is_literal, format_spec = result[0]
if is_literal:
return obj, auto_arg_index
if format_spec:
if format_spec or isinstance(obj, DeferredFormatString):
return self.format_field(obj, format_spec), auto_arg_index
else:
return obj, auto_arg_index
Expand Down
15 changes: 15 additions & 0 deletions tests/test_mu.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,21 @@ def test_published_lambda_architecture(self):
result = mgr.publish(pl)
self.assertEqual(result["Architectures"], ["arm64"])

def test_deferred_interpolation(self):
p = self.load_policy({
'name': 'ec2-foo-bar',
'resource': 'aws.ec2',
'mode': {
'type': 'cloudtrail',
'role': 'arn:aws:iam::644160558196:role/custodian-mu',
'events': ['RunInstances']},
'actions': [{
'type': 'tag', 'key': 'LastMatch', 'value': '{now}'
}]})
p.expand_variables(p.get_variables())
pl = PolicyLambda(p)
pl.get_archive()

def test_updated_lambda_architecture(self):
session_factory = self.replay_flight_data("test_updated_lambda_architecture")
lambda_client = session_factory().client("lambda")
Expand Down
63 changes: 56 additions & 7 deletions tests/test_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1070,8 +1070,6 @@ def test_policy_variable_interpolation(self):
'member-role': 'arn:aws:iam::{account_id}:role/BarFoo',
'role': 'FooBar'},
'actions': [
{'type': 'tag',
'value': 'bad monkey {account_id} {region} {now:+2d%Y-%m-%d}'},
{'type': 'notify',
'to': ['me@example.com'],
'transport': {
Expand All @@ -1081,16 +1079,67 @@ def test_policy_variable_interpolation(self):
'subject': "S3 - Cross-Account -[custodian {{ account }} - {{ region }}]"},
]}, config={'account_id': '12312311', 'region': 'zanzibar'})

ivalue = 'bad monkey 12312311 zanzibar %s' % (
(datetime.utcnow() + timedelta(2)).strftime('%Y-%m-%d'))
p.expand_variables(p.get_variables())
self.assertEqual(p.data['actions'][0]['value'], ivalue)
self.assertEqual(
p.data['actions'][1]['subject'],
p.data['actions'][0]['subject'],
"S3 - Cross-Account -[custodian {{ account }} - {{ region }}]")
self.assertEqual(p.data['mode']['role'], 'arn:aws:iam::12312311:role/FooBar')
self.assertEqual(p.data['mode']['member-role'], 'arn:aws:iam::{account_id}:role/BarFoo')
self.assertEqual(p.resource_manager.actions[0].data['value'], ivalue)

def test_now_interpolation(self):
"""Test interpolation of the {now} placeholder
- Only interpolate the value at runtime, not during provisioning
- When deferring interpolation, pass through custom format specifiers
"""

pull_mode_policy = self.load_policy({
'name': 'compute',
'resource': 'aws.ec2',
'actions': [
{'type': 'tag',
'value': 'bad monkey {account_id} {region} {now:+2d%Y-%m-%d}'},
{'type': 'tag',
'key': 'escaped_braces',
'value': '{{now}}'},
]}, config={'account_id': '12312311', 'region': 'zanzibar'})
lambda_mode_policy = self.load_policy({
**pull_mode_policy.data,
**{
'mode': {
'type': 'config-rule',
'role': 'FooBar'},
}
}, config=pull_mode_policy.ctx.options)

provision_time_value = 'bad monkey 12312311 zanzibar {now:+2d%Y-%m-%d}'
run_time_value = 'bad monkey 12312311 zanzibar %s' % (
(datetime.utcnow() + timedelta(2)).strftime('%Y-%m-%d'))

pull_mode_policy.expand_variables(pull_mode_policy.get_variables())
self.assertEqual(
pull_mode_policy.data['actions'][0]['value'],
run_time_value
)
self.assertEqual(
pull_mode_policy.resource_manager.actions[0].data['value'],
run_time_value
)

lambda_mode_policy.expand_variables(lambda_mode_policy.get_variables())
self.assertEqual(
lambda_mode_policy.data['actions'][0]['value'],
provision_time_value
)
self.assertEqual(
lambda_mode_policy.resource_manager.actions[0].data['value'],
provision_time_value
)
# Validate historical use of {{now}} to defer interpolation
self.assertEqual(
lambda_mode_policy.resource_manager.actions[1].data['value'],
'{now}'
)

def test_child_resource_trail_validation(self):
self.assertRaises(
Expand Down

0 comments on commit 4f15613

Please sign in to comment.