Repository: typesafehub/conductr-cli

Issue:
Conduct load command may resolve directory incorrectly
Conduct load command may fail unexpectedly with the following message:

```
Felixs-MBP-2:conductr felixsatyaputra$ sandbox run 0.1.0 -n 3:3
|------------------------------------------------|
| Starting ConductR                              |
|------------------------------------------------|
Extracting ConductR core to /Users/felixsatyaputra/.conductr/images/core
Extracting ConductR agent to /Users/felixsatyaputra/.conductr/images/agent
Starting ConductR core instance on 192.168.10.1..
Starting ConductR core instance on 192.168.10.2..
Starting ConductR core instance on 192.168.10.3..
Waiting for ConductR to start...
Starting ConductR agent instance on 192.168.10.1..
Starting ConductR agent instance on 192.168.10.2..
Starting ConductR agent instance on 192.168.10.3..
|------------------------------------------------|
| Starting OCI-in-Docker support                 |
|------------------------------------------------|
OCI-in-Docker provided by image lightbend-docker-registry.bintray.io/conductr/oci-in-docker:0.1.0
|------------------------------------------------|
| Starting HAProxy                               |
|------------------------------------------------|
Exposing the following ports [80, 443, 3000, 5601, 8999, 9000, 9200, 9999]
0c3815403fe7b847356f507ac368614536b40e0e2e7b295c7d6be98f61c4575d
Deploying bundle conductr-haproxy with configuration conductr-haproxy-dev-mode
Retrieving bundle..
Error: bndl: Unable to detect format. Provide a -f or --format argument
```

The error message above is caused by trying to load a bundle called `conductr-haproxy` while there's a local directory called `conductr-haproxy` relative to where `sandbox run` is being called. (Actually in this instance, I'm running the `sandbox run` command from the root of the ConductR project where `conductr-haproxy` directory exists).

The error message above will only appear if the `~/.conductr/cache` is empty.

The above error is caused by the `bndl` command trying to pack `conductr-haproxy` directory.

The possible fix for the above scenario is to tighten the URI resolver around directory detection, i.e. only return directory if it's considered a valid directory by `bndl_utils.detect_format_dir()`



Comments:

Patch:
diff --git a/conductr_cli/resolvers/bintray_resolver.py b/conductr_cli/resolvers/bintray_resolver.py
index 6418e4a..b6fc839 100644
--- a/conductr_cli/resolvers/bintray_resolver.py
+++ b/conductr_cli/resolvers/bintray_resolver.py
@@ -1,9 +1,9 @@
 from conductr_cli.exceptions import MalformedBundleUriError, BintrayResolutionError, \
     BintrayCredentialsNotFoundError, MalformedBintrayCredentialsError
 from conductr_cli.resolvers import uri_resolver
+from conductr_cli.resolvers.resolvers_util import is_local_file
 from conductr_cli import bundle_shorthand
 from requests.exceptions import HTTPError, ConnectionError
-from urllib.parse import urlparse
 import json
 import logging
 import os
@@ -142,24 +142,6 @@ def continuous_delivery_uri(resolved_version):
         return None
 
 
-def any_subdir_contains(dir, name):
-    for dir, sub_dirs, files in os.walk(dir):
-        if name in files:
-            return True
-
-    return False
-
-
-def is_local_file(uri, require_bundle_conf):
-    parsed = urlparse(uri, scheme='file')
-
-    return parsed.scheme == 'file' and os.path.exists(parsed.path) and (
-        not require_bundle_conf or os.path.isfile(parsed.path) or (
-            os.path.isdir(parsed.path) and any_subdir_contains(parsed.path, 'bundle.conf')
-        )
-    )
-
-
 def bintray_download_artefact(cache_dir, artefact, auth):
     if artefact:
         return uri_resolver.resolve_file(cache_dir, artefact['download_url'], auth)
diff --git a/conductr_cli/resolvers/docker_resolver.py b/conductr_cli/resolvers/docker_resolver.py
index b8e4308..688c6ff 100644
--- a/conductr_cli/resolvers/docker_resolver.py
+++ b/conductr_cli/resolvers/docker_resolver.py
@@ -1,9 +1,10 @@
 from collections import OrderedDict
 from conductr_cli import screen_utils
 from conductr_cli.constants import IO_CHUNK_SIZE
+from conductr_cli.resolvers.resolvers_util import is_local_file
 from functools import partial
 from requests.auth import HTTPBasicAuth
-from urllib.parse import urlencode, urlparse
+from urllib.parse import urlencode
 import gzip
 import hashlib
 import logging
@@ -169,7 +170,7 @@ def resolve_bundle(cache_dir, uri, auth=None):
 def do_resolve_bundle(cache_dir, uri, auth, offline_mode):
     log = logging.getLogger(__name__)
 
-    if is_local_file(uri):
+    if is_local_file(uri, require_bundle_conf=True):
         return False, None, None
 
     (provided_url, url), (provided_ns, ns), (provided_image, image), (provided_tag, tag) = parse_uri(uri)
@@ -278,11 +279,6 @@ def is_bundle_name(uri):
     return uri.count('/') == 0 and uri.count('.') == 0
 
 
-def is_local_file(uri):
-    parsed = urlparse(uri, scheme='file')
-    return parsed.scheme == 'file' and os.path.exists(parsed.path)
-
-
 def load_docker_credentials(server):
     log = logging.getLogger(__name__)
 
diff --git a/conductr_cli/resolvers/resolvers_util.py b/conductr_cli/resolvers/resolvers_util.py
new file mode 100644
index 0000000..21aed5e
--- /dev/null
+++ b/conductr_cli/resolvers/resolvers_util.py
@@ -0,0 +1,15 @@
+from conductr_cli.bndl_utils import detect_format_dir
+from urllib.parse import urlparse
+import os
+
+
+def is_local_file(uri, require_bundle_conf):
+    parsed = urlparse(uri, scheme='file')
+
+    return parsed.scheme == 'file' and os.path.exists(parsed.path) and (
+        os.path.isfile(parsed.path) or (
+            os.path.isdir(parsed.path) and
+            detect_format_dir(parsed.path) is not None and
+            (not require_bundle_conf or os.path.exists(os.path.join(parsed.path, 'bundle.conf')))
+        )
+    )


Test patch:
diff --git a/conductr_cli/resolvers/test/test_resolvers_util.py b/conductr_cli/resolvers/test/test_resolvers_util.py
new file mode 100644
index 0000000..b72c0a1
--- /dev/null
+++ b/conductr_cli/resolvers/test/test_resolvers_util.py
@@ -0,0 +1,41 @@
+from unittest import TestCase
+from conductr_cli.resolvers import resolvers_util
+import os
+import shutil
+import tempfile
+
+
+class TestResolveBundle(TestCase):
+    def test_is_local_file_with_file(self):
+        with tempfile.NamedTemporaryFile() as temp:
+            self.assertTrue(resolvers_util.is_local_file(temp.name, require_bundle_conf=True))
+            self.assertTrue(resolvers_util.is_local_file(temp.name, require_bundle_conf=False))
+
+    def test_is_local_file_with_empty_dir(self):
+        temp = tempfile.mkdtemp()
+
+        try:
+            self.assertFalse(resolvers_util.is_local_file(temp, require_bundle_conf=True))
+            self.assertFalse(resolvers_util.is_local_file(temp, require_bundle_conf=False))
+        finally:
+            shutil.rmtree(temp)
+
+    def test_is_local_file_with_bundle_dir(self):
+        temp = tempfile.mkdtemp()
+        open(os.path.join(temp, 'bundle.conf'), 'w').close()
+
+        try:
+            self.assertTrue(resolvers_util.is_local_file(temp, require_bundle_conf=True))
+            self.assertTrue(resolvers_util.is_local_file(temp, require_bundle_conf=False))
+        finally:
+            shutil.rmtree(temp)
+
+    def test_is_local_file_with_bundle_conf_dir(self):
+        temp = tempfile.mkdtemp()
+        open(os.path.join(temp, 'runtime-config.sh'), 'w').close()
+
+        try:
+            self.assertFalse(resolvers_util.is_local_file(temp, require_bundle_conf=True))
+            self.assertTrue(resolvers_util.is_local_file(temp, require_bundle_conf=False))
+        finally:
+            shutil.rmtree(temp)