diff --git a/src/snowflake/cli/plugins/nativeapp/commands.py b/src/snowflake/cli/plugins/nativeapp/commands.py index 2759197eb..ae94c9350 100644 --- a/src/snowflake/cli/plugins/nativeapp/commands.py +++ b/src/snowflake/cli/plugins/nativeapp/commands.py @@ -150,6 +150,7 @@ def app_list_templates(**options) -> CommandResult: @app.command("bundle") @with_project_definition() +@nativeapp_definition_v2_to_v1 def app_bundle( **options, ) -> CommandResult: @@ -284,6 +285,7 @@ def app_teardown( @app.command("deploy", requires_connection=True) @with_project_definition() +@nativeapp_definition_v2_to_v1 def app_deploy( prune: Optional[bool] = typer.Option( default=None, @@ -350,6 +352,7 @@ def app_deploy( @app.command("validate", requires_connection=True) @with_project_definition() +@nativeapp_definition_v2_to_v1 def app_validate(**options): """ Validates a deployed Snowflake Native App's setup script. diff --git a/src/snowflake/cli/plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py b/src/snowflake/cli/plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py index b71d00340..a886e4611 100644 --- a/src/snowflake/cli/plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +++ b/src/snowflake/cli/plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py @@ -15,7 +15,8 @@ from __future__ import annotations from functools import wraps -from typing import Any, Dict, Optional +from pathlib import Path +from typing import Any, Dict, Optional, Union from click import ClickException from snowflake.cli.api.cli_global_context import cli_context, cli_context_manager @@ -25,12 +26,25 @@ from snowflake.cli.api.project.schemas.entities.application_package_entity import ( ApplicationPackageEntity, ) +from snowflake.cli.api.project.schemas.native_app.path_mapping import PathMapping from snowflake.cli.api.project.schemas.project_definition import ( DefinitionV11, DefinitionV20, ) +def _convert_v2_artifact_to_v1_dict( + v2_artifact: Union[PathMapping, Path] +) -> Union[Dict, str]: + if isinstance(v2_artifact, PathMapping): + return { + "src": v2_artifact.src, + "dest": v2_artifact.dest, + "processors": v2_artifact.processors, + } + return str(v2_artifact) + + def _pdf_v2_to_v1(v2_definition: DefinitionV20) -> DefinitionV11: pdfv1: Dict[str, Any] = {"definition_version": "1.1", "native_app": {}} @@ -57,8 +71,13 @@ def _pdf_v2_to_v1(v2_definition: DefinitionV20) -> DefinitionV11: # NativeApp pdfv1["native_app"]["name"] = "Auto converted NativeApp project from V2" - pdfv1["native_app"]["artifacts"] = app_package_definition.artifacts + pdfv1["native_app"]["artifacts"] = [ + _convert_v2_artifact_to_v1_dict(a) for a in app_package_definition.artifacts + ] pdfv1["native_app"]["source_stage"] = app_package_definition.stage + pdfv1["native_app"]["bundle_root"] = str(app_package_definition.bundle_root) + pdfv1["native_app"]["generated_root"] = str(app_package_definition.generated_root) + pdfv1["native_app"]["deploy_root"] = str(app_package_definition.deploy_root) # Package pdfv1["native_app"]["package"] = {} diff --git a/tests/nativeapp/test_v2_to_v1.py b/tests/nativeapp/test_v2_to_v1.py index 99beb1970..66110e25a 100644 --- a/tests/nativeapp/test_v2_to_v1.py +++ b/tests/nativeapp/test_v2_to_v1.py @@ -88,6 +88,9 @@ "artifacts": [{"src": "app/*", "dest": "./"}], "manifest": "", "stage": "app.stage", + "bundle_root": "bundle_root", + "generated_root": "generated_root", + "deploy_root": "deploy_root", }, "app": { "type": "application", @@ -103,6 +106,9 @@ "name": "Auto converted NativeApp project from V2", "artifacts": [{"src": "app/*", "dest": "./"}], "source_stage": "app.stage", + "bundle_root": "bundle_root", + "generated_root": "generated_root", + "deploy_root": "deploy_root", "package": { "name": "pkg_name", }, diff --git a/tests_integration/nativeapp/__snapshots__/test_deploy.ambr b/tests_integration/nativeapp/__snapshots__/test_deploy.ambr index f72c71c40..002cfe8f0 100644 --- a/tests_integration/nativeapp/__snapshots__/test_deploy.ambr +++ b/tests_integration/nativeapp/__snapshots__/test_deploy.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_nativeapp_deploy +# name: test_nativeapp_deploy[v1] ''' Creating new application package myapp_pkg_@@USER@@ in account. Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. @@ -14,7 +14,7 @@ ''' # --- -# name: test_nativeapp_deploy_dot +# name: test_nativeapp_deploy[v2] ''' Creating new application package myapp_pkg_@@USER@@ in account. Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. @@ -29,20 +29,78 @@ ''' # --- -# name: test_nativeapp_deploy_files +# name: test_nativeapp_deploy_dot[v1] ''' Creating new application package myapp_pkg_@@USER@@ in account. Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. Performing a diff between the Snowflake stage and your local deploy_root ('@@DEPLOY_ROOT@@') directory. Local changes to be deployed: + added: app/README.md -> README.md + added: app/manifest.yml -> manifest.yml + added: app/setup_script.sql -> setup_script.sql + Updating the Snowflake stage from your local @@DEPLOY_ROOT@@ directory. + Validating Snowflake Native App setup script. + Deployed successfully. Application package and stage are up-to-date. + + ''' +# --- +# name: test_nativeapp_deploy_dot[v2] + ''' + Creating new application package myapp_pkg_@@USER@@ in account. + Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. + Performing a diff between the Snowflake stage and your local deploy_root ('@@DEPLOY_ROOT@@') directory. + Local changes to be deployed: + added: app/README.md -> README.md + added: app/manifest.yml -> manifest.yml + added: app/setup_script.sql -> setup_script.sql + Updating the Snowflake stage from your local @@DEPLOY_ROOT@@ directory. + Validating Snowflake Native App setup script. + Deployed successfully. Application package and stage are up-to-date. + + ''' +# --- +# name: test_nativeapp_deploy_files[v1] + ''' + Creating new application package myapp_pkg_@@USER@@ in account. + Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. + Performing a diff between the Snowflake stage and your local deploy_root ('@@DEPLOY_ROOT@@') directory. + Local changes to be deployed: + added: app/manifest.yml -> manifest.yml + added: app/setup_script.sql -> setup_script.sql + Updating the Snowflake stage from your local @@DEPLOY_ROOT@@ directory. + Deployed successfully. Application package and stage are up-to-date. + + ''' +# --- +# name: test_nativeapp_deploy_files[v2] + ''' + Creating new application package myapp_pkg_@@USER@@ in account. + Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. + Performing a diff between the Snowflake stage and your local deploy_root ('@@DEPLOY_ROOT@@') directory. + Local changes to be deployed: + added: app/manifest.yml -> manifest.yml + added: app/setup_script.sql -> setup_script.sql + Updating the Snowflake stage from your local @@DEPLOY_ROOT@@ directory. + Deployed successfully. Application package and stage are up-to-date. + + ''' +# --- +# name: test_nativeapp_deploy_looks_for_prefix_matches[v1] + ''' + Creating new application package myapp_pkg_@@USER@@ in account. + Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. + Performing a diff between the Snowflake stage and your local deploy_root ('@@DEPLOY_ROOT@@') directory. + Local changes to be deployed: + added: app/README.md -> README.md added: app/manifest.yml -> manifest.yml added: app/setup_script.sql -> setup_script.sql Updating the Snowflake stage from your local @@DEPLOY_ROOT@@ directory. + Validating Snowflake Native App setup script. Deployed successfully. Application package and stage are up-to-date. ''' # --- -# name: test_nativeapp_deploy_looks_for_prefix_matches +# name: test_nativeapp_deploy_looks_for_prefix_matches[v2] ''' Creating new application package myapp_pkg_@@USER@@ in account. Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. @@ -57,7 +115,7 @@ ''' # --- -# name: test_nativeapp_deploy_nested_directories +# name: test_nativeapp_deploy_nested_directories[v1] ''' Creating new application package myapp_pkg_@@USER@@ in account. Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. @@ -69,7 +127,55 @@ ''' # --- -# name: test_nativeapp_deploy_prune[app deploy --no-prune-contains2-not_contains2] +# name: test_nativeapp_deploy_nested_directories[v2] + ''' + Creating new application package myapp_pkg_@@USER@@ in account. + Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. + Performing a diff between the Snowflake stage and your local deploy_root ('@@DEPLOY_ROOT@@') directory. + Local changes to be deployed: + added: app/nested/dir/file.txt -> nested/dir/file.txt + Updating the Snowflake stage from your local @@DEPLOY_ROOT@@ directory. + Deployed successfully. Application package and stage are up-to-date. + + ''' +# --- +# name: test_nativeapp_deploy_prune[v1-app deploy --no-prune-contains2-not_contains2] + ''' + Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. + Performing a diff between the Snowflake stage and your local deploy_root ('@@DEPLOY_ROOT@@') directory. + The following files exist only on the stage: + README.md + + Use the --prune flag to delete them from the stage. + Your stage is up-to-date with your local deploy root. + Validating Snowflake Native App setup script. + Deployed successfully. Application package and stage are up-to-date. + + ''' +# --- +# name: test_nativeapp_deploy_prune[v1-app deploy --no-validate-contains1-not_contains1] + ''' + Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. + Performing a diff between the Snowflake stage and your local deploy_root ('@@DEPLOY_ROOT@@') directory. + Deleted paths to be removed from your stage: + deleted: README.md + Updating the Snowflake stage from your local @@DEPLOY_ROOT@@ directory. + Deployed successfully. Application package and stage are up-to-date. + + ''' +# --- +# name: test_nativeapp_deploy_prune[v1-app deploy --prune --no-validate-contains0-not_contains0] + ''' + Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. + Performing a diff between the Snowflake stage and your local deploy_root ('@@DEPLOY_ROOT@@') directory. + Deleted paths to be removed from your stage: + deleted: README.md + Updating the Snowflake stage from your local @@DEPLOY_ROOT@@ directory. + Deployed successfully. Application package and stage are up-to-date. + + ''' +# --- +# name: test_nativeapp_deploy_prune[v2-app deploy --no-prune-contains2-not_contains2] ''' Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. Performing a diff between the Snowflake stage and your local deploy_root ('@@DEPLOY_ROOT@@') directory. @@ -83,7 +189,7 @@ ''' # --- -# name: test_nativeapp_deploy_prune[app deploy --no-validate-contains1-not_contains1] +# name: test_nativeapp_deploy_prune[v2-app deploy --no-validate-contains1-not_contains1] ''' Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. Performing a diff between the Snowflake stage and your local deploy_root ('@@DEPLOY_ROOT@@') directory. @@ -94,7 +200,7 @@ ''' # --- -# name: test_nativeapp_deploy_prune[app deploy --prune --no-validate-contains0-not_contains0] +# name: test_nativeapp_deploy_prune[v2-app deploy --prune --no-validate-contains0-not_contains0] ''' Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. Performing a diff between the Snowflake stage and your local deploy_root ('@@DEPLOY_ROOT@@') directory. diff --git a/tests_integration/nativeapp/test_bundle.py b/tests_integration/nativeapp/test_bundle.py index 762c085e4..e4e4fb00f 100644 --- a/tests_integration/nativeapp/test_bundle.py +++ b/tests_integration/nativeapp/test_bundle.py @@ -14,13 +14,13 @@ import os import os.path +import yaml import uuid -from textwrap import dedent from snowflake.cli.api.project.util import generate_user_env from tests.project.fixtures import * -from tests_integration.test_utils import pushd +from tests_integration.test_utils import enable_definition_v2_feature_flag from tests_integration.testing_utils import ( assert_that_result_failed_with_message_containing, ) @@ -29,30 +29,59 @@ TEST_ENV = generate_user_env(USER_NAME) -@pytest.fixture -def template_setup(runner, temporary_working_directory): - project_name = "myapp" - result = runner.invoke_json( - ["app", "init", project_name], # Uses default template - env=TEST_ENV, - ) - assert result.exit_code == 0 +@pytest.fixture(scope="function", params=["v1", "v2"]) +def template_setup(runner, project_directory, request): + definition_version = request.param + with enable_definition_v2_feature_flag: + with project_directory(f"napp_init_{definition_version}") as project_root: + # Vanilla bundle on the unmodified template + result = runner.invoke_json( + ["app", "bundle"], + env=TEST_ENV, + ) + assert result.exit_code == 0 - # Vanilla bundle on the unmodified template - result = runner.invoke_json( - ["app", "bundle", "--project", project_name], - env=TEST_ENV, - ) - assert result.exit_code == 0 - # The newly created deploy_root is explicitly deleted here, as bundle should take care of it. + # The newly created deploy_root is explicitly deleted here, as bundle should take care of it. + + deploy_root = Path(project_root, "output", "deploy") + assert Path(deploy_root, "manifest.yml").is_file() + assert Path(deploy_root, "setup_script.sql").is_file() + assert Path(deploy_root, "README.md").is_file() - project_root = Path(temporary_working_directory, project_name) - deploy_root = Path(project_root, "output", "deploy") - assert Path(deploy_root, "manifest.yml").is_file() - assert Path(deploy_root, "setup_script.sql").is_file() - assert Path(deploy_root, "README.md").is_file() + yield project_root, runner, definition_version - return project_root, runner + +def override_snowflake_yml_artifacts( + definition_version, artifacts_section, deploy_root=Path("output", "deploy") +): + with open("snowflake.yml", "w") as f: + if definition_version == "v2": + file_content = yaml.dump( + { + "definition_version": "2", + "entities": { + "pkg": { + "type": "application package", + "name": "myapp_pkg_<% ctx.env.USER %>", + "artifacts": artifacts_section, + "manifest": "app/manifest.yml", + "deploy_root": str(deploy_root), + } + }, + } + ) + else: + file_content = yaml.dump( + { + "definition_version": "1", + "native_app": { + "name": "myapp", + "artifacts": artifacts_section, + "deploy_root": str(deploy_root), + }, + } + ) + f.write(file_content) # Tests that we copy files/directories directly to the deploy root instead of creating symlinks. @@ -60,50 +89,40 @@ def template_setup(runner, temporary_working_directory): def test_nativeapp_bundle_does_explicit_copy( template_setup, ): - project_root, runner = template_setup - - with pushd(project_root): - # overwrite the snowflake.yml rules - with open("snowflake.yml", "w") as f: - f.write( - dedent( - f""" - definition_version: 1 - native_app: - name: myapp - artifacts: - - src: app - dest: ./ - - src: snowflake.yml - dest: ./app/ - """ - ) - ) + project_root, runner, definition_version = template_setup + + override_snowflake_yml_artifacts( + definition_version, + artifacts_section=[ + {"src": "app", "dest": "./"}, + {"src": "snowflake.yml", "dest": "./app/"}, + ], + ) - result = runner.invoke_json( - ["app", "bundle"], - env=TEST_ENV, - ) - assert result.exit_code == 0 - assert not os.path.exists("app/snowflake.yml") - app_path = Path("output", "deploy", "app") - assert app_path.exists() and not app_path.is_symlink() - assert ( - Path(app_path, "manifest.yml").exists() - and Path(app_path, "manifest.yml").is_symlink() - ) - assert ( - Path(app_path, "setup_script.sql").exists() - and Path(app_path, "setup_script.sql").is_symlink() - ) - assert ( - Path(app_path, "README.md").exists() - and Path(app_path, "README.md").is_symlink() - ) - assert ( - Path(app_path, "snowflake.yml").exists() - and Path(app_path, "snowflake.yml").is_symlink() - ) + result = runner.invoke_json( + ["app", "bundle"], + env=TEST_ENV, + ) + assert result.exit_code == 0 + assert not os.path.exists("app/snowflake.yml") + app_path = Path("output", "deploy", "app") + assert app_path.exists() and not app_path.is_symlink() + assert ( + Path(app_path, "manifest.yml").exists() + and Path(app_path, "manifest.yml").is_symlink() + ) + assert ( + Path(app_path, "setup_script.sql").exists() + and Path(app_path, "setup_script.sql").is_symlink() + ) + assert ( + Path(app_path, "README.md").exists() + and Path(app_path, "README.md").is_symlink() + ) + assert ( + Path(app_path, "snowflake.yml").exists() + and Path(app_path, "snowflake.yml").is_symlink() + ) # Tests restrictions on the deploy root: It must be a sub-directory within the project directory @@ -112,227 +131,170 @@ def test_nativeapp_bundle_throws_error_due_to_project_root_deploy_root_mismatch( template_setup, ): - project_root, runner = template_setup + project_root, runner, definition_version = template_setup # Delete deploy_root since we test requirement of deploy_root being a directory shutil.rmtree(Path(project_root, "output", "deploy")) - with pushd(project_root) as project_dir: - deploy_root = Path(project_dir, "output") - # Make deploy root a file instead of directory - deploy_root_as_file = Path(deploy_root, "deploy") - deploy_root_as_file.touch(exist_ok=False) - - assert deploy_root_as_file.is_file() + deploy_root = Path(project_root, "output") + # Make deploy root a file instead of directory + deploy_root_as_file = Path(deploy_root, "deploy") + deploy_root_as_file.touch(exist_ok=False) - result = runner.invoke_json( - ["app", "bundle"], - env=TEST_ENV, - ) + assert deploy_root_as_file.is_file() - assert result.exit_code == 1 - assert_that_result_failed_with_message_containing( - result, "exists, but is not a directory!" - ) + result = runner.invoke_json( + ["app", "bundle"], + env=TEST_ENV, + ) - os.remove(deploy_root_as_file) - deploy_root.rmdir() + assert result.exit_code == 1 + assert_that_result_failed_with_message_containing( + result, "exists, but is not a directory!" + ) - original_cwd = os.getcwd() - assert not Path(original_cwd, "output").exists() + os.remove(deploy_root_as_file) + deploy_root.rmdir() # Make deploy root outside the project directory - deploy_root = Path(original_cwd, "output", "deploy") - deploy_root.mkdir(parents=True, exist_ok=False) - - with pushd(project_root): - with open("snowflake.yml", "w") as f: - f.write( - dedent( - f""" - definition_version: 1 - native_app: - name: myapp - deploy_root: {deploy_root} - artifacts: - - src: app - dest: ./ - - src: snowflake.yml - dest: ./app/ - """ - ) - ) + with tempfile.TemporaryDirectory() as tmpdir: + assert not Path(tmpdir, "output").exists() + deploy_root = Path(tmpdir, "output", "deploy") + deploy_root.mkdir(parents=True, exist_ok=False) + + override_snowflake_yml_artifacts( + definition_version, + artifacts_section=[ + {"src": "app", "dest": "./"}, + {"src": "snowflake.yml", "dest": "./app/"}, + ], + deploy_root=deploy_root, + ) - result = runner.invoke_json( - ["app", "bundle"], - env=TEST_ENV, - ) + result = runner.invoke_json( + ["app", "bundle"], + env=TEST_ENV, + ) - assert result.exit_code == 1 - assert_that_result_failed_with_message_containing( - result, "is not a descendent of the project directory!" - ) + assert result.exit_code == 1 + assert_that_result_failed_with_message_containing( + result, "is not a descendent of the project directory!" + ) # Tests restrictions on the src spec that it must be a glob that returns matches @pytest.mark.integration def test_nativeapp_bundle_throws_error_on_incorrect_src_glob(template_setup): - project_root, runner = template_setup - - with pushd(project_root): - # overwrite the snowflake.yml with incorrect glob - with open("snowflake.yml", "w") as f: - f.write( - dedent( - f""" - definition_version: 1 - native_app: - name: myapp - artifacts: - - app/? - """ - ), - ) + project_root, runner, definition_version = template_setup - result = runner.invoke_json( - ["app", "bundle"], - env=TEST_ENV, - ) - assert result.exit_code == 1 - assert_that_result_failed_with_message_containing( - result, - "No match was found for the specified source in the project directory", - ) + # incorrect glob + override_snowflake_yml_artifacts(definition_version, artifacts_section=["app/?"]) + + result = runner.invoke_json( + ["app", "bundle"], + env=TEST_ENV, + ) + assert result.exit_code == 1 + assert_that_result_failed_with_message_containing( + result, + "No match was found for the specified source in the project directory", + ) # Tests restrictions on the src spec that it must be relative to project root @pytest.mark.integration def test_nativeapp_bundle_throws_error_on_bad_src(template_setup): - project_root, runner = template_setup - - with pushd(project_root): - # overwrite the snowflake.yml with incorrect glob - with open("snowflake.yml", "w") as f: - f.write( - dedent( - f""" - definition_version: 1 - native_app: - name: myapp - artifacts: - - {Path(project_root, "app").absolute()} - """ - ), - ) + project_root, runner, definition_version = template_setup - result = runner.invoke_json( - ["app", "bundle"], - env=TEST_ENV, - ) - assert result.exit_code == 1 - assert_that_result_failed_with_message_containing( - result, "Source path must be a relative path" - ) + # absolute path + src_path = Path(project_root, "app").absolute() + override_snowflake_yml_artifacts( + definition_version, artifacts_section=[f"{src_path}"] + ) + + result = runner.invoke_json( + ["app", "bundle"], + env=TEST_ENV, + ) + assert result.exit_code == 1 + assert_that_result_failed_with_message_containing( + result, "Source path must be a relative path" + ) # Tests restrictions on the dest spec: It must be within the deploy root, and must be a relative path @pytest.mark.integration def test_nativeapp_bundle_throws_error_on_bad_dest(template_setup): - project_root, runner = template_setup - - with pushd(project_root): - # overwrite the snowflake.yml rules - with open("snowflake.yml", "w") as f: - f.write( - dedent( - f""" - definition_version: 1 - native_app: - name: myapp - artifacts: - - src: app/* - dest: / - """ - ) - ) + project_root, runner, definition_version = template_setup - result = runner.invoke_json( - ["app", "bundle"], - env=TEST_ENV, - ) - assert result.exit_code == 1 - assert_that_result_failed_with_message_containing( - result, "The specified destination path is outside of the deploy root" - ) - - with open("snowflake.yml", "w") as f: - f.write( - dedent( - f""" - definition_version: 1 - native_app: - name: myapp - artifacts: - - src: app/* - dest: {Path(project_root, "output", "deploy", "stagepath").absolute()} - """ - ) - ) + override_snowflake_yml_artifacts( + definition_version, artifacts_section=[{"src": "app/*", "dest": "/"}] + ) + + result = runner.invoke_json( + ["app", "bundle"], + env=TEST_ENV, + ) + assert result.exit_code == 1 + assert_that_result_failed_with_message_containing( + result, "The specified destination path is outside of the deploy root" + ) - result = runner.invoke_json( - ["app", "bundle"], - env=TEST_ENV, - ) - assert result.exit_code == 1 - assert_that_result_failed_with_message_containing( - result, "Destination path must be a relative path" - ) + override_snowflake_yml_artifacts( + definition_version, + artifacts_section=[ + { + "src": "app/*", + "dest": str( + Path(project_root, "output", "deploy", "stagepath").absolute() + ), + } + ], + ) + + result = runner.invoke_json( + ["app", "bundle"], + env=TEST_ENV, + ) + assert result.exit_code == 1 + assert_that_result_failed_with_message_containing( + result, "Destination path must be a relative path" + ) # Tests restriction on mapping multiple files to the same destination file @pytest.mark.integration def test_nativeapp_bundle_throws_error_on_too_many_files_to_dest(template_setup): + project_root, runner, definition_version = template_setup + + override_snowflake_yml_artifacts( + definition_version, + artifacts_section=[ + {"src": "app/manifest.yml", "dest": "manifest.yml"}, + {"src": "app/setup_script.sql", "dest": "manifest.yml"}, + ], + ) - project_root, runner = template_setup - with pushd(project_root): - # overwrite the snowflake.yml rules - with open("snowflake.yml", "w") as f: - f.write( - dedent( - f""" - definition_version: 1 - native_app: - name: myapp - artifacts: - - src: app/manifest.yml - dest: manifest.yml - - src: app/setup_script.sql - dest: manifest.yml - """ - ) - ) - - result = runner.invoke_json( - ["app", "bundle"], - env=TEST_ENV, - ) - assert result.exit_code == 1 - assert_that_result_failed_with_message_containing( - result, - "Multiple file or directories were mapped to one output destination.", - ) + result = runner.invoke_json( + ["app", "bundle"], + env=TEST_ENV, + ) + assert result.exit_code == 1 + assert_that_result_failed_with_message_containing( + result, + "Multiple file or directories were mapped to one output destination.", + ) # Tests that bundle wipes out any existing deploy root to recreate it from scratch on every run @pytest.mark.integration def test_nativeapp_bundle_deletes_existing_deploy_root(template_setup): - project_root, runner = template_setup - - with pushd(project_root) as project_dir: - existing_deploy_root_dest = Path(project_dir, "output", "deploy", "dummy.txt") - existing_deploy_root_dest.mkdir(parents=True, exist_ok=False) - result = runner.invoke_json( - ["app", "bundle"], - env=TEST_ENV, - ) - assert result.exit_code == 0 - assert not existing_deploy_root_dest.exists() + project_root, runner, definition_version = template_setup + + existing_deploy_root_dest = Path(project_root, "output", "deploy", "dummy.txt") + existing_deploy_root_dest.mkdir(parents=True, exist_ok=False) + result = runner.invoke_json( + ["app", "bundle"], + env=TEST_ENV, + ) + assert result.exit_code == 0 + assert not existing_deploy_root_dest.exists() diff --git a/tests_integration/nativeapp/test_deploy.py b/tests_integration/nativeapp/test_deploy.py index b54b4b0fb..cf4747a96 100644 --- a/tests_integration/nativeapp/test_deploy.py +++ b/tests_integration/nativeapp/test_deploy.py @@ -28,6 +28,7 @@ not_contains_row_with, pushd, row_from_snowflake_session, + enable_definition_v2_feature_flag, ) from tests_integration.testing_utils import ( assert_that_result_failed_with_message_containing, @@ -46,21 +47,18 @@ def sanitize_deploy_output(output): # Tests a simple flow of executing "snow app deploy", verifying that an application package was created, and an application was not @pytest.mark.integration +@enable_definition_v2_feature_flag +@pytest.mark.parametrize("definition_version", ["v1", "v2"]) def test_nativeapp_deploy( + definition_version, + project_directory, runner, snowflake_session, - temporary_working_directory, snapshot, print_paths_as_posix, ): project_name = "myapp" - result = runner.invoke_json( - ["app", "init", project_name], - env=TEST_ENV, - ) - assert result.exit_code == 0 - - with pushd(Path(os.getcwd(), project_name)): + with project_directory(f"napp_init_{definition_version}"): result = runner.invoke_with_connection( ["app", "deploy"], env=TEST_ENV, @@ -117,6 +115,7 @@ def test_nativeapp_deploy( @pytest.mark.integration +@enable_definition_v2_feature_flag @pytest.mark.parametrize( "command,contains,not_contains", [ @@ -132,24 +131,19 @@ def test_nativeapp_deploy( ["app deploy --no-prune", ["stage/README.md"], []], ], ) +@pytest.mark.parametrize("definition_version", ["v1", "v2"]) def test_nativeapp_deploy_prune( command, contains, not_contains, + definition_version, + project_directory, runner, - snowflake_session, - temporary_working_directory, snapshot, print_paths_as_posix, ): project_name = "myapp" - result = runner.invoke_json( - ["app", "init", project_name], - env=TEST_ENV, - ) - assert result.exit_code == 0 - - with pushd(Path(os.getcwd(), project_name)): + with project_directory(f"napp_init_{definition_version}"): result = runner.invoke_with_connection_json( ["app", "deploy"], env=TEST_ENV, @@ -198,20 +192,17 @@ def test_nativeapp_deploy_prune( # Tests a simple flow of executing "snow app deploy [files]", verifying that only the specified files are synced to the stage @pytest.mark.integration +@enable_definition_v2_feature_flag +@pytest.mark.parametrize("definition_version", ["v1", "v2"]) def test_nativeapp_deploy_files( + definition_version, + project_directory, runner, - temporary_working_directory, snapshot, print_paths_as_posix, ): project_name = "myapp" - result = runner.invoke_json( - ["app", "init", project_name], - env=TEST_ENV, - ) - assert result.exit_code == 0 - - with pushd(Path(os.getcwd(), project_name)): + with project_directory(f"napp_init_{definition_version}"): # sync only two specific files to stage result = runner.invoke_with_connection( [ @@ -258,21 +249,17 @@ def test_nativeapp_deploy_files( # Tests that files inside of a symlinked directory are deployed @pytest.mark.integration +@enable_definition_v2_feature_flag +@pytest.mark.parametrize("definition_version", ["v1", "v2"]) def test_nativeapp_deploy_nested_directories( + definition_version, + project_directory, runner, - temporary_working_directory, snapshot, print_paths_as_posix, ): project_name = "myapp" - project_dir = "app root" - result = runner.invoke_json( - ["app", "init", project_dir, "--name", project_name], - env=TEST_ENV, - ) - assert result.exit_code == 0 - - with pushd(Path(os.getcwd(), project_dir)): + with project_directory(f"napp_init_{definition_version}"): # create nested file under app/ touch("app/nested/dir/file.txt") @@ -312,19 +299,15 @@ def test_nativeapp_deploy_nested_directories( # Tests that deploying a directory recursively syncs all of its contents @pytest.mark.integration +@enable_definition_v2_feature_flag +@pytest.mark.parametrize("definition_version", ["v1", "v2"]) def test_nativeapp_deploy_directory( + definition_version, + project_directory, runner, - temporary_working_directory, ): project_name = "myapp" - project_dir = "app root" - result = runner.invoke_json( - ["app", "init", project_dir, "--name", project_name], - env=TEST_ENV, - ) - assert result.exit_code == 0 - - with pushd(Path(os.getcwd(), project_dir)): + with project_directory(f"napp_init_{definition_version}"): touch("app/dir/file.txt") result = runner.invoke_with_connection( ["app", "deploy", "app/dir", "--no-recursive", "--no-validate"], @@ -367,19 +350,14 @@ def test_nativeapp_deploy_directory( # Tests that deploying a directory without specifying -r returns an error @pytest.mark.integration +@enable_definition_v2_feature_flag +@pytest.mark.parametrize("definition_version", ["v1", "v2"]) def test_nativeapp_deploy_directory_no_recursive( + definition_version, + project_directory, runner, - temporary_working_directory, ): - project_name = "myapp" - project_dir = "app root" - result = runner.invoke_json( - ["app", "init", project_dir, "--name", project_name], - env=TEST_ENV, - ) - assert result.exit_code == 0 - - with pushd(Path(os.getcwd(), project_dir)): + with project_directory(f"napp_init_{definition_version}"): try: touch("app/nested/dir/file.txt") result = runner.invoke_with_connection_json( @@ -399,19 +377,14 @@ def test_nativeapp_deploy_directory_no_recursive( # Tests that specifying an unknown path to deploy results in an error @pytest.mark.integration +@enable_definition_v2_feature_flag +@pytest.mark.parametrize("definition_version", ["v1", "v2"]) def test_nativeapp_deploy_unknown_path( + definition_version, + project_directory, runner, - temporary_working_directory, ): - project_name = "myapp" - project_dir = "app root" - result = runner.invoke_json( - ["app", "init", project_dir, "--name", project_name], - env=TEST_ENV, - ) - assert result.exit_code == 0 - - with pushd(Path(os.getcwd(), project_dir)): + with project_directory(f"napp_init_{definition_version}"): try: result = runner.invoke_with_connection_json( ["app", "deploy", "does_not_exist", "--no-validate"], @@ -431,19 +404,14 @@ def test_nativeapp_deploy_unknown_path( # Tests that specifying a path with no deploy artifact results in an error @pytest.mark.integration +@enable_definition_v2_feature_flag +@pytest.mark.parametrize("definition_version", ["v1", "v2"]) def test_nativeapp_deploy_path_with_no_mapping( + definition_version, + project_directory, runner, - temporary_working_directory, ): - project_name = "myapp" - project_dir = "app root" - result = runner.invoke_json( - ["app", "init", project_dir, "--name", project_name], - env=TEST_ENV, - ) - assert result.exit_code == 0 - - with pushd(Path(os.getcwd(), project_dir)): + with project_directory(f"napp_init_{definition_version}"): try: result = runner.invoke_with_connection_json( ["app", "deploy", "snowflake.yml", "--no-validate"], @@ -463,19 +431,14 @@ def test_nativeapp_deploy_path_with_no_mapping( # Tests that specifying a path and pruning result in an error @pytest.mark.integration +@enable_definition_v2_feature_flag +@pytest.mark.parametrize("definition_version", ["v1", "v2"]) def test_nativeapp_deploy_rejects_pruning_when_path_is_specified( + definition_version, + project_directory, runner, - temporary_working_directory, ): - project_name = "myapp" - project_dir = "app root" - result = runner.invoke_json( - ["app", "init", project_dir, "--name", project_name], - env=TEST_ENV, - ) - assert result.exit_code == 0 - - with pushd(Path(os.getcwd(), project_dir)): + with project_directory(f"napp_init_{definition_version}"): try: os.unlink("app/README.md") result = runner.invoke_with_connection_json( @@ -498,39 +461,19 @@ def test_nativeapp_deploy_rejects_pruning_when_path_is_specified( # Tests that specifying a path with no direct mapping falls back to search for prefix matches @pytest.mark.integration +@enable_definition_v2_feature_flag +@pytest.mark.parametrize("definition_version", ["v1", "v2"]) def test_nativeapp_deploy_looks_for_prefix_matches( + definition_version, + project_directory, runner, - temporary_working_directory, snapshot, print_paths_as_posix, ): project_name = "myapp" - project_dir = "app root" - result = runner.invoke_json( - ["app", "init", project_dir, "--name", project_name], - env=TEST_ENV, - ) - assert result.exit_code == 0 - project_dir = Path(os.getcwd(), project_dir) - with pushd(project_dir): + with project_directory(f"napp_deploy_prefix_matches_{definition_version}"): try: - snowflake_yml = project_dir / "snowflake.yml" - project_definition_file = yaml.load( - snowflake_yml.read_text(), yaml.BaseLoader - ) - project_definition_file["native_app"]["artifacts"].append("src") - project_definition_file["native_app"]["artifacts"].append( - {"src": "lib/parent", "dest": "parent-lib"} - ) - snowflake_yml.write_text(yaml.dump(project_definition_file)) - - touch(str(project_dir / "src/main.py")) - - touch(str(project_dir / "lib/parent/child/a.py")) - touch(str(project_dir / "lib/parent/child/b.py")) - touch(str(project_dir / "lib/parent/child/c/c.py")) - result = runner.invoke_with_connection( ["app", "deploy", "-r", "app"], env=TEST_ENV, @@ -623,21 +566,17 @@ def test_nativeapp_deploy_looks_for_prefix_matches( # Tests that snow app deploy -r . deploys all changes @pytest.mark.integration +@enable_definition_v2_feature_flag +@pytest.mark.parametrize("definition_version", ["v1", "v2"]) def test_nativeapp_deploy_dot( + definition_version, + project_directory, runner, - temporary_working_directory, snapshot, print_paths_as_posix, ): project_name = "myapp" - project_dir = "app root" - result = runner.invoke_json( - ["app", "init", project_dir, "--name", project_name], - env=TEST_ENV, - ) - assert result.exit_code == 0 - - with pushd(Path(os.getcwd(), project_dir)): + with project_directory(f"napp_init_{definition_version}"): try: result = runner.invoke_with_connection( ["app", "deploy", "-r", "."], diff --git a/tests_integration/nativeapp/test_teardown.py b/tests_integration/nativeapp/test_teardown.py index 9ab288305..51f9cef64 100644 --- a/tests_integration/nativeapp/test_teardown.py +++ b/tests_integration/nativeapp/test_teardown.py @@ -12,16 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os import uuid -from textwrap import dedent -from unittest import mock from snowflake.cli.api.project.util import generate_user_env from tests.project.fixtures import * from tests_integration.test_utils import ( - pushd, contains_row_with, not_contains_row_with, row_from_snowflake_session, diff --git a/tests_integration/nativeapp/test_validate.py b/tests_integration/nativeapp/test_validate.py index aaabf68dd..73c69a207 100644 --- a/tests_integration/nativeapp/test_validate.py +++ b/tests_integration/nativeapp/test_validate.py @@ -12,13 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os import uuid from snowflake.cli.api.project.util import generate_user_env from tests.project.fixtures import * from tests_integration.test_utils import ( - pushd, + enable_definition_v2_feature_flag, ) USER_NAME = f"user_{uuid.uuid4().hex}" @@ -26,15 +25,10 @@ @pytest.mark.integration -def test_nativeapp_validate(runner, temporary_working_directory): - project_name = "myapp" - result = runner.invoke_json( - ["app", "init", project_name], - env=TEST_ENV, - ) - assert result.exit_code == 0, result.output - - with pushd(Path(os.getcwd(), project_name)): +@enable_definition_v2_feature_flag +@pytest.mark.parametrize("definition_version", ["v1", "v2"]) +def test_nativeapp_validate(definition_version, project_directory, runner): + with project_directory(f"napp_init_{definition_version}"): try: # validate the app's setup script result = runner.invoke_with_connection( @@ -52,15 +46,10 @@ def test_nativeapp_validate(runner, temporary_working_directory): @pytest.mark.integration -def test_nativeapp_validate_failing(runner, temporary_working_directory): - project_name = "myapp" - result = runner.invoke_json( - ["app", "init", project_name], - env=TEST_ENV, - ) - assert result.exit_code == 0, result.output - - with pushd(Path(os.getcwd(), project_name)): +@enable_definition_v2_feature_flag +@pytest.mark.parametrize("definition_version", ["v1", "v2"]) +def test_nativeapp_validate_failing(definition_version, project_directory, runner): + with project_directory(f"napp_init_{definition_version}"): # Create invalid SQL file Path("app/setup_script.sql").write_text("Lorem ipsum dolor sit amet") diff --git a/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/app/README.md b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/app/README.md new file mode 100644 index 000000000..f66bf75c9 --- /dev/null +++ b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/app/README.md @@ -0,0 +1,4 @@ +# README + +This directory contains an extremely simple application that is used for +integration testing SnowCLI. diff --git a/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/app/manifest.yml b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/app/manifest.yml new file mode 100644 index 000000000..5b8ef74e8 --- /dev/null +++ b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/app/manifest.yml @@ -0,0 +1,9 @@ +# This is a manifest.yml file, a required component of creating a Snowflake Native App. +# This file defines properties required by the application package, including the location of the setup script and version definitions. +# Refer to https://docs.snowflake.com/en/developer-guide/native-apps/creating-manifest for a detailed understanding of this file. + +manifest_version: 1 + +artifacts: + setup_script: setup_script.sql + readme: README.md diff --git a/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/app/setup_script.sql b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/app/setup_script.sql new file mode 100644 index 000000000..7fc3682b6 --- /dev/null +++ b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/app/setup_script.sql @@ -0,0 +1,11 @@ +-- This is the setup script that runs while installing a Snowflake Native App in a consumer account. +-- To write this script, you can familiarize yourself with some of the following concepts: +-- Application Roles +-- Versioned Schemas +-- UDFs/Procs +-- Extension Code +-- Refer to https://docs.snowflake.com/en/developer-guide/native-apps/creating-setup-script for a detailed understanding of this file. + +CREATE OR ALTER VERSIONED SCHEMA core; + +-- The rest of this script is left blank for purposes of your learning and exploration. diff --git a/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/lib/parent/child/a.py b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/lib/parent/child/a.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/lib/parent/child/b.py b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/lib/parent/child/b.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/lib/parent/child/c/c.py b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/lib/parent/child/c/c.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/snowflake.yml b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/snowflake.yml new file mode 100644 index 000000000..01b20a8f9 --- /dev/null +++ b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/snowflake.yml @@ -0,0 +1,10 @@ +definition_version: 1 +native_app: + name: myapp + source_stage: app_src.stage + artifacts: + - src: app/* + dest: ./ + - src + - src: lib/parent + dest: parent-lib diff --git a/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/src/main.py b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v1/src/main.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/app/README.md b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/app/README.md new file mode 100644 index 000000000..6a446bcf5 --- /dev/null +++ b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/app/README.md @@ -0,0 +1,3 @@ +# README + +This is the v2 version of the napp_deploy_prefix_matches_v1 project diff --git a/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/app/manifest.yml b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/app/manifest.yml new file mode 100644 index 000000000..1b444dab0 --- /dev/null +++ b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/app/manifest.yml @@ -0,0 +1,7 @@ +# This is the v2 version of the napp_deploy_prefix_matches_v1 project + +manifest_version: 1 + +artifacts: + setup_script: setup_script.sql + readme: README.md diff --git a/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/app/setup_script.sql b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/app/setup_script.sql new file mode 100644 index 000000000..352e2b23b --- /dev/null +++ b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/app/setup_script.sql @@ -0,0 +1,3 @@ +-- This is the v2 version of the napp_deploy_prefix_matches_v1 project + +CREATE OR ALTER VERSIONED SCHEMA core; diff --git a/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/lib/parent/child/a.py b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/lib/parent/child/a.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/lib/parent/child/b.py b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/lib/parent/child/b.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/lib/parent/child/c/c.py b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/lib/parent/child/c/c.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/snowflake.yml b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/snowflake.yml new file mode 100644 index 000000000..966b0fb35 --- /dev/null +++ b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/snowflake.yml @@ -0,0 +1,19 @@ +# This is the v2 version of the napp_deploy_prefix_matches_v1 project definition + +definition_version: 2 +entities: + pkg: + type: application package + name: myapp_pkg_<% ctx.env.USER %> + artifacts: + - src: app/* + dest: ./ + - src + - src: lib/parent + dest: parent-lib + manifest: app/manifest.yml + app: + type: application + name: myapp_<% ctx.env.USER %> + from: + target: pkg diff --git a/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/src/main.py b/tests_integration/test_data/projects/napp_deploy_prefix_matches_v2/src/main.py new file mode 100644 index 000000000..e69de29bb