Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## 7.0.0

### Breaking Changes

- **`datacustomcode zip` and `datacustomcode deploy` no longer package symlinks.**

Symlinks found inside the payload directory are skipped instead of being followed. A symlinked file logs a `Skipping symlink file <path>` warning and is left out of `deployment.zip`.

**Why:** Following a symlink packaged the bytes of its target into the archive, even when the target was outside the payload (for example `payload/notes.txt -> ~/.ssh/id_rsa`).

**Migration:** Replace any symlink in the payload with a real copy of the file or directory it points to:

```bash
# Before
ln -s ../shared/helpers.py payload/helpers.py

# After
cp ../shared/helpers.py payload/helpers.py
```

## 6.1.0

### Added
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,8 @@ Options:
#### `datacustomcode zip`
Zip a transformation job in preparation to upload to Data Cloud. Make sure to change directory into your code package folder (e.g., `my_package`) before running this command.

Symlinks inside the payload folder are not followed or packaged. Each symlinked file that is skipped is reported with a warning, and symlinked directories are not included. `datacustomcode deploy` zips the same way.

Arguments:
- `PATH`: Path to the code directory i.e. the payload folder (default: "payload")

Expand Down
11 changes: 7 additions & 4 deletions src/datacustomcode/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,12 +630,15 @@ def zip(
logger.debug(f"Zipping directory... {directory}")

with zipfile.ZipFile(ZIP_FILE_NAME, "w", zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(directory):
# Skip .DS_Store and local credentials.
for root, dirs, files in os.walk(directory, followlinks=False):
# Skip symlinks, .DS_Store and local credentials.
for file in files:
abs_path = os.path.join(root, file)
arcname = os.path.relpath(abs_path, directory)
if os.path.islink(abs_path):
logger.warning(f"Skipping symlink file {arcname}")
continue
if file not in EXCLUDED_FILES:
abs_path = os.path.join(root, file)
arcname = os.path.relpath(abs_path, directory)
zipf.write(abs_path, arcname)

logger.debug(f"Created zip file: {ZIP_FILE_NAME}")
Expand Down
38 changes: 38 additions & 0 deletions tests/test_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,44 @@ def test_zip_excludes_ds_store_and_external_credentials(
assert not any(".DS_Store" in path for path in written)


class TestZipSymlinks:
"""zip() must skip symlinked files instead of following them into the archive."""

@pytest.fixture
def mocks(self):
with (
patch(
"datacustomcode.deploy.has_nonempty_requirements_file",
return_value=False,
),
patch("zipfile.ZipFile") as mock_zipfile,
patch("os.walk") as mock_walk,
):
yield mock_walk, mock_zipfile.return_value.__enter__.return_value

def test_zip_skips_symlinked_files(self, mocks):
mock_walk, mock_zipfile_instance = mocks
mock_walk.return_value = [
("/test/dir", ["subdir"], ["entrypoint.py", "linked.txt"]),
("/test/dir/subdir", [], ["nested.py", "linked.py"]),
]
links = {"/test/dir/linked.txt", "/test/dir/subdir/linked.py"}

with patch("os.path.islink", side_effect=lambda path: path in links):
zip("/test/dir", "default", "script")

written = [call.args[0] for call in mock_zipfile_instance.write.call_args_list]
assert written == ["/test/dir/entrypoint.py", "/test/dir/subdir/nested.py"]

def test_zip_skips_symlinked_directories(self, mocks):
mock_walk, _ = mocks
mock_walk.return_value = []

zip("/test/dir", "default", "script")

mock_walk.assert_called_once_with("/test/dir", followlinks=False)


class TestUploadZip:
@patch("datacustomcode.deploy.requests.put")
@patch("builtins.open", new_callable=mock_open, read_data=b"test data")
Expand Down
Loading