Skip to content

Commit

Permalink
De-dupe app create / upgrade path in NativeAppRunProcessor (#1285)
Browse files Browse the repository at this point in the history
* tiny refactor

* Tests passing
  • Loading branch information
sfc-gh-cgorrie authored Jul 4, 2024
1 parent 880389d commit 7232f61
Show file tree
Hide file tree
Showing 2 changed files with 259 additions and 181 deletions.
207 changes: 95 additions & 112 deletions src/snowflake/cli/plugins/nativeapp/run_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
generic_sql_error_handler,
)
from snowflake.cli.plugins.nativeapp.policy import PolicyBase
from snowflake.cli.plugins.nativeapp.project_model import (
NativeAppProjectModel,
)
from snowflake.cli.plugins.stage.manager import StageManager
from snowflake.connector import ProgrammingError
from snowflake.connector.cursor import DictCursor, SnowflakeCursor
Expand All @@ -65,102 +68,69 @@
}


class NativeAppRunProcessor(NativeAppManager, NativeAppCommandProcessor):
def __init__(self, project_definition: NativeApp, project_root: Path):
super().__init__(project_definition, project_root)
class SameAccountInstallMethod:
_requires_created_by_cli: bool
_from_release_directive: bool
version: Optional[str]
patch: Optional[int]

def _create_dev_app(self, policy: PolicyBase, is_interactive: bool = False) -> None:
"""
(Re-)creates the application object with our up-to-date stage.
"""
with self.use_role(self.app_role):
def __init__(
self,
requires_created_by_cli: bool,
version: Optional[str] = None,
patch: Optional[int] = None,
from_release_directive: bool = False,
):
self._requires_created_by_cli = requires_created_by_cli
self.version = version
self.patch = patch
self._from_release_directive = from_release_directive

# 1. Need to use a warehouse to create an application object
try:
if self.application_warehouse:
self._execute_query(f"use warehouse {self.application_warehouse}")
except ProgrammingError as err:
generic_sql_error_handler(
err=err, role=self.app_role, warehouse=self.application_warehouse
)
@classmethod
def unversioned_dev(cls):
"""aka. stage dev aka loose files"""
return cls(True)

# 2. Check for an existing application object by the same name
show_app_row = self.get_existing_app_info()
@classmethod
def versioned_dev(cls, version: str, patch: Optional[int] = None):
return cls(False, version, patch)

# 3. If existing application object is found, perform a few validations and upgrade the application object.
if show_app_row:
@classmethod
def release_directive(cls):
return cls(False, from_release_directive=True)

# Check if not created by Snowflake CLI or not created using "files on a named stage" / stage dev mode.
if show_app_row[COMMENT_COL] not in ALLOWED_SPECIAL_COMMENTS or (
show_app_row[VERSION_COL] != LOOSE_FILES_MAGIC_VERSION
):
raise ApplicationAlreadyExistsError(self.app_name)
@property
def is_dev_mode(self) -> bool:
return not self._from_release_directive

# Check for the right owner
ensure_correct_owner(
row=show_app_row, role=self.app_role, obj_name=self.app_name
)
def using_clause(self, app: NativeAppProjectModel) -> str:
if self._from_release_directive:
return ""

# If all the above checks are in order, proceed to upgrade
try:
cc.step(f"Upgrading existing application object {self.app_name}.")
self._execute_query(
f"alter application {self.app_name} upgrade using @{self.stage_fqn}"
)
if self.version:
patch_clause = f"patch {self.patch}" if self.patch else ""
return f"using version {self.version} {patch_clause}"

# if debug_mode is present (controlled), ensure it is up-to-date
if self.debug_mode is not None:
self._execute_query(
f"alter application {self.app_name} set debug_mode = {self.debug_mode}"
)
stage_name = StageManager.quote_stage_name(app.stage_fqn)
return f"using {stage_name}"

self._execute_post_deploy_hooks()
return
def ensure_app_usable(self, app: NativeAppProjectModel, show_app_row: dict):
"""Raise an exception if we cannot proceed with install given the pre-existing application object"""

except ProgrammingError as 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)
if self._requires_created_by_cli:
if show_app_row[COMMENT_COL] not in ALLOWED_SPECIAL_COMMENTS or (
show_app_row[VERSION_COL] != LOOSE_FILES_MAGIC_VERSION
):
# this application object was not created by this tooling
raise ApplicationAlreadyExistsError(app.app_name)

# 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.")
# expected owner
ensure_correct_owner(row=show_app_row, role=app.app_role, obj_name=app.app_name)

if self.app_role != self.package_role:
with self.use_role(new_role=self.package_role):
self._execute_queries(
dedent(
f"""\
grant install, develop on application package {self.package_name} to role {self.app_role};
grant usage on schema {self.package_name}.{self.stage_schema} to role {self.app_role};
grant read on stage {self.stage_fqn} to role {self.app_role};
"""
)
)

stage_name = StageManager.quote_stage_name(self.stage_fqn)

try:
# by default, applications are created in debug mode; this can be overridden in the project definition
initial_debug_mode = (
self.debug_mode if self.debug_mode is not None else True
)
self._execute_query(
dedent(
f"""\
create application {self.app_name}
from application package {self.package_name}
using {stage_name}
debug_mode = {initial_debug_mode}
comment = {SPECIAL_COMMENT}
"""
)
)
except ProgrammingError as err:
generic_sql_error_handler(err)

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

def _execute_sql_script(self, sql_script_path):
"""
Expand Down Expand Up @@ -283,17 +253,12 @@ def drop_application_before_upgrade(
else:
generic_sql_error_handler(err)

def upgrade_app(
def create_or_upgrade_app(
self,
policy: PolicyBase,
is_interactive: bool,
version: Optional[str] = None,
patch: Optional[int] = None,
install_method: SameAccountInstallMethod,
is_interactive: bool = False,
):

patch_clause = f"patch {patch}" if patch else ""
using_clause = f"using version {version} {patch_clause}" if version else ""

with self.use_role(self.app_role):

# 1. Need to use a warehouse to create an application object
Expand All @@ -311,24 +276,25 @@ def upgrade_app(
# 3. If existing application is found, perform a few validations and upgrade the application object.
if show_app_row:

# We skip comment check here, because prod/pre-existing application objects may not be created by the Snowflake CLI.
# Check for the right owner
ensure_correct_owner(
row=show_app_row, role=self.app_role, obj_name=self.app_name
)
install_method.ensure_app_usable(self._na_project, show_app_row)

# If all the above checks are in order, proceed to upgrade
try:
cc.step(f"Upgrading existing application object {self.app_name}.")
using_clause = install_method.using_clause(self._na_project)
self._execute_query(
f"alter application {self.app_name} upgrade {using_clause}"
)

if using_clause:
if install_method.is_dev_mode:
# if debug_mode is present (controlled), ensure it is up-to-date
if self.debug_mode is not None:
self._execute_query(
f"alter application {self.app_name} set debug_mode = {self.debug_mode}"
)

# hooks always executed after a create or upgrade
self._execute_post_deploy_hooks()
return

except ProgrammingError as err:
Expand All @@ -342,27 +308,28 @@ def upgrade_app(
cc.step(f"Creating new application object {self.app_name} in account.")

if self.app_role != self.package_role:
with self.use_role(new_role=self.package_role):
with self.use_role(self.package_role):
self._execute_query(
f"grant install on application package {self.package_name} to role {self.app_role}"
f"grant install, develop on application package {self.package_name} to role {self.app_role}"
)
self._execute_query(
f"grant usage on schema {self.package_name}.{self.stage_schema} to role {self.app_role}"
)
self._execute_query(
f"grant read on stage {self.stage_fqn} to role {self.app_role}"
)
if version:
self._execute_query(
f"grant develop on application package {self.package_name} to role {self.app_role}"
)

try:
# by default, applications are created in debug mode when possible;
# this can be overridden in the project definition
debug_mode_clause = ""
if (
using_clause
): # release directive installations cannot use debug_mode
if install_method.is_dev_mode:
initial_debug_mode = (
self.debug_mode if self.debug_mode is not None else True
)
debug_mode_clause = f"debug_mode = {initial_debug_mode}"

using_clause = install_method.using_clause(self._na_project)
self._execute_query(
dedent(
f"""\
Expand All @@ -373,6 +340,9 @@ def upgrade_app(
)
)

# hooks always executed after a create or upgrade
self._execute_post_deploy_hooks()

except ProgrammingError as err:
generic_sql_error_handler(err)

Expand All @@ -388,12 +358,21 @@ def process(
*args,
**kwargs,
):
"""app run process"""
"""
Create or upgrade the application object using the given strategy
(unversioned dev, versioned dev, or same-account release directive).
"""

# same-account release directive
if from_release_directive:
self.upgrade_app(policy=policy, is_interactive=is_interactive)
self.create_or_upgrade_app(
policy=policy,
is_interactive=is_interactive,
install_method=SameAccountInstallMethod.release_directive(),
)
return

# versioned dev
if version:
try:
version_exists = self.get_existing_version_info(version)
Expand All @@ -406,15 +385,19 @@ def process(
f"Application package {self.package_name} does not exist. Use 'snow app version create' to first create an application package and then define a version in it."
)

self.upgrade_app(
self.create_or_upgrade_app(
policy=policy,
version=version,
patch=patch,
install_method=SameAccountInstallMethod.versioned_dev(version, patch),
is_interactive=is_interactive,
)
return

# unversioned dev
self.deploy(
bundle_map=bundle_map, prune=True, recursive=True, validate=validate
)
self._create_dev_app(policy=policy, is_interactive=is_interactive)
self.create_or_upgrade_app(
policy=policy,
is_interactive=is_interactive,
install_method=SameAccountInstallMethod.unversioned_dev(),
)
Loading

0 comments on commit 7232f61

Please sign in to comment.