Repository: anchore/anchore-cli

Issue:
Deleting an image reports success even if the image can't be deleted
Starting anchore-engine v0.8.0 image deletion is an async op and the response has changed to reflect this. The API always responds with 200 HTTP status unless the service is unable to respond. Status and details of the delete op are in the body of the response. anchore-cli does not currently parse the body, it simply keys off of the HTTP status to indicate whether the deletion attempt was successful

Discussion:

Patch:
diff --git a/anchorecli/cli/image.py b/anchorecli/cli/image.py
index dbc1892..61c21f7 100644
--- a/anchorecli/cli/image.py
+++ b/anchorecli/cli/image.py
@@ -375,6 +375,10 @@ def delete(input_image, force, all):
                 if image['imageDigest']:
                     ret = anchorecli.clients.apiexternal.delete_image(config, imageDigest=image['imageDigest'], force=force)
                     if ret['success']:
+                        payload = ret.get('payload')
+                        if payload and payload.get('status') != 'deleting':
+                            raise Exception(payload.get('details'))
+
                         for image_detail in image['image_detail']:
                             fulltag = image_detail.pop('registry', "None") + "/" + image_detail.pop('repo', "None") + ":" + image_detail.pop('tag', "None")
                             print(fulltag)
@@ -401,7 +405,12 @@ def delete(input_image, force, all):
 
             if ret:
                 if ret['success']:
-                    print(anchorecli.cli.utils.format_output(config, 'image_delete', {}, ret['payload']))
+                    payload = ret.get('payload')
+                    if payload and payload.get('status') != 'deleting':
+                        ecode = 1  # backwards compatibility with pre v0.8.0 error
+                        raise Exception(payload.get('details', 'cannot delete image'))
+                    else:
+                        print(anchorecli.cli.utils.format_output(config, 'image_delete', {}, ret['payload']))
                 else:
                     raise Exception(json.dumps(ret['error'], indent=4))
             else:


Test patch:
diff --git a/tests/unit/cli/test_image.py b/tests/unit/cli/test_image.py
index 7466974..00fc7e5 100644
--- a/tests/unit/cli/test_image.py
+++ b/tests/unit/cli/test_image.py
@@ -118,3 +118,70 @@ class TestQueryVuln:
         result = runner.invoke(image.query_vuln, ['centos/centos:8', 'all'])
         assert result.exit_code == 0
         assert item in result.stdout
+
+
+class TestDeleteImage:
+    def test_deleted_pre_v080(self, monkeypatch, response):
+        monkeypatch.setattr(image.anchorecli.cli.utils, 'discover_inputimage', lambda *a, **kw: (None, None, '<digest>'))
+        monkeypatch.setattr(image, 'config', {'jsonmode': False})
+        monkeypatch.setattr(image.anchorecli.clients.apiexternal, 'delete_image', lambda *a, **kw: {
+            'success': True,
+            'httpcode': 200,
+            'payload': {},
+            'error': {}
+        })
+        runner = CliRunner()
+        response(success=True)
+        result = runner.invoke(image.delete, ['centos/centos:8'])
+        assert result.exit_code == 0
+
+    def test_delete_failed_pre_v080(self, monkeypatch, response):
+        monkeypatch.setattr(image.anchorecli.cli.utils, 'discover_inputimage', lambda *a, **kw: (None, None, '<digest>'))
+        monkeypatch.setattr(image, 'config', {'jsonmode': False})
+        monkeypatch.setattr(image.anchorecli.clients.apiexternal, 'delete_image', lambda *a, **kw: {
+            'success': False,
+            'httpcode': 409,
+            'payload': {},
+            'error': 'cannot delete image'
+        })
+        runner = CliRunner()
+        response(success=True)
+        result = runner.invoke(image.delete, ['centos/centos:8'])
+        assert result.exit_code == 1
+
+    def test_is_deleting(self, monkeypatch, response):
+        monkeypatch.setattr(image.anchorecli.cli.utils, 'discover_inputimage', lambda *a, **kw: (None, None, '<digest>'))
+        monkeypatch.setattr(image, 'config', {'jsonmode': False})
+        monkeypatch.setattr(image.anchorecli.clients.apiexternal, 'delete_image', lambda *a, **kw: {
+            'success': True,
+            'httpcode': 200,
+            'payload': {
+                'detail': None,
+                'digest': '<digest>',
+                'status': 'deleting'
+            },
+            'error': {}
+        })
+        runner = CliRunner()
+        response(success=True)
+        result = runner.invoke(image.delete, ['centos/centos:8'])
+        assert result.exit_code == 0
+
+    def test_delete_failed(self, monkeypatch, response):
+        monkeypatch.setattr(image.anchorecli.cli.utils, 'discover_inputimage', lambda *a, **kw: (None, None, '<digest>'))
+        monkeypatch.setattr(image, 'config', {'jsonmode': False})
+        monkeypatch.setattr(image.anchorecli.clients.apiexternal, 'delete_image', lambda *a, **kw: {
+            'success': True,
+            'httpcode': 200,
+            'payload': {
+                'detail': 'cannot delete image',
+                'digest': '<digest>',
+                'status': 'delete_failed'
+            },
+            'error': {}
+        })
+        runner = CliRunner()
+        response(success=True)
+        result = runner.invoke(image.delete, ['centos/centos:8'])
+        assert result.exit_code == 1
+