-
Notifications
You must be signed in to change notification settings - Fork 3
[ACIX-1225] Create dda env dev fs import and export commands
#233
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Ishirui
merged 20 commits into
main
from
pierrelouis.veyrenc/ACIX-1225-env-dev-fs-commands
Jan 13, 2026
Merged
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
0d4165e
feat(env): Create `fs` command group with `export` and `import` comma…
Ishirui f398217
feat(fs): Implement skeleton for `export` and `import` commands
Ishirui f6638d4
fix(fs): Pass non-host paths as `str`
Ishirui 3cd4d22
feat(env): Implement `LinuxContainer.export_files`
Ishirui 95901d4
refactor(env): Move host filesystem import logic to `fs.py`
Ishirui 08d4b2e
feat(env): Implement hidden `localimport` command to easily call `imp…
Ishirui ce5915e
feat(fs): Allow passing `dir` to `temp_directory`
Ishirui d12ba33
feat(env): Implement `LinuxContainer.import_files`
Ishirui 78b4128
fix(linux_container): Actually mount the shared directory into the co…
Ishirui 721dada
fix(env): Small fixes for `import` command
Ishirui 82d700a
fix(fs): Make sure we `docker cp` into the temporary directory
Ishirui 365fdad
refactor: Copilot-suggested review changes
Ishirui 38b43e7
Revert initial implementation of `LinuxContainer.export_files` and `L…
Ishirui 216d5f9
refactor(fs): Simplify interface for `import` and `export` commands
Ishirui 608f3bf
feat(fs): Implement `LinuxContainer.export_path`
Ishirui 9bf5081
feat(fs): Create `dda.utils.fs::cp_r` function for matching `cp -r` b…
Ishirui 5caf9c7
feat(fs): Implement `LinuxContainer.import_path`
Ishirui 9109324
fix: Update copyright years
Ishirui 30773c9
fix(docs): Misnamed parameter
Ishirui b392a7c
fix(tests): Fix for windows paths
Ishirui File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| # SPDX-FileCopyrightText: 2024-present Datadog, Inc. <[email protected]> | ||
| # | ||
| # SPDX-License-Identifier: MIT | ||
| from __future__ import annotations | ||
|
|
||
| from dda.cli.base import dynamic_group | ||
|
|
||
|
|
||
| @dynamic_group( | ||
| short_help="Interact with the environment's filesystem", | ||
| ) | ||
| def cmd() -> None: | ||
| pass |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| # SPDX-FileCopyrightText: 2024-present Datadog, Inc. <[email protected]> | ||
| # | ||
| # SPDX-License-Identifier: MIT | ||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| import click | ||
|
|
||
| from dda.cli.base import dynamic_command, pass_app | ||
| from dda.cli.env.dev.utils import option_env_type | ||
| from dda.utils.fs import Path | ||
|
|
||
| if TYPE_CHECKING: | ||
| from dda.cli.application import Application | ||
|
|
||
|
|
||
| @dynamic_command( | ||
| short_help="""Export files and directories from a developer environment""", | ||
| ) | ||
| @option_env_type() | ||
| @click.option("--id", "instance", default="default", help="Unique identifier for the environment") | ||
| @click.argument("sources", nargs=-1, required=True) | ||
| @click.argument("destination", required=True, type=click.Path(resolve_path=True, path_type=Path)) | ||
| @click.option("--recursive", "-r", is_flag=True, help="Export files and directories recursively.") | ||
| @click.option( | ||
| "--force", | ||
| "-f", | ||
| is_flag=True, | ||
| help="Overwrite existing files. Without this option, an error will be raised if the destination file already exists.", | ||
| ) | ||
| @click.option( | ||
| "--mkpath", is_flag=True, help="Create the destination directories and their parents if they do not exist." | ||
| ) | ||
| @pass_app | ||
| def cmd( | ||
| app: Application, | ||
| *, | ||
| env_type: str, | ||
| instance: str, | ||
| sources: tuple[str, ...], # Passed as string since they are inside the env filesystem | ||
| destination: Path, | ||
| recursive: bool, | ||
| force: bool, | ||
| mkpath: bool, | ||
| ) -> None: | ||
| """ | ||
| Export files and directories from a developer environment, using an interface similar to `cp`. | ||
| The last path specified is the destination directory on the host filesystem. | ||
| """ | ||
| from dda.env.dev import get_dev_env | ||
| from dda.env.models import EnvironmentState | ||
|
|
||
| env = get_dev_env(env_type)( | ||
| app=app, | ||
| name=env_type, | ||
| instance=instance, | ||
| ) | ||
| status = env.status() | ||
|
|
||
| # TODO: This might end up depending on the environment type. | ||
| # For `linux-container` though, `docker cp` also works on stopped containers. | ||
Ishirui marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| possible_states = {EnvironmentState.STARTED, EnvironmentState.STOPPED} | ||
| if status.state not in possible_states: | ||
| app.abort( | ||
| f"Developer environment `{env_type}` is in state `{status.state}`, must be one of: " | ||
| f"{', '.join(sorted(possible_states))}" | ||
| ) | ||
|
|
||
| try: | ||
| env.export_files(sources, destination, recursive, force, mkpath) | ||
| except Exception as error: # noqa: BLE001 | ||
| app.abort(f"Failed to export files: {error}") | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # SPDX-FileCopyrightText: 2024-present Datadog, Inc. <[email protected]> | ||
| # | ||
| # SPDX-License-Identifier: MIT | ||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| import click | ||
|
|
||
| from dda.cli.base import dynamic_command, pass_app | ||
| from dda.cli.env.dev.utils import option_env_type | ||
| from dda.utils.fs import Path | ||
|
|
||
| if TYPE_CHECKING: | ||
| from dda.cli.application import Application | ||
|
|
||
|
|
||
| @dynamic_command(short_help="""Import files and directories into a developer environment""") | ||
| @option_env_type() | ||
| @click.option("--id", "instance", default="default", help="Unique identifier for the environment") | ||
| @click.argument("sources", nargs=-1, required=True, type=click.Path(exists=True, resolve_path=True, path_type=Path)) | ||
Ishirui marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Ishirui marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| @click.argument("destination", required=True) | ||
| @click.option("--recursive", "-r", is_flag=True, help="Import files and directories recursively.") | ||
| @click.option( | ||
| "--force", | ||
| "-f", | ||
| is_flag=True, | ||
| help="Overwrite existing files. Without this option, an error will be raised if the destination file already exists.", | ||
| ) | ||
| @click.option( | ||
| "--mkpath", is_flag=True, help="Create the destination directories and their parents if they do not exist." | ||
| ) | ||
| @pass_app | ||
| def cmd( | ||
| app: Application, | ||
| *, | ||
| env_type: str, | ||
| instance: str, | ||
| sources: tuple[Path, ...], | ||
| destination: str, # Passed as string since it is inside the env filesystem | ||
| recursive: bool, | ||
| force: bool, | ||
| mkpath: bool, | ||
| ) -> None: | ||
| """ | ||
| Import files and directories into a developer environment, using an interface similar to `cp`. | ||
| The last path specified is the destination directory inside the environment. | ||
| """ | ||
| from dda.env.dev import get_dev_env | ||
| from dda.env.models import EnvironmentState | ||
|
|
||
| env = get_dev_env(env_type)( | ||
| app=app, | ||
| name=env_type, | ||
| instance=instance, | ||
| ) | ||
| status = env.status() | ||
|
|
||
| possible_states = {EnvironmentState.STARTED} | ||
Ishirui marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if status.state not in possible_states: | ||
| app.abort( | ||
| f"Developer environment `{env_type}` is in state `{status.state}`, must be one of: " | ||
| f"{', '.join(sorted(possible_states))}" | ||
| ) | ||
|
|
||
| try: | ||
| env.import_files(sources, destination, recursive, force, mkpath) | ||
| except Exception as error: # noqa: BLE001 | ||
| app.abort(f"Failed to import files: {error}") | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # SPDX-FileCopyrightText: 2024-present Datadog, Inc. <[email protected]> | ||
| # | ||
| # SPDX-License-Identifier: MIT | ||
| from __future__ import annotations | ||
|
|
||
| import click | ||
|
|
||
| from dda.cli.base import dynamic_command | ||
| from dda.utils.fs import Path | ||
|
|
||
|
|
||
| @dynamic_command(short_help="""Internal command used to call import_from_dir in dev envs.""", hidden=True) | ||
| @click.argument( | ||
| "source", required=True, type=click.Path(exists=True, resolve_path=True, file_okay=False, path_type=Path) | ||
| ) | ||
| @click.argument("destination", required=True, type=click.Path(resolve_path=True, path_type=Path)) | ||
| # Use arguments instead of options to enforce the idea that these are required | ||
| @click.argument("recursive", required=True, type=bool) | ||
| @click.argument("force", required=True, type=bool) | ||
| @click.argument("mkpath", required=True, type=bool) | ||
Ishirui marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| def cmd( | ||
| *, | ||
| source: Path, | ||
| destination: Path, | ||
| recursive: bool, | ||
| force: bool, | ||
| mkpath: bool, | ||
| ) -> None: | ||
| """ | ||
| Internal command used to call import_from_dir in dev envs. | ||
Ishirui marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| This allows us to use the same semantics for importing files and directories into a dev env as for exporting them on the host filesystem. | ||
| """ | ||
| from dda.env.dev.fs import import_from_dir | ||
|
|
||
| import_from_dir(source, destination, recursive=recursive, force=force, mkpath=mkpath) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| # SPDX-FileCopyrightText: 2024-present Datadog, Inc. <[email protected]> | ||
| # | ||
| # SPDX-License-Identifier: MIT | ||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| if TYPE_CHECKING: | ||
| from dda.utils.fs import Path | ||
|
|
||
|
|
||
| def determine_final_copy_target(source_name: str, source_is_dir: bool, destination_spec: Path) -> Path: # noqa: FBT001 | ||
| """ | ||
| Determines the final target for a copy operation, given a destination specification and some details about the source. | ||
| For example: | ||
| - f("file.txt", False, "/tmp/some-dir") -> "/tmp/some-dir/file.txt" (move into directory) | ||
| - f("file.txt", False, "/tmp/new-file.txt") -> "/tmp/new-file.txt" (rename file) | ||
| - f("some-dir", True, "/tmp/some-dir") -> "/tmp/some-dir/some-dir" (move directory into directory) | ||
|
|
||
| Parameters: | ||
| - source_name: The name of the source file or directory. The source is usually inside the env filesystem, not the host. | ||
| - source_is_dir: Whether the source is a directory. | ||
| - destination_spec: The destination specification, which can be a directory or a file. The destination is usually on the host filesystem. | ||
|
|
||
| Returns: | ||
| - The final target path. | ||
| """ | ||
|
|
||
| if destination_spec.is_dir(): | ||
| # The destination exists and is a directory or a symlink to one | ||
| # Always move the source inside it | ||
| # TODO: Add a check if destination_spec / source.name is an already-existing file or directory | ||
| # Currently shutil.move will fail with an ugly error message when we eventually call it | ||
| return destination_spec / source_name | ||
|
|
||
| if destination_spec.is_file(): | ||
| # The destination exists and is a file | ||
| if source_is_dir: | ||
| # Never overwrite a file with a directory | ||
| msg = f"Refusing to overwrite existing file with directory: {destination_spec}" | ||
| raise ValueError(msg) | ||
| # Source and destination are both files - rename | ||
| return destination_spec | ||
|
|
||
| # The destination does not exist, assume we want it exactly there | ||
| return destination_spec | ||
|
|
||
|
|
||
| def handle_overwrite(dest: Path, *, force: bool) -> None: | ||
| if not dest.exists(): | ||
| return | ||
|
|
||
| if dest.is_dir(): | ||
| msg = f"Refusing to overwrite directory {dest}." | ||
| raise ValueError(msg) | ||
|
|
||
| if not force: | ||
| msg = f"Refusing to overwrite existing file: {dest} (force flag is not set)." | ||
| raise ValueError(msg) | ||
|
|
||
| dest.unlink() | ||
|
|
||
|
|
||
| def import_from_dir(source_dir: Path, destination_spec: Path, *, recursive: bool, force: bool, mkpath: bool) -> None: | ||
Ishirui marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """ | ||
| Import files and directories from a given directory into a destination directory on the "host" filesystem. | ||
| "Host" in this context refers to the environment `dda` is being executed in: if that is inside of a dev env, then we mean the dev env's file system. | ||
| """ | ||
Ishirui marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| from shutil import move | ||
|
|
||
| if mkpath: | ||
| destination_spec.ensure_dir() | ||
|
|
||
| for element in source_dir.iterdir(): | ||
| if not recursive and element.is_dir(): | ||
| msg = "Refusing to copy directories as recursive flag is not set" | ||
| raise ValueError(msg) | ||
|
|
||
| final_target = determine_final_copy_target(element.name, element.is_dir(), destination_spec) | ||
| handle_overwrite(final_target, force=force) | ||
| move(element, final_target) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.