Skip to content
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

Add optional deploy files argument #933

Merged
merged 82 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
ad43ea1
add files argument to deploy
sfc-gh-gbloom Mar 22, 2024
041b385
add integration test
sfc-gh-gbloom Mar 25, 2024
159752b
stage files relative to deploy root
sfc-gh-gbloom Mar 25, 2024
dc4c967
add unit tests
sfc-gh-gbloom Mar 26, 2024
b95962d
check for empty list
sfc-gh-gbloom Mar 26, 2024
6265837
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Mar 26, 2024
2149053
release notes update
sfc-gh-gbloom Mar 26, 2024
f8b8c5e
update command description
sfc-gh-gbloom Mar 26, 2024
7a07388
unit test grooming
sfc-gh-gbloom Mar 26, 2024
9fa9f44
pr comments
sfc-gh-gbloom Mar 26, 2024
1e29c4b
add types
sfc-gh-gbloom Mar 26, 2024
e2119c3
fix ignore only on stage files
sfc-gh-gbloom Mar 27, 2024
5616e01
update snapshot
sfc-gh-gbloom Mar 27, 2024
9c4a902
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Mar 27, 2024
3a1e46d
fix set type for python3.8
sfc-gh-gbloom Mar 27, 2024
29994b0
delete remote only files
sfc-gh-gbloom Mar 27, 2024
f7963d8
fix relative paths
sfc-gh-gbloom Mar 27, 2024
1e82b1c
add integration test of removing remote only file
sfc-gh-gbloom Mar 27, 2024
e9c593b
add prune flag
sfc-gh-gbloom Apr 3, 2024
4031f68
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 3, 2024
a003915
default prune
sfc-gh-gbloom Apr 4, 2024
dc3ec90
move paths filter logic to manager
sfc-gh-gbloom Apr 4, 2024
28f916d
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 4, 2024
9749655
fix remote md5 map
sfc-gh-gbloom Apr 4, 2024
c60b20c
add build_md5_map test
sfc-gh-gbloom Apr 5, 2024
421607e
add no prune test
sfc-gh-gbloom Apr 5, 2024
868314d
change to relative to project root
sfc-gh-gbloom Apr 9, 2024
4c03ee3
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 9, 2024
da14615
type fix
sfc-gh-gbloom Apr 9, 2024
6392691
add integration coverage
sfc-gh-gbloom Apr 9, 2024
e162cda
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 10, 2024
1f22260
pr nits
sfc-gh-gbloom Apr 10, 2024
f4d01f5
pass created build files to find source from deploy path
sfc-gh-gbloom Apr 16, 2024
39c0d36
add recursive flag
sfc-gh-gbloom Apr 17, 2024
b6b80d2
add unit tests
sfc-gh-gbloom Apr 17, 2024
38ddfd6
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 17, 2024
3ac8f27
integration tests
sfc-gh-gbloom Apr 18, 2024
89381dc
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 18, 2024
3da37b1
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 18, 2024
488cac8
update snapshot
sfc-gh-gbloom Apr 18, 2024
0acd51b
relpath fix
sfc-gh-gbloom Apr 18, 2024
9ba1201
relpath fix
sfc-gh-gbloom Apr 18, 2024
c961ccd
fix windows relative path
sfc-gh-gbloom Apr 19, 2024
3f94f4e
resolved paths
sfc-gh-gbloom Apr 22, 2024
fead52e
resolve deploy root
sfc-gh-gbloom Apr 22, 2024
1177c8b
resolve deploy root
sfc-gh-gbloom Apr 23, 2024
e79ae17
fix test regex
sfc-gh-gbloom Apr 23, 2024
60551ef
debug
sfc-gh-gbloom Apr 23, 2024
b69921b
resolve source path
sfc-gh-gbloom Apr 23, 2024
d1e38b2
debug
sfc-gh-gbloom Apr 23, 2024
26497cb
debug
sfc-gh-gbloom Apr 23, 2024
7fff826
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 23, 2024
aecc5b4
handle windows path in integration test
sfc-gh-gbloom Apr 24, 2024
7176727
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 24, 2024
6ffbcf3
release notes
sfc-gh-gbloom Apr 24, 2024
8d89da9
move default arguments
sfc-gh-gbloom Apr 24, 2024
fc58314
pr nits
sfc-gh-gbloom Apr 24, 2024
b41ae52
pr nits
sfc-gh-gbloom Apr 25, 2024
26d0407
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 25, 2024
2dcd2d7
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 26, 2024
4d179c5
no recursive check in full deploy
sfc-gh-gbloom Apr 26, 2024
ccea424
prune test
sfc-gh-gbloom Apr 26, 2024
7ca66fa
add file not found test
sfc-gh-gbloom Apr 26, 2024
f7bc13e
fix non recursive full deploy
sfc-gh-gbloom Apr 26, 2024
b57f7d2
use different names in src/dest
sfc-gh-gbloom Apr 26, 2024
4568237
rename mapped_files
sfc-gh-gbloom Apr 29, 2024
c7be70e
rename source_path
sfc-gh-gbloom Apr 29, 2024
fbd6b04
nit
sfc-gh-gbloom Apr 29, 2024
a282972
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 29, 2024
ef3dedd
better docs
sfc-gh-gbloom Apr 29, 2024
4e4bf85
update snapshot
sfc-gh-gbloom Apr 29, 2024
b8f0650
better docs
sfc-gh-gbloom Apr 29, 2024
e1cc96d
use ClickException
sfc-gh-gbloom Apr 30, 2024
e3550de
tests
sfc-gh-gbloom Apr 30, 2024
63db862
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 30, 2024
1ea1785
add snapshot
sfc-gh-gbloom Apr 30, 2024
ece7214
match click exception output
sfc-gh-gbloom Apr 30, 2024
4e39f76
remove text assertion
sfc-gh-gbloom Apr 30, 2024
be7d3a1
doc
sfc-gh-gbloom Apr 30, 2024
19ab8a2
snapshot
sfc-gh-gbloom Apr 30, 2024
5b55216
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom May 2, 2024
dd293e9
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom May 6, 2024
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
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 @@
* Added `--if-not-exists` option to `create` commands for `service`, and `compute-pool`. Added `--replace` and `--if-not-exists` options for `image-repository create`.
* Added support for python connector diagnostic report.
* Added `snow app deploy` command that creates an application package and syncs the local changes to the stage without creating or updating the application.
* Added `snow app deploy [files]` option to sync only specific files to the stage. Can also be used to remove files that exists only on the stage.
sfc-gh-gbloom marked this conversation as resolved.
Show resolved Hide resolved
* `snow snowpark package create`:
* new `--ignore-anaconda` flag disables package lookup in Snowflake Anaconda channel.
All dependencies will be downloaded from PyPi.
Expand Down
10 changes: 8 additions & 2 deletions src/snowflake/cli/plugins/nativeapp/commands.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from typing import Optional
from pathlib import Path
from typing import List, Optional

import typer
from snowflake.cli.api.cli_global_context import cli_context
Expand Down Expand Up @@ -229,6 +230,11 @@ def app_teardown(
@app.command("deploy", requires_connection=True)
@with_project_definition("native_app")
def app_deploy(
files: Optional[List[Path]] = typer.Argument(
default=None,
show_default=False,
help=f"""Paths of the files, relative to the deploy root, to be uploaded to a stage. If the path exists remotely but not locally, it will be deleted from the stage [default: sync all local changes to the stage]""",
sfc-gh-gbloom marked this conversation as resolved.
Show resolved Hide resolved
),
**options,
) -> CommandResult:
"""
Expand All @@ -240,6 +246,6 @@ def app_deploy(
)

manager.build_bundle()
manager.deploy()
manager.deploy(files)

return MessageResult(f"Deployed successfully.")
15 changes: 11 additions & 4 deletions src/snowflake/cli/plugins/nativeapp/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,11 @@ def build_bundle(self) -> None:
"""
build_bundle(self.project_root, self.deploy_root, self.artifacts)

def sync_deploy_root_with_stage(self, role: str) -> DiffResult:
def sync_deploy_root_with_stage(
self,
role: str,
files_to_sync: Optional[List[Path]] = 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.
Expand All @@ -295,7 +299,7 @@ def sync_deploy_root_with_stage(self, role: str) -> DiffResult:
"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 = stage_diff(self.deploy_root, self.stage_fqn, files_to_sync)
cc.message(str(diff))

# Upload diff-ed files to application package stage
Expand Down Expand Up @@ -421,7 +425,10 @@ def _apply_package_scripts(self) -> None:
err, role=self.package_role, warehouse=self.package_warehouse
)

def deploy(self) -> DiffResult:
def deploy(
self,
files_to_sync: Optional[List[Path]] = None,
) -> DiffResult:
"""app deploy process"""

# 1. Create an empty application package, if none exists
Expand All @@ -432,6 +439,6 @@ def deploy(self) -> DiffResult:
self._apply_package_scripts()

# 3. Upload files from deploy root local folder to the above stage
diff = self.sync_deploy_root_with_stage(self.package_role)
diff = self.sync_deploy_root_with_stage(self.package_role, files_to_sync)

return diff
52 changes: 48 additions & 4 deletions src/snowflake/cli/plugins/object/stage/diff.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import hashlib
import logging
import os
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional

from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
from typing import Dict, List, Optional, Set

from click.exceptions import (
ClickException,
FileError,
)
from snowflake.cli.api.exceptions import (
SnowflakeSQLExecutionError,
)
from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
from snowflake.connector.cursor import SnowflakeCursor

Expand Down Expand Up @@ -154,7 +161,36 @@ def build_md5_map(list_stage_cursor: SnowflakeCursor) -> Dict[str, str]:
}


def stage_diff(local_path: Path, stage_fqn: str) -> DiffResult:
def _get_relative_paths_to_sync(
sfc-gh-gbloom marked this conversation as resolved.
Show resolved Hide resolved
relative_files_to_sync: List[Path], local_path: Path, remote_paths: Set[str]
sfc-gh-gbloom marked this conversation as resolved.
Show resolved Hide resolved
) -> List[str]:
paths = []
for file in relative_files_to_sync:
path = Path(os.path.join(local_path, file))
sfc-gh-gbloom marked this conversation as resolved.
Show resolved Hide resolved
relpath = path.relative_to(local_path)
if not path.exists() and str(relpath) not in remote_paths:
raise FileError(
str(file), "This file does not exist either locally or remotely"
)
elif path.is_dir():
raise ClickException(f"Specifying directories is not supported: '{file}'")
else:
paths.append(str(relpath))
return paths


def _filter_from_diff(result: DiffResult, paths_to_keep: Set[str]) -> DiffResult:
sfc-gh-gbloom marked this conversation as resolved.
Show resolved Hide resolved
result.different = [i for i in result.different if i in paths_to_keep]
result.only_local = [i for i in result.only_local if i in paths_to_keep]
result.only_on_stage = [i for i in result.only_on_stage if i in paths_to_keep]
return result


def stage_diff(
local_path: Path,
stage_fqn: str,
files_to_sync: Optional[List[Path]] = None, # relative to local_path
) -> DiffResult:
"""
Diffs the files in a stage with a local folder.
"""
Expand Down Expand Up @@ -190,6 +226,14 @@ def stage_diff(local_path: Path, stage_fqn: str) -> DiffResult:
for relpath in remote_md5.keys():
result.only_on_stage.append(relpath)

if files_to_sync is not None and len(files_to_sync) > 0:
paths_to_keep = set(
_get_relative_paths_to_sync(
files_to_sync, local_path, set(result.only_on_stage)
)
)
return _filter_from_diff(result, paths_to_keep)

return result


Expand Down
8 changes: 7 additions & 1 deletion tests/__snapshots__/test_help_messages.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,17 @@
# name: test_help_messages[app.deploy]
'''

Usage: default app deploy [OPTIONS]
Usage: default app deploy [OPTIONS] [FILES]...

Creates an application package in your Snowflake account and syncs the local
changes to the stage without creating or updating the application.

╭─ Arguments ──────────────────────────────────────────────────────────────────╮
│ files [FILES]... Paths of the files, relative to the deploy root, to │
│ be uploaded to a stage. If the path exists remotely │
│ but not locally, it will be deleted from the stage │
│ [default: sync all local changes to the stage] │
sfc-gh-gbloom marked this conversation as resolved.
Show resolved Hide resolved
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --project -p TEXT Path where the Snowflake Native App project │
│ resides. Defaults to current working directory. │
Expand Down
2 changes: 1 addition & 1 deletion tests/nativeapp/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def test_sync_deploy_root_with_stage(
]
assert mock_execute.mock_calls == expected
mock_stage_diff.assert_called_once_with(
native_app_manager.deploy_root, "app_pkg.app_src.stage"
native_app_manager.deploy_root, "app_pkg.app_src.stage", None
)
mock_local_diff_with_stage.assert_called_once_with(
role="new_role",
Expand Down
100 changes: 98 additions & 2 deletions tests/object/stage/test_diff.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import hashlib
from pathlib import Path
from typing import Dict, List, Tuple, Union
from typing import Dict, List, Set, Tuple, Union
from unittest import mock

import pytest
from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
from click.exceptions import (
ClickException,
FileError,
)
from snowflake.cli.api.exceptions import (
SnowflakeSQLExecutionError,
)
from snowflake.cli.plugins.object.stage.diff import (
DiffResult,
_filter_from_diff,
_get_relative_paths_to_sync,
delete_only_on_stage_files,
enumerate_files,
get_stage_path_from_file,
Expand All @@ -19,6 +27,7 @@
from tests.testing_utils.files_and_dirs import temp_local_dir

STAGE_MANAGER = "snowflake.cli.plugins.object.stage.manager.StageManager"
STAGE_DIFF = "snowflake.cli.plugins.object.stage.diff"

FILE_CONTENTS = {
"README.md": "This is a README\n",
Expand Down Expand Up @@ -215,3 +224,90 @@ def test_sync_local_diff_with_stage(mock_remove, other_directory):
diff_result=diff,
stage_path=stage_name,
)


def exists_mock(path: Path):
if str(path) in ["/file", "/dir", "/dir/nested_file"]:
return True
else:
return False


def is_dir_mock(path: Path):
if str(path) == "/dir":
return True
else:
return False


# Mocking Path to mimic the following directory structure:
# /file
# /dir/nested_file
@mock.patch(f"{STAGE_DIFF}.Path.is_dir", autospec=True)
@mock.patch(f"{STAGE_DIFF}.Path.exists", autospec=True)
@pytest.mark.parametrize(
"files_to_sync,remote_paths,expected_exception",
[
[["file", "dir/nested_file"], set(), None],
[["file", "file2", "dir/file3"], set(["file2", "dir/file3"]), None],
[["file", "file2"], set(), FileError],
[["dir/file3"], set(), FileError],
[["dir"], set(), ClickException],
],
)
def test_get_full_file_paths_to_sync(
path_mock_exists,
path_mock_is_dir,
files_to_sync: List[Path],
remote_paths: Set[str],
expected_exception: Exception,
):
path_mock_exists.side_effect = exists_mock
path_mock_is_dir.side_effect = is_dir_mock
if expected_exception is None:
result = _get_relative_paths_to_sync(files_to_sync, "/", remote_paths)
assert len(result) == len(files_to_sync)
else:
with pytest.raises(expected_exception):
_get_relative_paths_to_sync(files_to_sync, "/", remote_paths)


def test_filter_from_diff():
diff: DiffResult = DiffResult()
diff.different = [
"different",
"different-2",
"dir/different",
"dir/different-2",
]
diff.only_local = [
"only_local",
"only_local-2",
"dir/only_local",
"dir/only_local-2",
]
diff.only_on_stage = [
"only_on_stage",
"only_on_stage-2",
"dir/only_on_stage",
"dir/only_on_stage-2",
]

paths_to_keep = set(
[
"different",
"only-local",
"only-stage",
"dir/different",
"dir/only-local",
"dir/only-stage",
]
)
diff = _filter_from_diff(diff, paths_to_keep)

for path in diff.different:
assert path in paths_to_keep
for path in diff.only_local:
assert path in paths_to_keep
for path in diff.only_on_stage:
assert path in paths_to_keep
Loading
Loading