Repository: cloud-custodian/cloud-custodian

Issue:
Please add a date type parameter for the value filter
@kapilt  We are turning on tag enforcement for tons of services but we are only going to enforce on new resources going forward as another team will have to cleanup the technical debt of all the pre-existing resources. So being able to specify the date which we turn on enforcement in the policy and then any resource created after that date would have it's tags checked and enforced on, even 6 months down the road if they change their correct tags to something invalid it would be able to pick that up as the resource create date would be after the enforce date so it would be amazing to have that so I could enforce policies from X date going forward.   I currently have around 80 - 100 policies queued up that we need to deploy in a month and I would need this feature to be able to successfully implement my strategy as I can't simply add a tag to instances created after a certain point as our customers have access to modify all tags on their resources and without this date_value type parameter, customer will figure out how to get around the enforcement policies and our massive tag enforcement project will be a bust.   This is hugely important to us and I'm sure lots of other customers when they realize it is an option and what it can do to help them with enforcing on resources created before or after a certain date.


Thinking some kind of age filter that might look something like this:

```
filters:
    <check all required tag values here>
    - type: value
      value_type: date
      key: CreateDate
      op: greater-than
      value: "05/15/2019"
actions:
   <enforce tags here>


Discussion:

Resolving patch:
diff --git a/c7n/filters/core.py b/c7n/filters/core.py
--- a/c7n/filters/core.py
+++ b/c7n/filters/core.py
@@ -23,6 +23,7 @@
 import logging
 import operator
 import re
+import sys
 
 from dateutil.tz import tzutc
 from dateutil.parser import parse
@@ -110,7 +111,7 @@ def intersect(x, y):
 VALUE_TYPES = [
     'age', 'integer', 'expiration', 'normalize', 'size',
     'cidr', 'cidr_size', 'swap', 'resource_count', 'expr',
-    'unique_size']
+    'unique_size', 'date']
 
 
 class FilterRegistry(PluginRegistry):
@@ -420,7 +421,11 @@ def validate(self):
         # the value filters because it operates on the full resource list
         if self.data.get('value_type') == 'resource_count':
             return self._validate_resource_count()
-
+        elif self.data.get('value_type') == 'date':
+            if not parse_date(self.data.get('value')):
+                raise PolicyValidationError(
+                    "value_type: date with invalid date value:%s",
+                    self.data.get('value', ''))
         if 'key' not in self.data and 'key' in self.required_keys:
             raise PolicyValidationError(
                 "Missing 'key' in value filter %s" % self.data)
@@ -587,21 +592,15 @@ def process_value_type(self, sentinel, value, resource):
                 return sentinel, 0
         elif self.vtype == 'swap':
             return value, sentinel
+        elif self.vtype == 'date':
+            return parse_date(sentinel), parse_date(value)
         elif self.vtype == 'age':
             if not isinstance(sentinel, datetime.datetime):
                 sentinel = datetime.datetime.now(tz=tzutc()) - timedelta(sentinel)
-            if isinstance(value, (str, int, float)):
-                try:
-                    value = datetime.datetime.fromtimestamp(float(value)).replace(tzinfo=tzutc())
-                except ValueError:
-                    pass
-            if not isinstance(value, datetime.datetime):
-                # EMR bug when testing ages in EMR. This is due to
-                # EMR not having more functionality.
-                try:
-                    value = parse(value, default=datetime.datetime.now(tz=tzutc()))
-                except (AttributeError, TypeError, ValueError):
-                    value = 0
+            value = parse_date(value)
+            if value is None:
+                # compatiblity
+                value = 0
             # Reverse the age comparison, we want to compare the value being
             # greater than the sentinel typically. Else the syntax for age
             # comparisons is intuitively wrong.
@@ -623,13 +622,9 @@ def process_value_type(self, sentinel, value, resource):
         elif self.vtype == 'expiration':
             if not isinstance(sentinel, datetime.datetime):
                 sentinel = datetime.datetime.now(tz=tzutc()) + timedelta(sentinel)
-
-            if not isinstance(value, datetime.datetime):
-                try:
-                    value = parse(value, default=datetime.datetime.now(tz=tzutc()))
-                except (AttributeError, TypeError, ValueError):
-                    value = 0
-
+            value = parse_date(value)
+            if value is None:
+                value = 0
             return sentinel, value
         return sentinel, value
 
@@ -699,6 +694,38 @@ def process(self, resources, event=None):
         return []
 
 
+def cast_tz(d, tz):
+    if sys.version_info.major == 2:
+        return d.replace(tzinfo=tz)
+    return d.astimezone(tz)
+
+
+def parse_date(v, tz=None):
+    if v is None:
+        return v
+
+    tz = tz or tzutc()
+
+    if isinstance(v, datetime.datetime):
+        if v.tzinfo is None:
+            return cast_tz(v, tz)
+        return v
+
+    if isinstance(v, six.string_types):
+        try:
+            return cast_tz(parse(v), tz)
+        except (AttributeError, TypeError, ValueError):
+            pass
+
+    if isinstance(v, (int, float) + six.string_types):
+        try:
+            v = cast_tz(datetime.datetime.fromtimestamp(float(v)), tz)
+        except ValueError:
+            pass
+
+    return isinstance(v, datetime.datetime) and v or None
+
+
 class ValueRegex(object):
     """Allows filtering based on the output of a regex capture.
     This is useful for parsing data that has a weird format.


Test patch:
diff --git a/tests/test_filters.py b/tests/test_filters.py
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -16,6 +16,7 @@
 import calendar
 from datetime import datetime, timedelta
 from dateutil import tz
+from dateutil.parser import parse as parse_date
 import unittest
 
 from c7n.exceptions import PolicyValidationError
@@ -353,6 +354,20 @@ def i(d):
         self.assertFilter(fdata, i(calendar.timegm(now.timetuple())), True)
         self.assertFilter(fdata, i(str(calendar.timegm(now.timetuple()))), True)
 
+    def test_date(self):
+        def i(d):
+            return instance(LaunchTime=d)
+
+        fdata = {
+            'type': 'value',
+            'key': 'LaunchTime',
+            'op': 'less-than',
+            'value_type': 'date',
+            'value': '2019/05/01'}
+
+        self.assertFilter(fdata, i(parse_date('2019/04/01')), True)
+        self.assertFilter(fdata, i(datetime.now().isoformat()), False)
+
     def test_expiration(self):
 
         now = datetime.now(tz=tz.tzutc())