Repository: docker/docker-py

Issue:
Invalid symlinks now break utils.create_archive(), previously were skipped, skipped by docker CLI
Version 2.7.0 seems to die if you try to include a symlink that points to something that doesn't exist.  This problem seems to have been introduced in this commit:
 https://github.com/docker/docker-py/commit/5c5705045be72530091a51372ae920f958192bfb

Example from 2.6.1 and `docker` cli tool:
```
$ mkdir test
$ cd test
$ ln -s /doesnt_exist doesnt_exist
$ cat << EOF > Dockerfile 
FROM scratch

COPY . /tmp
EOF
$ docker build -t test . 
Sending build context to Docker daemon   2.56kB
Step 1/2 : FROM scratch
 ---> 
Step 2/2 : COPY . /tmp
 ---> cb6f16dc4b1f
Removing intermediate container 4eda4dd7d7c4
Successfully built cb6f16dc4b1f
Successfully tagged test:latest
$ python
Python 2.7.6 (default, Nov 23 2017, 15:49:48) 
[GCC 4.8.4] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import docker
>>> docker.__version__
'2.6.1'
>>> docker.utils.create_archive(".", ['doesnt_exist'])
<open file '<fdopen>', mode 'w+b' at 0x7fa47208f8a0>
>>> 
```

But as of 2.7.0:
```
$ python
Python 2.7.6 (default, Nov 23 2017, 15:49:48) 
[GCC 4.8.4] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import docker
>>> docker.__version__
'2.7.0'
>>> docker.utils.create_archive(".", ['doesnt_exist'])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/docker/utils/utils.py", line 103, in create_archive
    'Can not access file in context: {}'.format(full_path)
IOError: Can not access file in context: ./doesnt_exist
>>> 
```

It seems that symlinks should be skipped rather than trying to checked to see if they are accessible, as was the previous behavior and the `docker` CLI tool behavior.


Discussion:
I have this same problem, for the moment I will have to pull that version

Resolving patch:
diff --git a/docker/utils/utils.py b/docker/utils/utils.py
--- a/docker/utils/utils.py
+++ b/docker/utils/utils.py
@@ -98,7 +98,8 @@ def create_archive(root, files=None, fileobj=None, gzip=False):
         files = build_file_list(root)
     for path in files:
         full_path = os.path.join(root, path)
-        if not os.access(full_path, os.R_OK):
+
+        if os.lstat(full_path).st_mode & os.R_OK == 0:
             raise IOError(
                 'Can not access file in context: {}'.format(full_path)
             )


Test patch:
diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py
--- a/tests/unit/utils_test.py
+++ b/tests/unit/utils_test.py
@@ -948,6 +948,20 @@ def test_tar_with_empty_directory(self):
             tar_data = tarfile.open(fileobj=archive)
             self.assertEqual(sorted(tar_data.getnames()), ['bar', 'foo'])
 
+    @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No chmod on Windows')
+    def test_tar_with_inaccessible_file(self):
+        base = tempfile.mkdtemp()
+        full_path = os.path.join(base, 'foo')
+        self.addCleanup(shutil.rmtree, base)
+        with open(full_path, 'w') as f:
+            f.write('content')
+        os.chmod(full_path, 0o222)
+        with pytest.raises(IOError) as ei:
+            tar(base)
+
+        assert 'Can not access file in context: {}'.format(full_path) in \
+            ei.exconly()
+
     @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No symlinks on Windows')
     def test_tar_with_file_symlinks(self):
         base = tempfile.mkdtemp()
@@ -975,6 +989,18 @@ def test_tar_with_directory_symlinks(self):
                 sorted(tar_data.getnames()), ['bar', 'bar/foo', 'foo']
             )
 
+    @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No symlinks on Windows')
+    def test_tar_with_broken_symlinks(self):
+        base = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, base)
+        for d in ['foo', 'bar']:
+            os.makedirs(os.path.join(base, d))
+
+        os.symlink('../baz', os.path.join(base, 'bar/foo'))
+        with tar(base) as archive:
+            tar_data = tarfile.open(fileobj=archive)
+            assert sorted(tar_data.getnames()) == ['bar', 'bar/foo', 'foo']
+
     @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No UNIX sockets on Win32')
     def test_tar_socket_file(self):
         base = tempfile.mkdtemp()