Skip to content

Commit

Permalink
When running snow app run against an application whose package was …
Browse files Browse the repository at this point in the history
…dropped, offer to automatically tear it down (#1245)

When an app package has been dropped but the app still exists, the app can't be interacted with. In this case, running snow app run fails to upgrade the app to the latest staged files.

Since the app is effectively unusable at this point, the only option is to drop it. If the app owns any external resources (e.g. compute pools), the drop must use cascade. Unfortunately, since the app is unusable, show objects owned by application throws an error, so we have to handle that case and ask the user if they still want to drop the app without knowing what the objects are. If they say yes, we retry the drop with cascade.
  • Loading branch information
sfc-gh-fcampbell authored Jun 26, 2024
1 parent 2444bd5 commit 82d08ac
Show file tree
Hide file tree
Showing 7 changed files with 473 additions and 36 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
* Passing a directory to `snow app deploy` will now deploy any contained file or subfolder specified in the application's artifact rules
* Fixes markup escaping errors in `snow sql` that may occur when users use unintentionally markup-like escape tags.
* Fixed case where `snow app teardown` could leave behind orphan applications if they were not created by the Snowflake CLI
* Fixed case where `snow app run` could fail to run an existing application whose package was dropped by prompting to drop and recreate the application
* Improve terminal output sanitization to avoid ASCII escape codes.
* The `snow sql` command will show query text before executing it.
* Improved stage diff output in `snow app` commands
Expand Down
2 changes: 2 additions & 0 deletions src/snowflake/cli/plugins/nativeapp/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@
ERROR_MESSAGE_2003 = "does not exist or not authorized"
ERROR_MESSAGE_2043 = "Object does not exist, or operation cannot be performed."
ERROR_MESSAGE_606 = "No active warehouse selected in the current session."
ERROR_MESSAGE_093079 = "Application is no longer available for use"
ERROR_MESSAGE_093128 = "The application owns one or more objects within the account"
14 changes: 14 additions & 0 deletions src/snowflake/cli/plugins/nativeapp/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,20 @@ def get_objects_owned_by_application(self) -> List[ApplicationOwnedObject]:
).fetchall()
return [{"name": row[1], "type": row[2]} for row in results]

def _application_objects_to_str(
self, application_objects: list[ApplicationOwnedObject]
) -> str:
"""
Returns a list in an "(Object Type) Object Name" format. Database-level and schema-level object names are fully qualified:
(COMPUTE_POOL) POOL_NAME
(DATABASE) DB_NAME
(SCHEMA) DB_NAME.PUBLIC
...
"""
return "\n".join(
[f"({obj['type']}) {obj['name']}" for obj in application_objects]
)

def get_snowsight_url(self) -> str:
"""Returns the URL that can be used to visit this app via Snowsight."""
name = identifier_for_url(self.app_name)
Expand Down
59 changes: 49 additions & 10 deletions src/snowflake/cli/plugins/nativeapp/run_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
from snowflake.cli.plugins.nativeapp.constants import (
ALLOWED_SPECIAL_COMMENTS,
COMMENT_COL,
ERROR_MESSAGE_093079,
ERROR_MESSAGE_093128,
LOOSE_FILES_MAGIC_VERSION,
PATCH_COL,
SPECIAL_COMMENT,
Expand All @@ -49,19 +51,25 @@
generic_sql_error_handler,
)
from snowflake.cli.plugins.nativeapp.policy import PolicyBase
from snowflake.cli.plugins.stage.diff import DiffResult
from snowflake.cli.plugins.stage.manager import StageManager
from snowflake.connector import ProgrammingError
from snowflake.connector.cursor import DictCursor, SnowflakeCursor

UPGRADE_RESTRICTION_CODES = {93044, 93055, 93045, 93046}
# Reasons why an `alter application ... upgrade` might fail
UPGRADE_RESTRICTION_CODES = {
93044, # Cannot upgrade dev mode application from loose stage files to version
93045, # Cannot upgrade dev mode application from version to loose stage files
93046, # Operation only permitted on dev mode application
93055, # Operation not supported on dev mode application
93079, # App package access lost
}


class NativeAppRunProcessor(NativeAppManager, NativeAppCommandProcessor):
def __init__(self, project_definition: NativeApp, project_root: Path):
super().__init__(project_definition, project_root)

def _create_dev_app(self, diff: DiffResult) -> None:
def _create_dev_app(self, policy: PolicyBase, is_interactive: bool = False) -> None:
"""
(Re-)creates the application object with our up-to-date stage.
"""
Expand Down Expand Up @@ -109,7 +117,11 @@ def _create_dev_app(self, diff: DiffResult) -> None:
return

except ProgrammingError as err:
generic_sql_error_handler(err)
if err.errno not in UPGRADE_RESTRICTION_CODES:
generic_sql_error_handler(err)
else:
cc.warning(err.msg)
self.drop_application_before_upgrade(policy, is_interactive)

# 4. If no existing application object is found, create an application object using "files on a named stage" / stage dev mode.
cc.step(f"Creating new application {self.app_name} in account.")
Expand Down Expand Up @@ -220,11 +232,31 @@ def get_existing_version_info(self, version: str) -> Optional[dict]:
generic_sql_error_handler(err=err, role=self.package_role)
return None

def drop_application_before_upgrade(self, policy: PolicyBase, is_interactive: bool):
def drop_application_before_upgrade(
self, policy: PolicyBase, is_interactive: bool, cascade: bool = False
):
"""
This method will attempt to drop an application object if a previous upgrade fails.
"""
user_prompt = "Do you want the Snowflake CLI to drop the existing application object and recreate it?"
if cascade:
try:
if application_objects := self.get_objects_owned_by_application():
application_objects_str = self._application_objects_to_str(
application_objects
)
cc.message(
f"The following objects are owned by application {self.app_name} and need to be dropped:\n{application_objects_str}"
)
except ProgrammingError as err:
if err.errno != 93079 and ERROR_MESSAGE_093079 not in err.msg:
generic_sql_error_handler(err)
cc.warning(
"The application owns other objects but they could not be determined."
)
user_prompt = "Do you want the Snowflake CLI to drop these objects, then drop the existing application object and recreate it?"
else:
user_prompt = "Do you want the Snowflake CLI to drop the existing application object and recreate it?"

if not policy.should_proceed(user_prompt):
if is_interactive:
cc.message("Not upgrading the application object.")
Expand All @@ -235,9 +267,16 @@ def drop_application_before_upgrade(self, policy: PolicyBase, is_interactive: bo
)
raise typer.Exit(1)
try:
self._execute_query(f"drop application {self.app_name}")
cascade_sql = " cascade" if cascade else ""
self._execute_query(f"drop application {self.app_name}{cascade_sql}")
except ProgrammingError as err:
generic_sql_error_handler(err)
if (err.errno == 93128 or ERROR_MESSAGE_093128 in err.msg) and not cascade:
# We need to cascade the deletion, let's try again (only if we didn't try with cascade already)
return self.drop_application_before_upgrade(
policy, is_interactive, cascade=True
)
else:
generic_sql_error_handler(err)

def upgrade_app(
self,
Expand Down Expand Up @@ -363,7 +402,7 @@ def process(
)
return

diff = self.deploy(
self.deploy(
bundle_map=bundle_map, prune=True, recursive=True, validate=validate
)
self._create_dev_app(diff)
self._create_dev_app(policy=policy, is_interactive=is_interactive)
15 changes: 0 additions & 15 deletions src/snowflake/cli/plugins/nativeapp/teardown_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
CouldNotDropApplicationPackageWithVersions,
)
from snowflake.cli.plugins.nativeapp.manager import (
ApplicationOwnedObject,
NativeAppCommandProcessor,
NativeAppManager,
ensure_correct_owner,
Expand Down Expand Up @@ -65,20 +64,6 @@ def drop_generic_object(

cc.message(f"Dropped {object_type} {object_name} successfully.")

def _application_objects_to_str(
self, application_objects: list[ApplicationOwnedObject]
) -> str:
"""
Returns a list in an "(Object Type) Object Name" format. Database-level and schema-level object names are fully qualified:
(COMPUTE_POOL) POOL_NAME
(DATABASE) DB_NAME
(SCHEMA) DB_NAME.PUBLIC
...
"""
return "\n".join(
[f"({obj['type']}) {obj['name']}" for obj in application_objects]
)

def drop_application(
self, auto_yes: bool, interactive: bool = False, cascade: Optional[bool] = None
):
Expand Down
Loading

0 comments on commit 82d08ac

Please sign in to comment.