Repository: anchore/anchore-cli

Issue:
"anchore-cli system wait" endlessly waiting for disabled feeds to sync
We currently have the problem, that our scan pipeline is hanging, because it is stuck on waiting:

    $ anchore-cli system wait      
    Starting checks to wait for anchore-engine to be available timeout=-1.0 interval=5.0
    API availability: Checking anchore-engine URL (http://anchore-engine-anchore-engine- 
    api.anchore.svc.cluster.local:8228/v1/)...
    API availability: Success.
    Service availability: Checking for service set (catalog,apiext,policy_engine,simplequeue,analyzer)...
    Service availability: Success.
    Feed sync: Checking sync completion for feed set (vulnerabilities)...
    Feed sync: Checking sync completion for feed set (vulnerabilities)...
    Feed sync: Checking sync completion for feed set (vulnerabilities)...
    [and so on ...]

Running with debug ouput shows:

    ...
    DEBUG:anchorecli.cli.system:response shows group name=ubuntu:14.10 was last_sync=2020-12- 15T14:00:02.497251
    DEBUG:anchorecli.cli.system:response shows group name=centos:5 was last_sync=None
    DEBUG:anchorecli.cli.system:response shows feed name=nvdv2 was last_full_sync=2020-12-15T14:00:52.906874
    DEBUG:anchorecli.cli.system:response shows feed name=github was last_full_sync=2020-12-15T14:00:56.900292
    DEBUG:anchorecli.cli.system:some feeds not yet synced {'vulnerabilities': False}

The centos feeds were disabled:

    $ anchore-cli system feeds list  
    Feed                   Group                     LastSync                          RecordCount    
    ...
    vulnerabilities        amzn:2                    2020-12-15T14:00:18.990919        510                
    vulnerabilities        centos:5(disabled)        -                                 0                  
    vulnerabilities        centos:6(disabled)        -                                 0                  
    vulnerabilities        centos:7(disabled)        -                                 0                  
    vulnerabilities        centos:8(disabled)        -                                 0                  
    vulnerabilities        debian:10                 2020-12-15T14:00:14.715920        24048              
    vulnerabilities        debian:11                 2020-12-15T14:00:21.483493        21322 
    ...

I suspect that this corner case was not thought of in https://github.com/anchore/anchore-cli/pull/116

After enabling the feeds and triggering a sync, "anchore-cli system wait" finishes quickly.

Discussion:

Patch:
diff --git a/anchorecli/cli/system.py b/anchorecli/cli/system.py
index 48a9c44..8ba1011 100644
--- a/anchorecli/cli/system.py
+++ b/anchorecli/cli/system.py
@@ -11,6 +11,10 @@ config = {}
 _logger = logging.getLogger(__name__)
 
 
+class WaitOnDisabledFeedError(Exception):
+    pass
+
+
 @click.group(name="system", short_help="System operations")
 @click.pass_context
 @click.pass_obj
@@ -234,6 +238,13 @@ def wait(timeout, interval, feedsready, servicesready):
                                 feed_record.get("last_full_sync"),
                             )
                             if feed_record.get("name", None) in all_up:
+                                if not feed_record.get("enabled"):
+                                    raise WaitOnDisabledFeedError(
+                                        "Requesting wait for disabled feed: {}".format(
+                                            feed_record.get("name")
+                                        )
+                                    )
+
                                 if feed_record.get("last_full_sync", None):
                                     all_groups_synced = False
                                     for group_record in feed_record.get("groups", []):
@@ -242,7 +253,9 @@ def wait(timeout, interval, feedsready, servicesready):
                                             group_record.get("name", None),
                                             group_record.get("last_sync", None),
                                         )
-                                        if group_record.get("last_sync", None):
+                                        if group_record.get(
+                                            "last_sync", None
+                                        ) or not group_record.get("enabled", None):
                                             all_groups_synced = True
                                         else:
                                             all_groups_synced = False
@@ -255,6 +268,8 @@ def wait(timeout, interval, feedsready, servicesready):
                             break
                         else:
                             _logger.debug("some feeds not yet synced %s", all_up)
+                except WaitOnDisabledFeedError as err:
+                    raise err
                 except Exception as err:
                     print("service feeds list failed {}".format(err))
                 time.sleep(interval)


Test patch:
diff --git a/tests/unit/cli/test_system.py b/tests/unit/cli/test_system.py
index 01d1193..fb663e0 100644
--- a/tests/unit/cli/test_system.py
+++ b/tests/unit/cli/test_system.py
@@ -1,1 +1,126 @@
+import pytest
 from anchorecli.cli import system
+from click.testing import CliRunner
+import anchorecli.cli.utils
+import anchorecli.clients.apiexternal
+
+# Patches all attributes needed for just testing the feeds wait
+@pytest.fixture
+def patch_for_feeds_wait(monkeypatch):
+    monkeypatch.setattr(
+        system, "config", {"url": "http://localhost:8228", "jsonmode": False}
+    )
+    monkeypatch.setattr(anchorecli.cli.utils, "check_access", lambda x: {})
+    monkeypatch.setattr(
+        anchorecli.clients.apiexternal,
+        "system_status",
+        lambda x: {"success": True, "httpcode": 200},
+    )
+
+
+# feed response generator that can conditionally add group members in order to parameterize tests
+@pytest.fixture
+def make_feed_response():
+    def _make_feed_response(additional_vuln_group_records=[], vuln_enabled=True):
+        vuln_groups = [
+            {
+                "created_at": "2020-03-27T22:48:57Z",
+                "enabled": True,
+                "last_sync": "2020-12-23T19:34:06Z",
+                "name": "alpine:3.7",
+                "record_count": 1412,
+                "updated_at": "2020-12-23T19:34:07Z",
+            },
+            {
+                "created_at": "2020-03-27T22:48:57Z",
+                "enabled": True,
+                "last_sync": "2020-12-23T19:33:29Z",
+                "name": "centos:5",
+                "record_count": 1347,
+                "updated_at": "2020-12-23T19:34:06Z",
+            },
+        ]
+
+        vuln_groups += additional_vuln_group_records
+
+        response = {
+            "success": True,
+            "httpcode": 200,
+            "payload": [
+                {
+                    "name": "vulnerabilities",
+                    "enabled": vuln_enabled,
+                    "last_full_sync": "2020-12-23T19:34:29Z",
+                    "updated_at": "2020-12-23T19:34:29Z",
+                    "groups": vuln_groups,
+                }
+            ],
+        }
+
+        return response
+
+    return _make_feed_response
+
+
+feed_wait_tests = [
+    {"additional_vuln_group_records": [], "expect_success": True},
+    {
+        "additional_vuln_group_records": [
+            {"name": "centos:7", "enabled": True, "last_sync": None, "record_count": 0}
+        ],
+        "expect_success": False,
+    },
+    {
+        "additional_vuln_group_records": [
+            {"name": "centos:7", "enabled": False, "last_sync": None, "record_count": 0}
+        ],
+        "expect_success": True,
+    },
+]
+
+
+@pytest.mark.parametrize("test_context", feed_wait_tests)
+def test_wait_for_group(
+    monkeypatch, make_feed_response, test_context, patch_for_feeds_wait
+):
+    monkeypatch.setattr(
+        anchorecli.clients.apiexternal,
+        "system_feeds_list",
+        lambda x: make_feed_response(
+            additional_vuln_group_records=test_context["additional_vuln_group_records"]
+        ),
+    )
+
+    runner = CliRunner()
+    result = runner.invoke(system.wait, ["--servicesready", "", "--timeout", "5"])
+
+    if test_context["expect_success"]:
+        assert result.exit_code == 0
+        assert "Feed sync: Success." in result.output
+    else:
+        assert result.exit_code == 2
+        assert "Error: timed out" in result.output
+
+
+def test_wait_for_disabled_feed(monkeypatch, make_feed_response, patch_for_feeds_wait):
+    monkeypatch.setattr(
+        anchorecli.clients.apiexternal,
+        "system_feeds_list",
+        lambda x: make_feed_response(vuln_enabled=False),
+    )
+    runner = CliRunner()
+    result = runner.invoke(system.wait, ["--servicesready", "", "--timeout", "5"])
+    assert result.exit_code == 2
+    assert "Error: Requesting wait for disabled feed: vulnerabilities" in result.output
+
+
+def test_wait_for_enabled_feed(monkeypatch, make_feed_response, patch_for_feeds_wait):
+    monkeypatch.setattr(
+        anchorecli.clients.apiexternal,
+        "system_feeds_list",
+        lambda x: make_feed_response(),
+    )
+    runner = CliRunner()
+    result = runner.invoke(system.wait, ["--servicesready", "", "--timeout", "5"])
+    assert result.exit_code == 0
+    assert "Feed sync: Success" in result.output