Skip to content

Commit

Permalink
[SNOW-1345064] Fix stage path handling on Windows (#1045)
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-bdufour committed May 8, 2024
1 parent 92f3a20 commit d08be66
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 174 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
the number of anaconda dependencies for snowpark projects.
* Added support for fully qualified stage names in stage and git execute commands.
* Fixed a bug where `snow app run` was not upgrading the application when the local state and remote stage are identical (for example immediately after `snow app deploy`).
* Fixed handling of stage path separators on Windows

# v2.2.0

Expand Down
5 changes: 2 additions & 3 deletions src/snowflake/cli/plugins/nativeapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,8 @@ def app_deploy(
Creates an application package in your Snowflake account and syncs the local changes to the stage without creating or updating the application.
Running this command with no arguments at all, as in `snow app deploy`, is a shorthand for `snow app deploy --prune --recursive`.
"""
if files is None:
files = []
if prune is None and recursive is None and len(files) == 0:
has_files = files is not None and len(files) > 0
if prune is None and recursive is None and not has_files:
prune = True
recursive = True
else:
Expand Down
86 changes: 55 additions & 31 deletions src/snowflake/cli/plugins/nativeapp/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@
from snowflake.cli.plugins.nativeapp.utils import verify_exists, verify_no_directories
from snowflake.cli.plugins.stage.diff import (
DiffResult,
filter_from_diff,
stage_diff,
StagePath,
compute_stage_diff,
preserve_from_diff,
sync_local_diff_with_stage,
to_stage_path,
)
from snowflake.connector import ProgrammingError
from snowflake.connector.cursor import DictCursor
Expand Down Expand Up @@ -106,18 +108,23 @@ def ensure_correct_owner(row: dict, role: str, obj_name: str) -> None:
raise UnexpectedOwnerError(obj_name, role, actual_owner)


def _get_paths_to_sync(paths_to_sync: List[Path], deploy_root: Path) -> List[str]:
"""Takes a list of paths (files and directories), returning a list of all files recursively relative to the deploy root."""
paths = []
for path in paths_to_sync:
def _get_stage_paths_to_sync(
local_paths_to_sync: List[Path], deploy_root: Path
) -> List[StagePath]:
"""
Takes a list of paths (files and directories), returning a list of all files recursively relative to the deploy root.
"""

stage_paths = []
for path in local_paths_to_sync:
if path.is_dir():
for current_dir, _dirs, files in os.walk(path):
for file in files:
deploy_path = Path(current_dir, file).relative_to(deploy_root)
paths.append(str(deploy_path))
stage_paths.append(to_stage_path(deploy_path))
else:
paths.append(str(path.relative_to(deploy_root)))
return paths
stage_paths.append(to_stage_path(path.relative_to(deploy_root)))
return stage_paths


class NativeAppCommandProcessor(ABC):
Expand Down Expand Up @@ -309,12 +316,24 @@ def sync_deploy_root_with_stage(
role: str,
prune: bool,
recursive: bool,
paths_to_sync: List[Path] = [], # relative to project root
local_paths_to_sync: List[Path] | None = None,
mapped_files: Optional[ArtifactDeploymentMap] = None,
) -> DiffResult:
"""
Ensures that the files on our remote stage match the artifacts we have in
the local filesystem. Returns the DiffResult used to make changes.
the local filesystem.
Args:
role (str): The name of the role to use for queries and commands.
prune (bool): Whether to prune artifacts from the stage that don't exist locally.
recursive (bool): Whether to traverse directories recursively.
local_paths_to_sync (List[Path], optional): List of local paths to sync. Defaults to None to sync all
local paths. Note that providing an empty list here is equivalent to None.
mapped_files: the file mapping computed during the `bundle` step. Required when local_paths_to_sync is
provided.
Returns:
A `DiffResult` instance describing the changes that were performed.
"""

# Does a stage already exist within the application package, or we need to create one?
Expand All @@ -336,37 +355,42 @@ def sync_deploy_root_with_stage(
"Performing a diff between the Snowflake stage and your local deploy_root ('%s') directory."
% self.deploy_root
)
diff: DiffResult = stage_diff(self.deploy_root, self.stage_fqn)
diff: DiffResult = compute_stage_diff(self.deploy_root, self.stage_fqn)

files_not_removed = []
if len(paths_to_sync) > 0:
if local_paths_to_sync:
assert mapped_files is not None

# Deploying specific files/directories
resolved_paths_to_sync = [resolve_without_follow(p) for p in paths_to_sync]
resolved_paths_to_sync = [
resolve_without_follow(p) for p in local_paths_to_sync
]
if not recursive:
verify_no_directories(resolved_paths_to_sync)
deploy_paths_to_sync = [
source_path_to_deploy_path(p, mapped_files)
for p in resolved_paths_to_sync
]
verify_exists(deploy_paths_to_sync)
paths_to_sync_set = set(
_get_paths_to_sync(deploy_paths_to_sync, self.deploy_root.resolve())
stage_paths_to_sync = _get_stage_paths_to_sync(
deploy_paths_to_sync, self.deploy_root.resolve()
)
files_not_removed = filter_from_diff(diff, paths_to_sync_set, prune)
diff = preserve_from_diff(diff, stage_paths_to_sync)
else:
# Full deploy
if not recursive:
deploy_files = os.listdir(str(self.deploy_root.resolve()))
verify_no_directories([Path(path_str) for path_str in deploy_files])
if not prune:
files_not_removed = diff.only_on_stage
diff.only_on_stage = []

if len(files_not_removed) > 0:
files_not_removed_str = "\n".join(files_not_removed)
cc.warning(
f"The following files exist only on the stage:\n{files_not_removed_str}\n\nUse the --prune flag to delete them from the stage."
)
deploy_files = [p for p in self.deploy_root.resolve().iterdir()]
verify_no_directories(deploy_files)

if not prune:
files_not_removed = [str(path) for path in diff.only_on_stage]
diff.only_on_stage = []

if len(files_not_removed) > 0:
files_not_removed_str = "\n".join(files_not_removed)
cc.warning(
f"The following files exist only on the stage:\n{files_not_removed_str}\n\nUse the --prune flag to delete them from the stage."
)

cc.message(str(diff))

Expand All @@ -380,7 +404,7 @@ def sync_deploy_root_with_stage(
role=role,
deploy_root_path=self.deploy_root,
diff_result=diff,
stage_path=self.stage_fqn,
stage_fqn=self.stage_fqn,
)
return diff

Expand Down Expand Up @@ -497,7 +521,7 @@ def deploy(
self,
prune: bool,
recursive: bool,
paths_to_sync: List[Path] = [],
local_paths_to_sync: List[Path] | None = None,
mapped_files: Optional[ArtifactDeploymentMap] = None,
) -> DiffResult:
"""app deploy process"""
Expand All @@ -511,7 +535,7 @@ def deploy(

# 3. Upload files from deploy root local folder to the above stage
diff = self.sync_deploy_root_with_stage(
self.package_role, prune, recursive, paths_to_sync, mapped_files
self.package_role, prune, recursive, local_paths_to_sync, mapped_files
)

return diff
8 changes: 4 additions & 4 deletions src/snowflake/cli/plugins/stage/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
SingleQueryResult,
)
from snowflake.cli.api.utils.path_utils import is_stage_path
from snowflake.cli.plugins.stage.diff import DiffResult
from snowflake.cli.plugins.stage.diff import DiffResult, compute_stage_diff
from snowflake.cli.plugins.stage.manager import OnErrorType, StageManager

app = SnowTyper(
Expand Down Expand Up @@ -125,14 +125,14 @@ def stage_remove(

@app.command("diff", hidden=True, requires_connection=True)
def stage_diff(
stage_name: str = typer.Argument(None, help="Fully qualified name of a stage"),
folder_name: str = typer.Argument(None, help="Path to local folder"),
stage_name: str = typer.Argument(help="Fully qualified name of a stage"),
folder_name: str = typer.Argument(help="Path to local folder"),
**options,
) -> ObjectResult:
"""
Diffs a stage with a local folder.
"""
diff: DiffResult = stage_diff(Path(folder_name), stage_name)
diff: DiffResult = compute_stage_diff(Path(folder_name), stage_name)
return ObjectResult(str(diff))


Expand Down
Loading

0 comments on commit d08be66

Please sign in to comment.