From 709177648fe8ce4ee22cc18a6c65ead6038b873e Mon Sep 17 00:00:00 2001 From: Francois Campbell Date: Fri, 20 Sep 2024 17:05:35 -0400 Subject: [PATCH] SNOW-1643309 Move version create to app package entity (#1585) Moves implementation of `snow app version create` to `ApplicationPackageEntity.version_drop` and wires it into `snow ws version create` too. --- .../_plugins/nativeapp/application_entity.py | 46 +- .../nativeapp/application_package_entity.py | 422 ++++++++++++++- .../cli/_plugins/nativeapp/manager.py | 1 + .../cli/_plugins/nativeapp/run_processor.py | 2 +- .../_plugins/nativeapp/version/commands.py | 24 +- .../nativeapp/version/version_processor.py | 260 ++-------- .../cli/_plugins/workspace/commands.py | 47 ++ src/snowflake/cli/api/entities/common.py | 1 + tests/nativeapp/test_run_processor.py | 8 +- ...te_processor.py => test_version_create.py} | 489 +++++------------- tests/nativeapp/utils.py | 5 +- 11 files changed, 645 insertions(+), 660 deletions(-) rename tests/nativeapp/{test_version_create_processor.py => test_version_create.py} (54%) diff --git a/src/snowflake/cli/_plugins/nativeapp/application_entity.py b/src/snowflake/cli/_plugins/nativeapp/application_entity.py index 14c9793e92..ad4d0b9277 100644 --- a/src/snowflake/cli/_plugins/nativeapp/application_entity.py +++ b/src/snowflake/cli/_plugins/nativeapp/application_entity.py @@ -24,9 +24,7 @@ COMMENT_COL, NAME_COL, OWNER_COL, - PATCH_COL, SPECIAL_COMMENT, - VERSION_COL, ) from snowflake.cli._plugins.nativeapp.exceptions import ( ApplicationPackageDoesNotExistError, @@ -59,16 +57,11 @@ NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS, ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS, ) -from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError from snowflake.cli.api.project.schemas.entities.common import PostDeployHook from snowflake.cli.api.project.util import ( extract_schema, - identifier_to_show_like_pattern, - unquote_identifier, ) -from snowflake.cli.api.utils.cursor import find_all_rows from snowflake.connector import ProgrammingError -from snowflake.connector.cursor import DictCursor # Reasons why an `alter application ... upgrade` might fail UPGRADE_RESTRICTION_CODES = { @@ -423,7 +416,7 @@ def deploy( # versioned dev if version: try: - version_exists = cls.get_existing_version_info( + version_exists = ApplicationPackageEntity.get_existing_version_info( version=version, package_name=package_name, package_role=package_role, @@ -656,40 +649,3 @@ def get_existing_app_info( return sql_executor.show_specific_object( "applications", app_name, name_col=NAME_COL ) - - @staticmethod - def get_existing_version_info( - version: str, - package_name: str, - package_role: str, - ) -> Optional[dict]: - """ - Get the latest patch on an existing version by name in the application package. - Executes 'show versions like ... in application package' query and returns - the latest patch in the version as a single row, if one exists. Otherwise, - returns None. - """ - sql_executor = get_sql_executor() - with sql_executor.use_role(package_role): - try: - query = f"show versions like {identifier_to_show_like_pattern(version)} in application package {package_name}" - cursor = sql_executor.execute_query(query, cursor_class=DictCursor) - - if cursor.rowcount is None: - raise SnowflakeSQLExecutionError(query) - - matching_rows = find_all_rows( - cursor, lambda row: row[VERSION_COL] == unquote_identifier(version) - ) - - if not matching_rows: - return None - - return max(matching_rows, key=lambda row: row[PATCH_COL]) - - except ProgrammingError as err: - if err.msg.__contains__("does not exist or not authorized"): - raise ApplicationPackageDoesNotExistError(package_name) - else: - generic_sql_error_handler(err=err, role=package_role) - return None diff --git a/src/snowflake/cli/_plugins/nativeapp/application_package_entity.py b/src/snowflake/cli/_plugins/nativeapp/application_package_entity.py index 5476c5d911..7c7e33b317 100644 --- a/src/snowflake/cli/_plugins/nativeapp/application_package_entity.py +++ b/src/snowflake/cli/_plugins/nativeapp/application_package_entity.py @@ -5,11 +5,15 @@ from typing import Callable, List, Optional import typer -from click import ClickException +from click import BadOptionUsage, ClickException from snowflake.cli._plugins.nativeapp.application_package_entity_model import ( ApplicationPackageEntityModel, ) -from snowflake.cli._plugins.nativeapp.artifacts import build_bundle +from snowflake.cli._plugins.nativeapp.artifacts import ( + BundleMap, + build_bundle, + find_version_info_in_manifest_file, +) from snowflake.cli._plugins.nativeapp.bundle_context import BundleContext from snowflake.cli._plugins.nativeapp.codegen.compiler import NativeAppCompiler from snowflake.cli._plugins.nativeapp.constants import ( @@ -19,7 +23,9 @@ INTERNAL_DISTRIBUTION, NAME_COL, OWNER_COL, + PATCH_COL, SPECIAL_COMMENT, + VERSION_COL, ) from snowflake.cli._plugins.nativeapp.exceptions import ( ApplicationPackageAlreadyExistsError, @@ -56,10 +62,16 @@ from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError from snowflake.cli.api.project.schemas.entities.common import PostDeployHook from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import PathMapping -from snowflake.cli.api.project.util import extract_schema +from snowflake.cli.api.project.util import ( + extract_schema, + identifier_to_show_like_pattern, + to_identifier, + unquote_identifier, +) from snowflake.cli.api.rendering.jinja import ( get_basic_jinja_env, ) +from snowflake.cli.api.utils.cursor import find_all_rows from snowflake.connector import ProgrammingError from snowflake.connector.cursor import DictCursor, SnowflakeCursor @@ -110,6 +122,7 @@ def action_deploy( bundle_root=Path(model.bundle_root), generated_root=Path(model.generated_root), artifacts=model.artifacts, + bundle_map=None, package_name=package_name, package_role=(model.meta and model.meta.role) or ctx.default_role, package_distribution=model.distribution, @@ -160,7 +173,7 @@ def deploy_to_scratch_stage_fn(): recursive=True, paths=[], validate=False, - stage_fqn=model.scratch_stage, + stage_fqn=f"{package_name}.{model.scratch_stage}", interactive=interactive, force=force, ) @@ -171,7 +184,7 @@ def deploy_to_scratch_stage_fn(): package_role=package_role, stage_fqn=stage_fqn, use_scratch_stage=True, - scratch_stage_fqn=model.scratch_stage, + scratch_stage_fqn=f"{package_name}.{model.scratch_stage}", deploy_to_scratch_stage_fn=deploy_to_scratch_stage_fn, ) ctx.console.message("Setup script is valid") @@ -185,6 +198,47 @@ def action_version_list( package_role=(model.meta and model.meta.role) or ctx.default_role, ) + def action_version_create( + self, + ctx: ActionContext, + version: Optional[str], + patch: Optional[int], + skip_git_check: bool, + interactive: bool, + force: bool, + *args, + **kwargs, + ): + model = self._entity_model + package_name = model.fqn.identifier + return self.version_create( + console=ctx.console, + project_root=ctx.project_root, + deploy_root=Path(model.deploy_root), + bundle_root=Path(model.bundle_root), + generated_root=Path(model.generated_root), + artifacts=model.artifacts, + package_name=package_name, + package_role=(model.meta and model.meta.role) or ctx.default_role, + package_distribution=model.distribution, + prune=True, + recursive=True, + paths=None, + print_diff=True, + validate=True, + stage_fqn=f"{package_name}.{model.stage}", + package_warehouse=( + (model.meta and model.meta.warehouse) or ctx.default_warehouse + ), + post_deploy_hooks=model.meta and model.meta.post_deploy, + package_scripts=[], # Package scripts are not supported in PDFv2 + version=version, + patch=patch, + skip_git_check=skip_git_check, + force=force, + interactive=interactive, + ) + @staticmethod def bundle( project_root: Path, @@ -216,13 +270,14 @@ def deploy( bundle_root: Path, generated_root: Path, artifacts: list[PathMapping], + bundle_map: BundleMap | None, package_name: str, package_role: str, package_distribution: str, package_warehouse: str | None, prune: bool, recursive: bool, - paths: List[Path], + paths: List[Path] | None, print_diff: bool, validate: bool, stage_fqn: str, @@ -230,8 +285,8 @@ def deploy( package_scripts: List[str], policy: PolicyBase, ) -> DiffResult: - # 1. Create a bundle - bundle_map = cls.bundle( + # 1. Create a bundle if one wasn't passed in + bundle_map = bundle_map or cls.bundle( project_root=project_root, deploy_root=deploy_root, bundle_root=bundle_root, @@ -317,6 +372,357 @@ def version_list(package_name: str, package_role: str) -> SnowflakeCursor: return show_obj_cursor + @classmethod + def version_create( + cls, + console: AbstractConsole, + project_root: Path, + deploy_root: Path, + bundle_root: Path, + generated_root: Path, + artifacts: list[PathMapping], + package_name: str, + package_role: str, + package_distribution: str, + package_warehouse: str | None, + prune: bool, + recursive: bool, + paths: List[Path] | None, + print_diff: bool, + validate: bool, + stage_fqn: str, + post_deploy_hooks: list[PostDeployHook] | None, + package_scripts: List[str], + version: Optional[str], + patch: Optional[int], + force: bool, + interactive: bool, + skip_git_check: bool, + ): + """ + Perform bundle, application package creation, stage upload, version and/or patch to an application package. + """ + is_interactive = False + if force: + policy = AllowAlwaysPolicy() + elif interactive: + is_interactive = True + policy = AskAlwaysPolicy() + else: + policy = DenyAlwaysPolicy() + + if skip_git_check: + git_policy = DenyAlwaysPolicy() + else: + git_policy = AllowAlwaysPolicy() + + # Make sure version is not None before proceeding any further. + # This will raise an exception if version information is not found. Patch can be None. + bundle_map = None + if not version: + console.message( + dedent( + f"""\ + Version was not provided through the Snowflake CLI. Checking version in the manifest.yml instead. + This step will bundle your app artifacts to determine the location of the manifest.yml file. + """ + ) + ) + bundle_map = cls.bundle( + project_root=project_root, + deploy_root=deploy_root, + bundle_root=bundle_root, + generated_root=generated_root, + artifacts=artifacts, + package_name=package_name, + ) + version, patch = find_version_info_in_manifest_file(deploy_root) + if not version: + raise ClickException( + "Manifest.yml file does not contain a value for the version field." + ) + + # Check if --patch needs to throw a bad option error, either if application package does not exist or if version does not exist + if patch is not None: + try: + if not cls.get_existing_version_info( + version, package_name, package_role + ): + raise BadOptionUsage( + option_name="patch", + message=f"Cannot create a custom patch when version {version} is not defined in the application package {package_name}. Try again without using --patch.", + ) + except ApplicationPackageDoesNotExistError as app_err: + raise BadOptionUsage( + option_name="patch", + message=f"Cannot create a custom patch when application package {package_name} does not exist. Try again without using --patch.", + ) + + if git_policy.should_proceed(): + cls.check_index_changes_in_git_repo( + console=console, + project_root=project_root, + policy=policy, + is_interactive=is_interactive, + ) + + cls.deploy( + console=console, + project_root=project_root, + deploy_root=deploy_root, + bundle_root=bundle_root, + generated_root=generated_root, + artifacts=artifacts, + bundle_map=bundle_map, + package_name=package_name, + package_role=package_role, + package_distribution=package_distribution, + prune=prune, + recursive=recursive, + paths=paths, + print_diff=print_diff, + validate=validate, + stage_fqn=stage_fqn, + package_warehouse=package_warehouse, + post_deploy_hooks=post_deploy_hooks, + package_scripts=package_scripts, + policy=policy, + ) + + # Warn if the version exists in a release directive(s) + existing_release_directives = ( + cls.get_existing_release_directive_info_for_version( + package_name, package_role, version + ) + ) + + if existing_release_directives: + release_directive_names = ", ".join( + row["name"] for row in existing_release_directives + ) + console.warning( + dedent( + f"""\ + Version {version} already defined in application package {package_name} and in release directive(s): {release_directive_names}. + """ + ) + ) + + user_prompt = ( + f"Are you sure you want to create a new patch for version {version} in application " + f"package {package_name}? Once added, this operation cannot be undone." + ) + if not policy.should_proceed(user_prompt): + if is_interactive: + console.message("Not creating a new patch.") + raise typer.Exit(0) + else: + console.message( + "Cannot create a new patch non-interactively without --force." + ) + raise typer.Exit(1) + + # Define a new version in the application package + if not cls.get_existing_version_info(version, package_name, package_role): + cls.add_new_version( + console=console, + package_name=package_name, + package_role=package_role, + stage_fqn=stage_fqn, + version=version, + ) + return # A new version created automatically has patch 0, we do not need to further increment the patch. + + # Add a new patch to an existing (old) version + cls.add_new_patch_to_version( + console=console, + package_name=package_name, + package_role=package_role, + stage_fqn=stage_fqn, + version=version, + patch=patch, + ) + + @staticmethod + def get_existing_version_info( + version: str, + package_name: str, + package_role: str, + ) -> Optional[dict]: + """ + Get the latest patch on an existing version by name in the application package. + Executes 'show versions like ... in application package' query and returns + the latest patch in the version as a single row, if one exists. Otherwise, + returns None. + """ + sql_executor = get_sql_executor() + with sql_executor.use_role(package_role): + try: + query = f"show versions like {identifier_to_show_like_pattern(version)} in application package {package_name}" + cursor = sql_executor.execute_query(query, cursor_class=DictCursor) + + if cursor.rowcount is None: + raise SnowflakeSQLExecutionError(query) + + matching_rows = find_all_rows( + cursor, lambda row: row[VERSION_COL] == unquote_identifier(version) + ) + + if not matching_rows: + return None + + return max(matching_rows, key=lambda row: row[PATCH_COL]) + + except ProgrammingError as err: + if err.msg.__contains__("does not exist or not authorized"): + raise ApplicationPackageDoesNotExistError(package_name) + else: + generic_sql_error_handler(err=err, role=package_role) + return None + + @classmethod + def get_existing_release_directive_info_for_version( + cls, + package_name: str, + package_role: str, + version: str, + ) -> List[dict]: + """ + Get all existing release directives, if present, set on the version defined in an application package. + It executes a 'show release directives in application package' query and returns the filtered results, if they exist. + """ + sql_executor = get_sql_executor() + with sql_executor.use_role(package_role): + show_obj_query = ( + f"show release directives in application package {package_name}" + ) + show_obj_cursor = sql_executor.execute_query( + show_obj_query, cursor_class=DictCursor + ) + + if show_obj_cursor.rowcount is None: + raise SnowflakeSQLExecutionError(show_obj_query) + + show_obj_rows = find_all_rows( + show_obj_cursor, + lambda row: row[VERSION_COL] == unquote_identifier(version), + ) + + return show_obj_rows + + @classmethod + def add_new_version( + cls, + console: AbstractConsole, + package_name: str, + package_role: str, + stage_fqn: str, + version: str, + ) -> None: + """ + Defines a new version in an existing application package. + """ + # Make the version a valid identifier, adding quotes if necessary + version = to_identifier(version) + sql_executor = get_sql_executor() + with sql_executor.use_role(package_role): + console.step( + f"Defining a new version {version} in application package {package_name}" + ) + add_version_query = dedent( + f"""\ + alter application package {package_name} + add version {version} + using @{stage_fqn} + """ + ) + sql_executor.execute_query(add_version_query, cursor_class=DictCursor) + console.message( + f"Version {version} created for application package {package_name}." + ) + + @classmethod + def add_new_patch_to_version( + cls, + console: AbstractConsole, + package_name: str, + package_role: str, + stage_fqn: str, + version: str, + patch: Optional[int] = None, + ): + """ + Add a new patch, optionally a custom one, to an existing version in an application package. + """ + # Make the version a valid identifier, adding quotes if necessary + version = to_identifier(version) + sql_executor = get_sql_executor() + with sql_executor.use_role(package_role): + console.step( + f"Adding new patch to version {version} defined in application package {package_name}" + ) + add_version_query = dedent( + f"""\ + alter application package {package_name} + add patch {patch if patch else ""} for version {version} + using @{stage_fqn} + """ + ) + result_cursor = sql_executor.execute_query( + add_version_query, cursor_class=DictCursor + ) + + show_row = result_cursor.fetchall()[0] + new_patch = show_row["patch"] + console.message( + f"Patch {new_patch} created for version {version} defined in application package {package_name}." + ) + + @classmethod + def check_index_changes_in_git_repo( + cls, + console: AbstractConsole, + project_root: Path, + policy: PolicyBase, + is_interactive: bool, + ) -> None: + """ + Checks if the project root, i.e. the native apps project is a git repository. If it is a git repository, + it also checks if there any local changes to the directory that may not be on the application package stage. + """ + + from git import Repo + from git.exc import InvalidGitRepositoryError + + try: + repo = Repo(project_root, search_parent_directories=True) + assert repo.git_dir is not None + + # Check if the repo has any changes, including untracked files + if repo.is_dirty(untracked_files=True): + console.warning( + "Changes detected in the git repository. " + "(Rerun your command with --skip-git-check flag to ignore this check)" + ) + repo.git.execute(["git", "status"]) + + user_prompt = ( + "You have local changes in this repository that are not part of a previous commit. " + "Do you still want to continue?" + ) + if not policy.should_proceed(user_prompt): + if is_interactive: + console.message("Not creating a new version.") + raise typer.Exit(0) + else: + console.message( + "Cannot create a new version non-interactively without --force." + ) + raise typer.Exit(1) + + except InvalidGitRepositoryError: + pass # not a git repository, which is acceptable + @staticmethod def get_existing_app_pkg_info( package_name: str, diff --git a/src/snowflake/cli/_plugins/nativeapp/manager.py b/src/snowflake/cli/_plugins/nativeapp/manager.py index eb5bde56f2..e7ddc8cea5 100644 --- a/src/snowflake/cli/_plugins/nativeapp/manager.py +++ b/src/snowflake/cli/_plugins/nativeapp/manager.py @@ -320,6 +320,7 @@ def deploy( bundle_root=self.bundle_root, generated_root=self.generated_root, artifacts=self.artifacts, + bundle_map=bundle_map, package_name=self.package_name, package_role=self.package_role, package_distribution=self.package_distribution, diff --git a/src/snowflake/cli/_plugins/nativeapp/run_processor.py b/src/snowflake/cli/_plugins/nativeapp/run_processor.py index 7f93ee6767..32520c3a28 100644 --- a/src/snowflake/cli/_plugins/nativeapp/run_processor.py +++ b/src/snowflake/cli/_plugins/nativeapp/run_processor.py @@ -56,7 +56,7 @@ def get_all_existing_versions(self) -> SnowflakeCursor: ) def get_existing_version_info(self, version: str) -> Optional[dict]: - return ApplicationEntity.get_existing_version_info( + return ApplicationPackageEntity.get_existing_version_info( version=version, package_name=self.package_name, package_role=self.package_role, diff --git a/src/snowflake/cli/_plugins/nativeapp/version/commands.py b/src/snowflake/cli/_plugins/nativeapp/version/commands.py index 8a3dcc61e5..f593943ed0 100644 --- a/src/snowflake/cli/_plugins/nativeapp/version/commands.py +++ b/src/snowflake/cli/_plugins/nativeapp/version/commands.py @@ -82,35 +82,17 @@ def create( if version is None and patch is not None: raise MissingParameter("Cannot provide a patch without version!") - is_interactive = False - if force: - policy = AllowAlwaysPolicy() - elif interactive: - is_interactive = True - policy = AskAlwaysPolicy() - else: - policy = DenyAlwaysPolicy() - - if skip_git_check: - git_policy = DenyAlwaysPolicy() - else: - git_policy = AllowAlwaysPolicy() - cli_context = get_cli_context() processor = NativeAppVersionCreateProcessor( project_definition=cli_context.project_definition.native_app, project_root=cli_context.project_root, ) - - # We need build_bundle() to (optionally) find version in manifest.yml and create an application package - bundle_map = processor.build_bundle() processor.process( - bundle_map=bundle_map, version=version, patch=patch, - policy=policy, - git_policy=git_policy, - is_interactive=is_interactive, + force=force, + interactive=interactive, + skip_git_check=skip_git_check, ) return MessageResult(f"Version create is now complete.") diff --git a/src/snowflake/cli/_plugins/nativeapp/version/version_processor.py b/src/snowflake/cli/_plugins/nativeapp/version/version_processor.py index 0fcf7cea55..42da3a3336 100644 --- a/src/snowflake/cli/_plugins/nativeapp/version/version_processor.py +++ b/src/snowflake/cli/_plugins/nativeapp/version/version_processor.py @@ -16,17 +16,17 @@ from pathlib import Path from textwrap import dedent -from typing import Dict, List, Optional +from typing import Dict, Optional import typer -from click import BadOptionUsage, ClickException +from click import ClickException +from snowflake.cli._plugins.nativeapp.application_package_entity import ( + ApplicationPackageEntity, +) from snowflake.cli._plugins.nativeapp.artifacts import ( - BundleMap, find_version_info_in_manifest_file, ) -from snowflake.cli._plugins.nativeapp.constants import VERSION_COL from snowflake.cli._plugins.nativeapp.exceptions import ( - ApplicationPackageAlreadyExistsError, ApplicationPackageDoesNotExistError, ) from snowflake.cli._plugins.nativeapp.manager import ( @@ -36,244 +36,50 @@ from snowflake.cli._plugins.nativeapp.policy import PolicyBase from snowflake.cli._plugins.nativeapp.run_processor import NativeAppRunProcessor from snowflake.cli.api.console import cli_console as cc -from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError from snowflake.cli.api.project.schemas.v1.native_app.native_app import NativeApp -from snowflake.cli.api.project.util import to_identifier, unquote_identifier -from snowflake.cli.api.utils.cursor import ( - find_all_rows, -) +from snowflake.cli.api.project.util import to_identifier from snowflake.connector import ProgrammingError -from snowflake.connector.cursor import DictCursor - - -def check_index_changes_in_git_repo( - project_root: Path, policy: PolicyBase, is_interactive: bool -) -> None: - """ - Checks if the project root, i.e. the native apps project is a git repository. If it is a git repository, - it also checks if there any local changes to the directory that may not be on the application package stage. - """ - from git import Repo - from git.exc import InvalidGitRepositoryError - - try: - repo = Repo(project_root, search_parent_directories=True) - assert repo.git_dir is not None - - # Check if the repo has any changes, including untracked files - if repo.is_dirty(untracked_files=True): - cc.warning( - "Changes detected in the git repository. " - "(Rerun your command with --skip-git-check flag to ignore this check)" - ) - repo.git.execute(["git", "status"]) - - user_prompt = ( - "You have local changes in this repository that are not part of a previous commit. " - "Do you still want to continue?" - ) - if not policy.should_proceed(user_prompt): - if is_interactive: - cc.message("Not creating a new version.") - raise typer.Exit(0) - else: - cc.message( - "Cannot create a new version non-interactively without --force." - ) - raise typer.Exit(1) - - except InvalidGitRepositoryError: - pass # not a git repository, which is acceptable class NativeAppVersionCreateProcessor(NativeAppRunProcessor): def __init__(self, project_definition: Dict, project_root: Path): super().__init__(project_definition, project_root) - def get_existing_release_directive_info_for_version( - self, version: str - ) -> List[dict]: - """ - Get all existing release directives, if present, set on the version defined in an application package. - It executes a 'show release directives in application package' query and returns the filtered results, if they exist. - """ - with self.use_role(self.package_role): - show_obj_query = ( - f"show release directives in application package {self.package_name}" - ) - show_obj_cursor = self._execute_query( - show_obj_query, cursor_class=DictCursor - ) - - if show_obj_cursor.rowcount is None: - raise SnowflakeSQLExecutionError(show_obj_query) - - show_obj_rows = find_all_rows( - show_obj_cursor, - lambda row: row[VERSION_COL] == unquote_identifier(version), - ) - - return show_obj_rows - - def add_new_version(self, version: str) -> None: - """ - Defines a new version in an existing application package. - """ - # Make the version a valid identifier, adding quotes if necessary - version = to_identifier(version) - with self.use_role(self.package_role): - cc.step( - f"Defining a new version {version} in application package {self.package_name}" - ) - add_version_query = dedent( - f"""\ - alter application package {self.package_name} - add version {version} - using @{self.stage_fqn} - """ - ) - self._execute_query(add_version_query, cursor_class=DictCursor) - cc.message( - f"Version {version} created for application package {self.package_name}." - ) - - def add_new_patch_to_version(self, version: str, patch: Optional[int] = None): - """ - Add a new patch, optionally a custom one, to an existing version in an application package. - """ - # Make the version a valid identifier, adding quotes if necessary - version = to_identifier(version) - with self.use_role(self.package_role): - cc.step( - f"Adding new patch to version {version} defined in application package {self.package_name}" - ) - add_version_query = dedent( - f"""\ - alter application package {self.package_name} - add patch {patch if patch else ""} for version {version} - using @{self.stage_fqn} - """ - ) - result_cursor = self._execute_query( - add_version_query, cursor_class=DictCursor - ) - - show_row = result_cursor.fetchall()[0] - new_patch = show_row["patch"] - cc.message( - f"Patch {new_patch} created for version {version} defined in application package {self.package_name}." - ) - def process( self, - bundle_map: BundleMap, version: Optional[str], patch: Optional[int], - policy: PolicyBase, - git_policy: PolicyBase, - is_interactive: bool, + force: bool, + interactive: bool, + skip_git_check: bool, *args, **kwargs, ): - """ - Perform bundle, application package creation, stage upload, version and/or patch to an application package. - """ - - # Make sure version is not None before proceeding any further. - # This will raise an exception if version information is not found. Patch can be None. - if not version: - cc.message( - "Version was not provided through the Snowflake CLI. Checking version in the manifest.yml instead." - ) - - version, patch = find_version_info_in_manifest_file(self.deploy_root) - if not version: - raise ClickException( - "Manifest.yml file does not contain a value for the version field." - ) - - # Check if --patch needs to throw a bad option error, either if application package does not exist or if version does not exist - if patch is not None: - try: - if not self.get_existing_version_info(version): - raise BadOptionUsage( - option_name="patch", - message=f"Cannot create a custom patch when version {version} is not defined in the application package {self.package_name}. Try again without using --patch.", - ) - except ApplicationPackageDoesNotExistError as app_err: - raise BadOptionUsage( - option_name="patch", - message=f"Cannot create a custom patch when application package {self.package_name} does not exist. Try again without using --patch.", - ) - - if git_policy.should_proceed(): - check_index_changes_in_git_repo( - project_root=self.project_root, - policy=policy, - is_interactive=is_interactive, - ) - - # TODO: consider using self.deploy() instead - - try: - self.create_app_package() - except ApplicationPackageAlreadyExistsError as e: - cc.warning(e.message) - if not policy.should_proceed("Proceed with using this package?"): - raise typer.Abort() from e - - with self.use_role(self.package_role): - # Now that the application package exists, create shared data - self._apply_package_scripts() - - # Upload files from deploy root local folder to the above stage - self.sync_deploy_root_with_stage( - bundle_map=bundle_map, - role=self.package_role, - prune=True, - recursive=True, - stage_fqn=self.stage_fqn, - ) - with self.use_package_warehouse(): - self.execute_package_post_deploy_hooks() - - # Warn if the version exists in a release directive(s) - existing_release_directives = ( - self.get_existing_release_directive_info_for_version(version) + return ApplicationPackageEntity.version_create( + console=cc, + project_root=self.project_root, + deploy_root=self.deploy_root, + bundle_root=self.bundle_root, + generated_root=self.generated_root, + artifacts=self.artifacts, + package_name=self.package_name, + package_role=self.package_role, + package_distribution=self.package_distribution, + prune=True, + recursive=True, + paths=None, + print_diff=True, + validate=True, + stage_fqn=self.stage_fqn, + package_warehouse=self.package_warehouse, + post_deploy_hooks=self.package_post_deploy_hooks, + package_scripts=self.package_scripts, + version=version, + patch=patch, + force=force, + interactive=interactive, + skip_git_check=skip_git_check, ) - if existing_release_directives: - release_directive_names = ", ".join( - row["name"] for row in existing_release_directives - ) - cc.warning( - dedent( - f"""\ - Version {version} already defined in application package {self.package_name} and in release directive(s): {release_directive_names}. - """ - ) - ) - - user_prompt = ( - f"Are you sure you want to create a new patch for version {version} in application " - f"package {self.package_name}? Once added, this operation cannot be undone." - ) - if not policy.should_proceed(user_prompt): - if is_interactive: - cc.message("Not creating a new patch.") - raise typer.Exit(0) - else: - cc.message( - "Cannot create a new patch non-interactively without --force." - ) - raise typer.Exit(1) - - # Define a new version in the application package - if not self.get_existing_version_info(version): - self.add_new_version(version=version) - return # A new version created automatically has patch 0, we do not need to further increment the patch. - - # Add a new patch to an existing (old) version - self.add_new_patch_to_version(version=version, patch=patch) class NativeAppVersionDropProcessor(NativeAppManager, NativeAppCommandProcessor): diff --git a/src/snowflake/cli/_plugins/workspace/commands.py b/src/snowflake/cli/_plugins/workspace/commands.py index 0e45c3423b..6086c24eda 100644 --- a/src/snowflake/cli/_plugins/workspace/commands.py +++ b/src/snowflake/cli/_plugins/workspace/commands.py @@ -20,6 +20,7 @@ from typing import List, Optional import typer +from click import MissingParameter from snowflake.cli._plugins.nativeapp.artifacts import BundleMap from snowflake.cli._plugins.nativeapp.common_flags import ( ForceOption, @@ -224,3 +225,49 @@ def version_list( EntityActions.VERSION_LIST, ) return QueryResult(cursor) + + +@version.command(name="create", requires_connection=True, hidden=True) +@with_project_definition() +def version_create( + entity_id: str = typer.Option( + help="The ID of the entity you want to create a version for.", + ), + version: Optional[str] = typer.Argument( + None, + help=f"""Version to define in your application package. If the version already exists, an auto-incremented patch is added to the version instead. Defaults to the version specified in the `manifest.yml` file.""", + ), + patch: Optional[int] = typer.Option( + None, + "--patch", + help=f"""The patch number you want to create for an existing version. + Defaults to undefined if it is not set, which means the Snowflake CLI either uses the patch specified in the `manifest.yml` file or automatically generates a new patch number.""", + ), + skip_git_check: Optional[bool] = typer.Option( + False, + "--skip-git-check", + help="When enabled, the Snowflake CLI skips checking if your project has any untracked or stages files in git. Default: unset.", + is_flag=True, + ), + interactive: bool = InteractiveOption, + force: Optional[bool] = ForceOption, + **options, +): + """Creates a new version for the specified entity.""" + if version is None and patch is not None: + raise MissingParameter("Cannot provide a patch without version!") + + cli_context = get_cli_context() + ws = WorkspaceManager( + project_definition=cli_context.project_definition, + project_root=cli_context.project_root, + ) + ws.perform_action( + entity_id, + EntityActions.VERSION_CREATE, + version=version, + patch=patch, + skip_git_check=skip_git_check, + interactive=interactive, + force=force, + ) diff --git a/src/snowflake/cli/api/entities/common.py b/src/snowflake/cli/api/entities/common.py index a05fe3f3c0..54becc7d2f 100644 --- a/src/snowflake/cli/api/entities/common.py +++ b/src/snowflake/cli/api/entities/common.py @@ -12,6 +12,7 @@ class EntityActions(str, Enum): VALIDATE = "action_validate" VERSION_LIST = "action_version_list" + VERSION_CREATE = "action_version_create" T = TypeVar("T") diff --git a/tests/nativeapp/test_run_processor.py b/tests/nativeapp/test_run_processor.py index 38ebc57beb..93698ade60 100644 --- a/tests/nativeapp/test_run_processor.py +++ b/tests/nativeapp/test_run_processor.py @@ -57,7 +57,7 @@ ) from tests.nativeapp.utils import ( APP_ENTITY_GET_EXISTING_APP_INFO, - APP_ENTITY_GET_EXISTING_VERSION_INFO, + APP_PACKAGE_ENTITY_GET_EXISTING_VERSION_INFO, NATIVEAPP_MODULE, SQL_EXECUTOR_EXECUTE, TYPER_CONFIRM, @@ -1775,7 +1775,7 @@ def test_upgrade_app_recreate_app( # Test upgrade app method for version AND no existing version info @mock.patch( - APP_ENTITY_GET_EXISTING_VERSION_INFO, + APP_PACKAGE_ENTITY_GET_EXISTING_VERSION_INFO, return_value=None, ) @pytest.mark.parametrize( @@ -1804,7 +1804,7 @@ def test_upgrade_app_from_version_throws_usage_error_one( # Test upgrade app method for version AND no existing app package from version info @mock.patch( - APP_ENTITY_GET_EXISTING_VERSION_INFO, + APP_PACKAGE_ENTITY_GET_EXISTING_VERSION_INFO, side_effect=ApplicationPackageDoesNotExistError("app_pkg"), ) @pytest.mark.parametrize( @@ -1833,7 +1833,7 @@ def test_upgrade_app_from_version_throws_usage_error_two( # Test upgrade app method for version AND existing app info AND user wants to drop app AND drop succeeds AND app is created successfully @mock.patch( - APP_ENTITY_GET_EXISTING_VERSION_INFO, + APP_PACKAGE_ENTITY_GET_EXISTING_VERSION_INFO, return_value={"key": "val"}, ) @mock.patch(SQL_EXECUTOR_EXECUTE) diff --git a/tests/nativeapp/test_version_create_processor.py b/tests/nativeapp/test_version_create.py similarity index 54% rename from tests/nativeapp/test_version_create_processor.py rename to tests/nativeapp/test_version_create.py index 4d542956af..ccc98f8a36 100644 --- a/tests/nativeapp/test_version_create_processor.py +++ b/tests/nativeapp/test_version_create.py @@ -13,16 +13,17 @@ # limitations under the License. import os -from contextlib import nullcontext from textwrap import dedent from unittest import mock import pytest import typer from click import BadOptionUsage, ClickException +from snowflake.cli._plugins.nativeapp.application_package_entity import ( + ApplicationPackageEntity, +) from snowflake.cli._plugins.nativeapp.constants import SPECIAL_COMMENT from snowflake.cli._plugins.nativeapp.exceptions import ( - ApplicationPackageAlreadyExistsError, ApplicationPackageDoesNotExistError, ) from snowflake.cli._plugins.nativeapp.policy import ( @@ -33,13 +34,13 @@ from snowflake.cli._plugins.nativeapp.version.version_processor import ( NativeAppVersionCreateProcessor, ) +from snowflake.cli.api.console import cli_console as cc from snowflake.cli.api.project.definition_manager import DefinitionManager from snowflake.connector.cursor import DictCursor from tests.nativeapp.utils import ( - FIND_VERSION_FROM_MANIFEST, - NATIVEAPP_MANAGER_EXECUTE, - VERSION_MODULE, + APPLICATION_PACKAGE_ENTITY_MODULE, + SQL_EXECUTOR_EXECUTE, mock_execute_helper, mock_snowflake_yml_file, ) @@ -61,7 +62,7 @@ def _get_version_create_processor(): # Test get_existing_release_directive_info_for_version returns release directives info correctly -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) +@mock.patch(SQL_EXECUTOR_EXECUTE) def test_get_existing_release_direction_info(mock_execute, temp_dir, mock_cursor): version = "V1" side_effects, expected = mock_execute_helper( @@ -98,13 +99,17 @@ def test_get_existing_release_direction_info(mock_execute, temp_dir, mock_cursor ) processor = _get_version_create_processor() - result = processor.get_existing_release_directive_info_for_version(version) + result = ApplicationPackageEntity.get_existing_release_directive_info_for_version( + package_name=processor.package_name, + package_role=processor.package_role, + version=version, + ) assert mock_execute.mock_calls == expected assert len(result) == 2 # Test add_new_version adds a new version to an app pkg correctly -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) +@mock.patch(SQL_EXECUTOR_EXECUTE) @pytest.mark.parametrize( ["version", "version_identifier"], [("V1", "V1"), ("1.0.0", '"1.0.0"'), ('"1.0.0"', '"1.0.0"')], @@ -143,12 +148,18 @@ def test_add_version(mock_execute, temp_dir, mock_cursor, version, version_ident ) processor = _get_version_create_processor() - processor.add_new_version(version) + ApplicationPackageEntity.add_new_version( + console=cc, + package_name=processor.package_name, + package_role=processor.package_role, + stage_fqn=processor.stage_fqn, + version=version, + ) assert mock_execute.mock_calls == expected # Test add_new_patch_to_version adds an "auto-increment" patch to an existing version -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) +@mock.patch(SQL_EXECUTOR_EXECUTE) @pytest.mark.parametrize( ["version", "version_identifier"], [("V1", "V1"), ("1.0.0", '"1.0.0"'), ('"1.0.0"', '"1.0.0"')], @@ -189,12 +200,18 @@ def test_add_new_patch_auto( ) processor = _get_version_create_processor() - processor.add_new_patch_to_version(version) + ApplicationPackageEntity.add_new_patch_to_version( + console=cc, + package_name=processor.package_name, + package_role=processor.package_role, + stage_fqn=processor.stage_fqn, + version=version, + ) assert mock_execute.mock_calls == expected # Test add_new_patch_to_version adds a custom patch to an existing version -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) +@mock.patch(SQL_EXECUTOR_EXECUTE) @pytest.mark.parametrize( ["version", "version_identifier"], [("V1", "V1"), ("1.0.0", '"1.0.0"'), ('"1.0.0"', '"1.0.0"')], @@ -235,20 +252,36 @@ def test_add_new_patch_custom( ) processor = _get_version_create_processor() - processor.add_new_patch_to_version(version, 12) + ApplicationPackageEntity.add_new_patch_to_version( + console=cc, + package_name=processor.package_name, + package_role=processor.package_role, + stage_fqn=processor.stage_fqn, + version=version, + patch=12, + ) assert mock_execute.mock_calls == expected # Test version create when user did not pass in a version AND we could not find a version in the manifest file either -@mock.patch(FIND_VERSION_FROM_MANIFEST, return_value=(None, None)) -@pytest.mark.parametrize( - "policy_param", [allow_always_policy, ask_always_policy, deny_always_policy] +@mock.patch( + f"{APPLICATION_PACKAGE_ENTITY_MODULE}.ApplicationPackageEntity.bundle", + return_value=None, +) +@mock.patch( + f"{APPLICATION_PACKAGE_ENTITY_MODULE}.find_version_info_in_manifest_file", + return_value=(None, None), ) +@pytest.mark.parametrize("force", [True, False]) +@pytest.mark.parametrize("interactive", [True, False]) +@pytest.mark.parametrize("skip_git_check", [True, False]) def test_process_no_version_from_user_no_version_in_manifest( mock_version_info_in_manifest, - policy_param, + mock_bundle, + force, + interactive, + skip_git_check, temp_dir, - mock_bundle_map, ): current_working_directory = os.getcwd() create_named_file( @@ -260,28 +293,29 @@ def test_process_no_version_from_user_no_version_in_manifest( processor = _get_version_create_processor() with pytest.raises(ClickException): processor.process( - bundle_map=mock_bundle_map, version=None, patch=None, - policy=policy_param, - git_policy=policy_param, - is_interactive=False, + force=force, + interactive=interactive, + skip_git_check=skip_git_check, ) # last three parameters do not matter here, so it should succeed for all policies. mock_version_info_in_manifest.assert_called_once() # Test version create when user passed in a version and patch AND version does not exist in app package @mock.patch( - f"{VERSION_MODULE}.{CREATE_PROCESSOR}.get_existing_version_info", return_value=None -) -@pytest.mark.parametrize( - "policy_param", [allow_always_policy, ask_always_policy, deny_always_policy] + f"{APPLICATION_PACKAGE_ENTITY_MODULE}.ApplicationPackageEntity.get_existing_version_info", + return_value=None, ) +@pytest.mark.parametrize("force", [True, False]) +@pytest.mark.parametrize("interactive", [True, False]) +@pytest.mark.parametrize("skip_git_check", [True, False]) def test_process_no_version_exists_throws_bad_option_exception_one( mock_existing_version_info, - policy_param, + force, + interactive, + skip_git_check, temp_dir, - mock_bundle_map, ): current_working_directory = os.getcwd() create_named_file( @@ -293,28 +327,28 @@ def test_process_no_version_exists_throws_bad_option_exception_one( processor = _get_version_create_processor() with pytest.raises(BadOptionUsage): processor.process( - bundle_map=mock_bundle_map, version="v1", patch=12, - policy=policy_param, - git_policy=policy_param, - is_interactive=False, + force=force, + interactive=interactive, + skip_git_check=skip_git_check, ) # last three parameters do not matter here, so it should succeed for all policies. # Test version create when user passed in a version and patch AND app package does not exist @mock.patch( - f"{VERSION_MODULE}.{CREATE_PROCESSOR}.get_existing_version_info", + f"{APPLICATION_PACKAGE_ENTITY_MODULE}.ApplicationPackageEntity.get_existing_version_info", side_effect=ApplicationPackageDoesNotExistError("app_pkg"), ) -@pytest.mark.parametrize( - "policy_param", [allow_always_policy, ask_always_policy, deny_always_policy] -) +@pytest.mark.parametrize("force", [True, False]) +@pytest.mark.parametrize("interactive", [True, False]) +@pytest.mark.parametrize("skip_git_check", [True, False]) def test_process_no_version_exists_throws_bad_option_exception_two( mock_existing_version_info, - policy_param, + force, + interactive, + skip_git_check, temp_dir, - mock_bundle_map, ): current_working_directory = os.getcwd() create_named_file( @@ -326,77 +360,47 @@ def test_process_no_version_exists_throws_bad_option_exception_two( processor = _get_version_create_processor() with pytest.raises(BadOptionUsage): processor.process( - bundle_map=mock_bundle_map, version="v1", patch=12, - policy=policy_param, - git_policy=policy_param, - is_interactive=False, + force=force, + interactive=interactive, + skip_git_check=skip_git_check, ) # last three parameters do not matter here, so it should succeed for all policies. # Test version create when there are no release directives matching the version AND no version exists for app pkg -@mock.patch(FIND_VERSION_FROM_MANIFEST, return_value=("manifest_version", None)) -@mock.patch(f"{VERSION_MODULE}.check_index_changes_in_git_repo", return_value=None) -@mock.patch.object( - NativeAppVersionCreateProcessor, "create_app_package", return_value=None -) -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) -@mock.patch.object(NativeAppVersionCreateProcessor, "use_package_warehouse") -@mock.patch.object( - NativeAppVersionCreateProcessor, - "execute_package_post_deploy_hooks", - return_value=None, -) -@mock.patch.object( - NativeAppVersionCreateProcessor, "_apply_package_scripts", return_value=None +@mock.patch( + f"{APPLICATION_PACKAGE_ENTITY_MODULE}.find_version_info_in_manifest_file", + return_value=("manifest_version", None), ) @mock.patch.object( - NativeAppVersionCreateProcessor, "sync_deploy_root_with_stage", return_value=None + ApplicationPackageEntity, "check_index_changes_in_git_repo", return_value=None ) +@mock.patch.object(ApplicationPackageEntity, "deploy", return_value=None) @mock.patch.object( - NativeAppVersionCreateProcessor, + ApplicationPackageEntity, "get_existing_release_directive_info_for_version", return_value=None, ) @mock.patch.object( - NativeAppVersionCreateProcessor, "get_existing_version_info", return_value=None -) -@mock.patch.object( - NativeAppVersionCreateProcessor, "add_new_version", return_value=None -) -@pytest.mark.parametrize( - "policy_param", [allow_always_policy, ask_always_policy, deny_always_policy] + ApplicationPackageEntity, "get_existing_version_info", return_value=None ) +@mock.patch.object(ApplicationPackageEntity, "add_new_version", return_value=None) +@pytest.mark.parametrize("force", [True, False]) +@pytest.mark.parametrize("interactive", [True, False]) def test_process_no_existing_release_directives_or_versions( mock_add_new_version, mock_existing_version_info, mock_rd, - mock_sync, - mock_apply_package_scripts, - mock_execute_package_post_deploy_hooks, - mock_use_package_warehouse, - mock_execute, - mock_create_app_pkg, + mock_deploy, mock_check_git, mock_find_version, - policy_param, + force, + interactive, temp_dir, mock_cursor, - mock_bundle_map, ): version = "V1" - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - (None, mock.call("use role old_role")), - ] - ) - mock_execute.side_effect = side_effects current_working_directory = os.getcwd() create_named_file( @@ -407,77 +411,52 @@ def test_process_no_existing_release_directives_or_versions( processor = _get_version_create_processor() processor.process( - bundle_map=mock_bundle_map, version=version, patch=None, - policy=policy_param, - git_policy=allow_always_policy, - is_interactive=False, + force=force, + interactive=interactive, + skip_git_check=False, ) # last three parameters do not matter here - assert mock_execute.mock_calls == expected mock_find_version.assert_not_called() mock_check_git.assert_called_once() mock_rd.assert_called_once() - mock_create_app_pkg.assert_called_once() - mock_apply_package_scripts.assert_called_once() - mock_use_package_warehouse.assert_called_once(), - mock_execute_package_post_deploy_hooks.assert_called_once(), - mock_sync.assert_called_once() + mock_deploy.assert_called_once() mock_existing_version_info.assert_called_once() mock_add_new_version.assert_called_once() # Test version create when there are no release directives matching the version AND a version exists for app pkg @mock.patch( - "snowflake.cli._plugins.nativeapp.artifacts.find_version_info_in_manifest_file" -) -@mock.patch(f"{VERSION_MODULE}.check_index_changes_in_git_repo", return_value=None) -@mock.patch.object( - NativeAppVersionCreateProcessor, "create_app_package", return_value=None + f"{APPLICATION_PACKAGE_ENTITY_MODULE}.find_version_info_in_manifest_file", ) -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) -@mock.patch.object(NativeAppVersionCreateProcessor, "use_package_warehouse") @mock.patch.object( - NativeAppVersionCreateProcessor, - "execute_package_post_deploy_hooks", - return_value=None, + ApplicationPackageEntity, "check_index_changes_in_git_repo", return_value=None ) +@mock.patch.object(ApplicationPackageEntity, "deploy", return_value=None) @mock.patch.object( - NativeAppVersionCreateProcessor, "_apply_package_scripts", return_value=None -) -@mock.patch.object( - NativeAppVersionCreateProcessor, "sync_deploy_root_with_stage", return_value=None -) -@mock.patch.object( - NativeAppVersionCreateProcessor, + ApplicationPackageEntity, "get_existing_release_directive_info_for_version", return_value=None, ) -@mock.patch.object(NativeAppVersionCreateProcessor, "get_existing_version_info") -@mock.patch.object(NativeAppVersionCreateProcessor, "add_new_version") +@mock.patch.object(ApplicationPackageEntity, "get_existing_version_info") +@mock.patch.object(ApplicationPackageEntity, "add_new_version") @mock.patch.object( - NativeAppVersionCreateProcessor, "add_new_patch_to_version", return_value=None -) -@pytest.mark.parametrize( - "policy_param", [allow_always_policy, ask_always_policy, deny_always_policy] + ApplicationPackageEntity, "add_new_patch_to_version", return_value=None ) +@pytest.mark.parametrize("force", [True, False]) +@pytest.mark.parametrize("interactive", [True, False]) def test_process_no_existing_release_directives_w_existing_version( mock_add_patch, mock_add_new_version, mock_existing_version_info, mock_rd, - mock_sync, - mock_apply_package_scripts, - mock_execute_package_post_deploy_hooks, - mock_use_package_warehouse, - mock_execute, - mock_create_app_pkg, + mock_deploy, mock_check_git, mock_find_version, - policy_param, + force, + interactive, temp_dir, mock_cursor, - mock_bundle_map, ): version = "V1" mock_existing_version_info.return_value = { @@ -486,17 +465,6 @@ def test_process_no_existing_release_directives_w_existing_version( "owner": "PACKAGE_ROLE", "version": version, } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - (None, mock.call("use role old_role")), - ] - ) - mock_execute.side_effect = side_effects current_working_directory = os.getcwd() create_named_file( @@ -507,22 +475,16 @@ def test_process_no_existing_release_directives_w_existing_version( processor = _get_version_create_processor() processor.process( - bundle_map=mock_bundle_map, version=version, patch=12, - policy=policy_param, - git_policy=allow_always_policy, - is_interactive=False, + force=force, + interactive=interactive, + skip_git_check=False, ) # last three parameters do not matter here - assert mock_execute.mock_calls == expected mock_find_version.assert_not_called() mock_check_git.assert_called_once() mock_rd.assert_called_once() - mock_create_app_pkg.assert_called_once() - mock_apply_package_scripts.assert_called_once() - mock_use_package_warehouse.assert_called_once(), - mock_execute_package_post_deploy_hooks.assert_called_once() - mock_sync.assert_called_once() + mock_deploy.assert_called_once() assert mock_existing_version_info.call_count == 2 mock_add_new_version.assert_not_called() mock_add_patch.assert_called_once() @@ -531,55 +493,34 @@ def test_process_no_existing_release_directives_w_existing_version( # Test version create when there are release directives matching the version AND no version exists for app pkg AND --force is False AND interactive mode is False AND --interactive is False # Test version create when there are release directives matching the version AND no version exists for app pkg AND --force is False AND interactive mode is False AND --interactive is True AND user does not want to proceed # Test version create when there are release directives matching the version AND no version exists for app pkg AND --force is False AND interactive mode is True AND user does not want to proceed -@mock.patch(f"{VERSION_MODULE}.check_index_changes_in_git_repo", return_value=None) @mock.patch.object( - NativeAppVersionCreateProcessor, "create_app_package", return_value=None + ApplicationPackageEntity, "check_index_changes_in_git_repo", return_value=None ) -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) -@mock.patch.object(NativeAppVersionCreateProcessor, "use_package_warehouse") +@mock.patch.object(ApplicationPackageEntity, "deploy", return_value=None) @mock.patch.object( - NativeAppVersionCreateProcessor, - "execute_package_post_deploy_hooks", - return_value=None, -) -@mock.patch.object( - NativeAppVersionCreateProcessor, "_apply_package_scripts", return_value=None -) -@mock.patch.object( - NativeAppVersionCreateProcessor, "sync_deploy_root_with_stage", return_value=None -) -@mock.patch.object( - NativeAppVersionCreateProcessor, + ApplicationPackageEntity, "get_existing_release_directive_info_for_version", return_value=None, ) @mock.patch.object(typer, "confirm", return_value=False) -@mock.patch.object(NativeAppVersionCreateProcessor, "get_existing_version_info") +@mock.patch.object(ApplicationPackageEntity, "get_existing_version_info") @pytest.mark.parametrize( - "policy_param, is_interactive_param, expected_code", + "interactive, expected_code", [ - (deny_always_policy, False, 1), - (ask_always_policy, True, 0), - (ask_always_policy, True, 0), + (False, 1), + (True, 0), ], ) def test_process_existing_release_directives_user_does_not_proceed( mock_existing_version_info, mock_typer_confirm, mock_rd, - mock_sync, - mock_apply_package_scripts, - mock_execute_package_post_deploy_hooks, - mock_use_package_warehouse, - mock_execute, - mock_create_app_pkg, + mock_deploy, mock_check_git, - policy_param, - is_interactive_param, + interactive, expected_code, temp_dir, mock_cursor, - mock_bundle_map, ): version = "V1" mock_existing_version_info.return_value = {"version": version, "patch": 0} @@ -587,17 +528,6 @@ def test_process_existing_release_directives_user_does_not_proceed( {"name": "RD1", "version": version}, {"name": "RD3", "version": version}, ] - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - (None, mock.call("use role old_role")), - ] - ) - mock_execute.side_effect = side_effects current_working_directory = os.getcwd() create_named_file( @@ -609,61 +539,41 @@ def test_process_existing_release_directives_user_does_not_proceed( processor = _get_version_create_processor() with pytest.raises(typer.Exit): processor.process( - bundle_map=mock_bundle_map, version=version, patch=12, - policy=policy_param, - git_policy=allow_always_policy, - is_interactive=is_interactive_param, + force=False, + interactive=interactive, + skip_git_check=False, ) - assert mock_execute.mock_calls == expected mock_check_git.assert_called_once() mock_rd.assert_called_once() - mock_create_app_pkg.assert_called_once() - mock_apply_package_scripts.assert_called_once() - mock_use_package_warehouse.assert_called_once(), - mock_execute_package_post_deploy_hooks.assert_called_once(), - mock_sync.assert_called_once() + mock_deploy.assert_called_once() # Test version create when there are release directives matching the version AND no version exists for app pkg AND --force is True # Test version create when there are release directives matching the version AND no version exists for app pkg AND --force is False AND interactive mode is False AND --interactive is True AND user wants to proceed # Test version create when there are release directives matching the version AND no version exists for app pkg AND --force is False AND interactive mode is True AND user wants to proceed -@mock.patch(f"{VERSION_MODULE}.check_index_changes_in_git_repo", return_value=None) @mock.patch.object( - NativeAppVersionCreateProcessor, "create_app_package", return_value=None + ApplicationPackageEntity, "check_index_changes_in_git_repo", return_value=None ) -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) -@mock.patch.object(NativeAppVersionCreateProcessor, "use_package_warehouse") +@mock.patch.object(ApplicationPackageEntity, "deploy", return_value=None) @mock.patch.object( - NativeAppVersionCreateProcessor, - "execute_package_post_deploy_hooks", - return_value=None, -) -@mock.patch.object( - NativeAppVersionCreateProcessor, "_apply_package_scripts", return_value=None -) -@mock.patch.object( - NativeAppVersionCreateProcessor, "sync_deploy_root_with_stage", return_value=None -) -@mock.patch.object( - NativeAppVersionCreateProcessor, + ApplicationPackageEntity, "get_existing_release_directive_info_for_version", return_value=None, ) @mock.patch.object( - NativeAppVersionCreateProcessor, "get_existing_version_info", return_value=None + ApplicationPackageEntity, "get_existing_version_info", return_value=None ) @mock.patch.object( - NativeAppVersionCreateProcessor, "add_new_patch_to_version", return_value=None + ApplicationPackageEntity, "add_new_patch_to_version", return_value=None ) @mock.patch.object(typer, "confirm", return_value=True) @pytest.mark.parametrize( - "policy_param, is_interactive_param", + "force, interactive", [ - (allow_always_policy, False), - (ask_always_policy, True), - (ask_always_policy, True), + (False, True), + (True, True), ], ) def test_process_existing_release_directives_w_existing_version_two( @@ -671,18 +581,12 @@ def test_process_existing_release_directives_w_existing_version_two( mock_add_patch, mock_existing_version_info, mock_rd, - mock_sync, - mock_apply_package_scripts, - mock_execute_package_post_deploy_hooks, - mock_use_package_warehouse, - mock_execute, - mock_create_app_pkg, + mock_deploy, mock_check_git, - policy_param, - is_interactive_param, + force, + interactive, temp_dir, mock_cursor, - mock_bundle_map, ): version = "V1" mock_existing_version_info.return_value = { @@ -695,17 +599,6 @@ def test_process_existing_release_directives_w_existing_version_two( {"name": "RD1", "version": version}, {"name": "RD3", "version": version}, ] - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - (None, mock.call("use role old_role")), - ] - ) - mock_execute.side_effect = side_effects current_working_directory = os.getcwd() create_named_file( @@ -716,124 +609,14 @@ def test_process_existing_release_directives_w_existing_version_two( processor = _get_version_create_processor() processor.process( - bundle_map=mock_bundle_map, version=version, patch=12, - policy=policy_param, - git_policy=allow_always_policy, - is_interactive=is_interactive_param, + force=force, + interactive=interactive, + skip_git_check=False, ) - assert mock_execute.mock_calls == expected mock_check_git.assert_called_once() mock_rd.assert_called_once() - mock_create_app_pkg.assert_called_once() - mock_apply_package_scripts.assert_called_once() - mock_use_package_warehouse.assert_called_once(), - mock_execute_package_post_deploy_hooks.assert_called_once() - mock_sync.assert_called_once() + mock_deploy.assert_called_once() assert mock_existing_version_info.call_count == 2 mock_add_patch.assert_called_once() - - -# Test version create when the app package doesn't have the magic CLI comment -@mock.patch(FIND_VERSION_FROM_MANIFEST, return_value=("manifest_version", None)) -@mock.patch(f"{VERSION_MODULE}.check_index_changes_in_git_repo", return_value=None) -@mock.patch.object( - NativeAppVersionCreateProcessor, - "create_app_package", - side_effect=ApplicationPackageAlreadyExistsError(""), -) -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) -@mock.patch.object(NativeAppVersionCreateProcessor, "use_package_warehouse") -@mock.patch.object( - NativeAppVersionCreateProcessor, - "execute_package_post_deploy_hooks", - return_value=None, -) -@mock.patch.object( - NativeAppVersionCreateProcessor, "_apply_package_scripts", return_value=None -) -@mock.patch.object( - NativeAppVersionCreateProcessor, "sync_deploy_root_with_stage", return_value=None -) -@mock.patch.object( - NativeAppVersionCreateProcessor, - "get_existing_release_directive_info_for_version", - return_value=None, -) -@mock.patch.object( - NativeAppVersionCreateProcessor, "get_existing_version_info", return_value=None -) -@mock.patch.object( - NativeAppVersionCreateProcessor, "add_new_version", return_value=None -) -@mock.patch.object(typer, "confirm") -@pytest.mark.parametrize( - "policy_param", [allow_always_policy, ask_always_policy, deny_always_policy] -) -@pytest.mark.parametrize("confirm_response", [True, False]) -def test_process_package_no_magic_comment( - mock_typer_confirm, - mock_add_new_version, - mock_existing_version_info, - mock_rd, - mock_sync, - mock_apply_package_scripts, - mock_execute_package_post_deploy_hooks, - mock_use_package_warehouse, - mock_execute, - mock_create_app_pkg, - mock_check_git, - mock_find_version, - policy_param, - confirm_response, - temp_dir, - mock_cursor, - mock_bundle_map, -): - version = "V1" - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - (None, mock.call("use role old_role")), - ] - ) - mock_execute.side_effect = side_effects - mock_typer_confirm.return_value = confirm_response - - current_working_directory = os.getcwd() - create_named_file( - file_name="snowflake.yml", - dir_name=current_working_directory, - contents=[mock_snowflake_yml_file], - ) - - should_abort = policy_param is deny_always_policy or ( - policy_param is ask_always_policy and not confirm_response - ) - processor = _get_version_create_processor() - with pytest.raises(typer.Abort) if should_abort else nullcontext(): - processor.process( - bundle_map=mock_bundle_map, - version=version, - patch=None, - policy=policy_param, - git_policy=allow_always_policy, - is_interactive=False, - ) # last two parameters do not matter here - mock_find_version.assert_not_called() - if not should_abort: - assert mock_execute.mock_calls == expected - mock_check_git.assert_called_once() - mock_rd.assert_called_once() - mock_create_app_pkg.assert_called_once() - mock_apply_package_scripts.assert_called_once() - mock_execute_package_post_deploy_hooks.assert_called_once() - mock_use_package_warehouse.assert_called_once() - mock_sync.assert_called_once() - mock_existing_version_info.assert_called_once() - mock_add_new_version.assert_called_once() diff --git a/tests/nativeapp/utils.py b/tests/nativeapp/utils.py index d5f00d33c2..259294263d 100644 --- a/tests/nativeapp/utils.py +++ b/tests/nativeapp/utils.py @@ -76,7 +76,6 @@ APP_ENTITY_MODULE = "snowflake.cli._plugins.nativeapp.application_entity" APP_ENTITY = f"{APP_ENTITY_MODULE}.ApplicationEntity" APP_ENTITY_GET_EXISTING_APP_INFO = f"{APP_ENTITY}.get_existing_app_info" -APP_ENTITY_GET_EXISTING_VERSION_INFO = f"{APP_ENTITY}.get_existing_version_info" APP_ENTITY_DROP_GENERIC_OBJECT = f"{APP_ENTITY_MODULE}.drop_generic_object" APP_ENTITY_GET_OBJECTS_OWNED_BY_APPLICATION = ( f"{APP_ENTITY}.get_objects_owned_by_application" @@ -92,6 +91,9 @@ APP_PACKAGE_ENTITY_GET_EXISTING_APP_PKG_INFO = ( f"{APP_PACKAGE_ENTITY}.get_existing_app_pkg_info" ) +APP_PACKAGE_ENTITY_GET_EXISTING_VERSION_INFO = ( + f"{APP_PACKAGE_ENTITY}.get_existing_version_info" +) APP_PACKAGE_ENTITY_IS_DISTRIBUTION_SAME = ( f"{APP_PACKAGE_ENTITY}.verify_project_distribution" ) @@ -123,6 +125,7 @@ package: name: app_pkg role: package_role + warehouse: pkg_warehouse scripts: - shared_content.sql """