Repository: cloud-custodian/cloud-custodian

Issue:
`AttributeError: __enter__` from SqlKvCache when using `value_from`
### Describe the bug

I was testing a policy using a `value` filter in combination with `value_from` to pull data from S3. It bombed trying to load data from the cache.

### What did you expect to happen?

I expected data to load from the cache, if available.

### Cloud Provider

Amazon Web Services (AWS)

### Cloud Custodian version and dependency information

```shell
Running from a dev install using https://github.com/cloud-custodian/cloud-custodian/commit/dfe038ad96e30e67b59a158e4d3b9251fdf1131f
```


### Policy

```shell
policies:
  - name: sqs-test
    resource: aws.sqs
    filters:
      - type: value
        key: "tag:Owner"
        op: in
        value_from:
            url: s3://my-c7n-bucket/valid_tags.json
            format: json
            expr: Owner[]
```


### Relevant log/traceback output

```shell
2022-08-23 16:24:29,937: custodian.commands:DEBUG Loaded file /home/aj/code/sandbox-policies/sqs-test.yml. Contains 1 policies
2022-08-23 16:24:29,978: custodian.aws:DEBUG using default region:us-east-2 from boto
2022-08-23 16:24:30,317: custodian.output:DEBUG Storing output with <LogFile file:///home/aj/c7n-out/sqs-test/custodian-run.log>
2022-08-23 16:24:30,355: custodian.policy:DEBUG Running policy:sqs-test resource:aws.sqs region:us-east-2 c7n:0.9.18
2022-08-23 16:24:30,356: custodian.cache:DEBUG expiring stale cache entries
2022-08-23 16:24:30,358: custodian.resources.sqs:DEBUG Using cached c7n.resources.sqs.SQS: 48
2022-08-23 16:24:30,366: custodian.output:DEBUG metric:PolicyException Count:1 policy:sqs-test restype:aws.sqs
2022-08-23 16:24:30,366: custodian.output:DEBUG metric:ApiCalls Count:0 policy:sqs-test restype:aws.sqs
2022-08-23 16:24:30,367: custodian.output:ERROR Error while executing policy
Traceback (most recent call last):
  File "/home/aj/code/cloud-custodian/c7n/policy.py", line 294, in run
    resources = self.policy.resource_manager.resources()
  File "/home/aj/code/cloud-custodian/c7n/query.py", line 535, in resources
    resources = self.filter_resources(resources)
  File "/home/aj/code/cloud-custodian/c7n/manager.py", line 111, in filter_resources
    resources = f.process(resources, event)
  File "/home/aj/code/cloud-custodian/c7n/filters/core.py", line 553, in process
    return super(ValueFilter, self).process(resources, event)
  File "/home/aj/code/cloud-custodian/c7n/filters/core.py", line 190, in process
    return list(filter(self, resources))
  File "/home/aj/code/cloud-custodian/c7n/filters/core.py", line 540, in __call__
    matched = self.match(i)
  File "/home/aj/code/cloud-custodian/c7n/filters/core.py", line 566, in match
    self.v = values.get_values()
  File "/home/aj/code/cloud-custodian/c7n/resolver.py", line 149, in get_values
    contents = self.cache.get(("value-from", key))
  File "/home/aj/code/cloud-custodian/c7n/cache.py", line 139, in get
    with self.conn as cursor:
AttributeError: __enter__
2022-08-23 16:24:30,368: custodian.commands:ERROR Error while executing policy sqs-test, continuing
Traceback (most recent call last):
  File "/home/aj/code/cloud-custodian/c7n/commands.py", line 302, in run
    policy()
  File "/home/aj/code/cloud-custodian/c7n/policy.py", line 1281, in __call__
    resources = PullMode(self).run()
  File "/home/aj/code/cloud-custodian/c7n/policy.py", line 294, in run
    resources = self.policy.resource_manager.resources()
  File "/home/aj/code/cloud-custodian/c7n/query.py", line 535, in resources
    resources = self.filter_resources(resources)
  File "/home/aj/code/cloud-custodian/c7n/manager.py", line 111, in filter_resources
    resources = f.process(resources, event)
  File "/home/aj/code/cloud-custodian/c7n/filters/core.py", line 553, in process
    return super(ValueFilter, self).process(resources, event)
  File "/home/aj/code/cloud-custodian/c7n/filters/core.py", line 190, in process
    return list(filter(self, resources))
  File "/home/aj/code/cloud-custodian/c7n/filters/core.py", line 540, in __call__
    matched = self.match(i)
  File "/home/aj/code/cloud-custodian/c7n/filters/core.py", line 566, in match
    self.v = values.get_values()
  File "/home/aj/code/cloud-custodian/c7n/resolver.py", line 149, in get_values
    contents = self.cache.get(("value-from", key))
  File "/home/aj/code/cloud-custodian/c7n/cache.py", line 139, in get
    with self.conn as cursor:
AttributeError: __enter__
```


### Extra information or context

Looks like the issue is that when evaluating `ValuesFrom`, there's a check [here](https://github.com/cloud-custodian/cloud-custodian/blob/dfe038ad96e30e67b59a158e4d3b9251fdf1131f/c7n/resolver.py#L144) that checks to see if we have a cache. But it does _not_ trigger a `load()` like we do in cases like [this](https://github.com/cloud-custodian/cloud-custodian/blob/dfe038ad96e30e67b59a158e4d3b9251fdf1131f/c7n/query.py#L512).

So each time we initialize a cache [here](https://github.com/cloud-custodian/cloud-custodian/blob/dfe038ad96e30e67b59a158e4d3b9251fdf1131f/c7n/cache.py#L35), that cache has a null `self.conn` until we call `load()`.

I _think_ we'll either need to make sure a connection is available during `__init__()` (accounting for any flat file migrations first) or explicitly call `load()` in more places. (This issue doesn't appear limited to `value_from`)


Discussion:
doh. I forgot the cache usage in value from.

Resolving patch:
diff --git a/c7n/cache.py b/c7n/cache.py
--- a/c7n/cache.py
+++ b/c7n/cache.py
@@ -115,7 +115,7 @@ def __init__(self, config):
         self.cache_path = resolve_path(config.cache)
         self.conn = None
 
-    def load(self):
+    def init(self):
         # migration from pickle cache file
         if os.path.exists(self.cache_path):
             with open(self.cache_path, 'rb') as fh:
@@ -133,6 +133,10 @@ def load(self):
             cursor.execute(
                 'delete from c7n_cache where create_date < ?',
                 [datetime.utcnow() - timedelta(minutes=self.cache_period)])
+
+    def load(self):
+        if not self.conn:
+            self.init()
         return True
 
     def get(self, key):
diff --git a/c7n/resolver.py b/c7n/resolver.py
--- a/c7n/resolver.py
+++ b/c7n/resolver.py
@@ -26,9 +26,10 @@ def __init__(self, session_factory, cache):
         self.cache = cache
 
     def resolve(self, uri):
-        if self.cache:
+        if self.cache and self.cache.load():
             contents = self.cache.get(("uri-resolver", uri))
             if contents is not None:
+                self.cache.close()
                 return contents
 
         if uri.startswith('s3://'):
@@ -40,6 +41,7 @@ def resolve(self, uri):
 
         if self.cache:
             self.cache.save(("uri-resolver", uri), contents)
+            self.cache.close()
         return contents
 
     def handle_response_encoding(self, response):
@@ -141,11 +143,11 @@ def get_contents(self):
         return contents, format
 
     def get_values(self):
-        if self.cache:
+        key = [self.data.get(i) for i in ('url', 'format', 'expr')]
+        if self.cache and self.cache.load():
             # use these values as a key to cache the result so if we have
             # the same filter happening across many resources, we can reuse
             # the results.
-            key = [self.data.get(i) for i in ('url', 'format', 'expr')]
             contents = self.cache.get(("value-from", key))
             if contents is not None:
                 return contents
@@ -153,6 +155,7 @@ def get_values(self):
         contents = self._get_values()
         if self.cache:
             self.cache.save(("value-from", key), contents)
+            self.cache.close()
         return contents
 
     def _get_values(self):


Test patch:
diff --git a/tests/test_resolver.py b/tests/test_resolver.py
--- a/tests/test_resolver.py
+++ b/tests/test_resolver.py
@@ -11,6 +11,7 @@
 from .common import BaseTest, ACCOUNT_ID, Bag
 from .test_s3 import destroyBucket
 
+from c7n.cache import SqlKvCache
 from c7n.config import Config
 from c7n.resolver import ValuesFrom, URIResolver
 
@@ -30,6 +31,12 @@ def save(self, key, data):
         self.saves += 1
         self.state[pickle.dumps(key)] = data
 
+    def load(self):
+        return True
+
+    def close(self):
+        pass
+
 
 class FakeResolver:
 
@@ -90,6 +97,18 @@ def test_resolve_file(self):
             self.assertEqual(resolver.resolve("file:%s" % fh.name), content)
 
 
+def test_value_from_sqlkv(tmp_path):
+
+    kv = SqlKvCache(Bag(cache=tmp_path / "cache.db", cache_period=60))
+    config = Config.empty(account_id=ACCOUNT_ID)
+    mgr = Bag({"session_factory": None, "_cache": kv, "config": config})
+    values = ValuesFrom(
+        {"url": "moon", "expr": "[].bean", "format": "json"}, mgr)
+    values.resolver = FakeResolver(json.dumps([{"bean": "magic"}]))
+    assert values.get_values() == {"magic"}
+    assert values.get_values() == {"magic"}
+
+
 class UrlValueTest(BaseTest):
 
     def setUp(self):