Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor app package deploy logic from NativeAppManager to ApplicationPackageEntity #1442

Merged
merged 14 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
488 changes: 63 additions & 425 deletions src/snowflake/cli/_plugins/nativeapp/manager.py

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions src/snowflake/cli/_plugins/nativeapp/project_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@
default_application,
default_role,
)
from snowflake.cli.api.project.schemas.native_app.application import (
PostDeployHook,
)
from snowflake.cli.api.project.schemas.entities.common import PostDeployHook
from snowflake.cli.api.project.schemas.native_app.native_app import NativeApp
from snowflake.cli.api.project.schemas.native_app.path_mapping import PathMapping
from snowflake.cli.api.project.util import (
Expand Down
6 changes: 4 additions & 2 deletions src/snowflake/cli/_plugins/nativeapp/run_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,17 @@
from snowflake.cli._plugins.nativeapp.manager import (
NativeAppCommandProcessor,
NativeAppManager,
ensure_correct_owner,
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.cli.api.console import cli_console as cc
from snowflake.cli.api.entities.utils import (
ensure_correct_owner,
generic_sql_error_handler,
)
from snowflake.cli.api.errno import (
APPLICATION_NO_LONGER_AVAILABLE,
APPLICATION_OWNS_EXTERNAL_OBJECTS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@
from snowflake.cli._plugins.nativeapp.manager import (
NativeAppCommandProcessor,
NativeAppManager,
ensure_correct_owner,
)
from snowflake.cli._plugins.nativeapp.utils import (
needs_confirmation,
)
from snowflake.cli.api.console import cli_console as cc
from snowflake.cli.api.entities.utils import ensure_correct_owner
from snowflake.cli.api.errno import APPLICATION_NO_LONGER_AVAILABLE
from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
from snowflake.connector import ProgrammingError
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@
from snowflake.cli._plugins.nativeapp.manager import (
NativeAppCommandProcessor,
NativeAppManager,
ensure_correct_owner,
)
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.entities.utils import ensure_correct_owner
from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
from snowflake.cli.api.project.schemas.native_app.native_app import NativeApp
from snowflake.cli.api.project.util import to_identifier, unquote_identifier
Expand Down
229 changes: 228 additions & 1 deletion src/snowflake/cli/api/entities/application_package_entity.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
from contextlib import contextmanager
from pathlib import Path
from textwrap import dedent
from typing import List, Optional

from click import ClickException
from snowflake.cli._plugins.nativeapp.artifacts import build_bundle
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 (
ALLOWED_SPECIAL_COMMENTS,
COMMENT_COL,
INTERNAL_DISTRIBUTION,
NAME_COL,
SPECIAL_COMMENT,
)
from snowflake.cli._plugins.nativeapp.exceptions import (
ApplicationPackageAlreadyExistsError,
)
from snowflake.cli._plugins.workspace.action_context import ActionContext
from snowflake.cli.api.entities.common import EntityBase
from snowflake.cli.api.console.abc import AbstractConsole
from snowflake.cli.api.entities.common import EntityBase, get_sql_executor
from snowflake.cli.api.entities.utils import (
ensure_correct_owner,
generic_sql_error_handler,
render_script_templates,
)
from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
from snowflake.cli.api.project.schemas.entities.application_package_entity_model import (
ApplicationPackageEntityModel,
)
from snowflake.cli.api.rendering.jinja import (
jinja_render_from_str,
)
from snowflake.connector import ProgrammingError


class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
Expand All @@ -31,3 +56,205 @@ def action_bundle(self, ctx: ActionContext):
compiler = NativeAppCompiler(bundle_context)
compiler.compile_artifacts()
return bundle_map

@staticmethod
def get_existing_app_pkg_info(
package_name: str,
package_role: str,
sfc-gh-gbloom marked this conversation as resolved.
Show resolved Hide resolved
) -> Optional[dict]:
"""
Check for an existing application package by the same name as in project definition, in account.
It executes a 'show application packages like' query and returns the result as single row, if one exists.
"""
sql_executor = get_sql_executor()
with sql_executor.use_role(package_role):
return sql_executor.show_specific_object(
"application packages", package_name, name_col=NAME_COL
)

@staticmethod
def get_app_pkg_distribution_in_snowflake(
package_name: str,
package_role: str,
) -> str:
"""
Returns the 'distribution' attribute of a 'describe application package' SQL query, in lowercase.
"""
sql_executor = get_sql_executor()
with sql_executor.use_role(package_role):
try:
desc_cursor = sql_executor.execute_query(
f"describe application package {package_name}"
)
except ProgrammingError as err:
generic_sql_error_handler(err)

if desc_cursor.rowcount is None or desc_cursor.rowcount == 0:
raise SnowflakeSQLExecutionError()
else:
for row in desc_cursor:
if row[0].lower() == "distribution":
return row[1].lower()
raise ProgrammingError(
msg=dedent(
f"""\
Could not find the 'distribution' attribute for application package {package_name} in the output of SQL query:
'describe application package {package_name}'
"""
)
)

@classmethod
def verify_project_distribution(
cls,
console: AbstractConsole,
package_name: str,
package_role: str,
package_distribution: str,
expected_distribution: Optional[str] = None,
) -> bool:
"""
Returns true if the 'distribution' attribute of an existing application package in snowflake
is the same as the the attribute specified in project definition file.
"""
actual_distribution = (
expected_distribution
if expected_distribution
else cls.get_app_pkg_distribution_in_snowflake(
package_name=package_name,
package_role=package_role,
)
)
project_def_distribution = package_distribution.lower()
if actual_distribution != project_def_distribution:
console.warning(
dedent(
f"""\
Application package {package_name} in your Snowflake account has distribution property {actual_distribution},
which does not match the value specified in project definition file: {project_def_distribution}.
"""
)
)
return False
return True

@staticmethod
@contextmanager
def use_package_warehouse(
package_warehouse: Optional[str],
):
if package_warehouse:
with get_sql_executor().use_warehouse(package_warehouse):
yield
else:
raise ClickException(
dedent(
f"""\
Application package warehouse cannot be empty.
Please provide a value for it in your connection information or your project definition file.
"""
)
)

@classmethod
def apply_package_scripts(
cls,
console: AbstractConsole,
package_scripts: List[str],
package_warehouse: Optional[str],
project_root: Path,
package_role: str,
package_name: str,
) -> None:
"""
Assuming the application package exists and we are using the correct role,
applies all package scripts in-order to the application package.
"""

if package_scripts:
console.warning(
"WARNING: native_app.package.scripts is deprecated. Please migrate to using native_app.package.post_deploy."
)

queued_queries = render_script_templates(
project_root,
jinja_render_from_str,
dict(package_name=package_name),
package_scripts,
)

# once we're sure all the templates expanded correctly, execute all of them
with cls.use_package_warehouse(
package_warehouse=package_warehouse,
):
try:
for i, queries in enumerate(queued_queries):
console.step(f"Applying package script: {package_scripts[i]}")
get_sql_executor().execute_queries(queries)
except ProgrammingError as err:
generic_sql_error_handler(
err, role=package_role, warehouse=package_warehouse
)

@classmethod
def create_app_package(
cls,
console: AbstractConsole,
package_name: str,
package_role: str,
package_distribution: str,
) -> None:
"""
Creates the application package with our up-to-date stage if none exists.
"""

# 1. Check for existing existing application package
show_obj_row = cls.get_existing_app_pkg_info(
package_name=package_name,
package_role=package_role,
)

if show_obj_row:
# 1. Check for the right owner role
ensure_correct_owner(
row=show_obj_row, role=package_role, obj_name=package_name
)

# 2. Check distribution of the existing application package
actual_distribution = cls.get_app_pkg_distribution_in_snowflake(
package_name=package_name,
package_role=package_role,
)
if not cls.verify_project_distribution(
console=console,
package_name=package_name,
package_role=package_role,
package_distribution=package_distribution,
expected_distribution=actual_distribution,
):
console.warning(
f"Continuing to execute `snow app run` on application package {package_name} with distribution '{actual_distribution}'."
)

# 3. If actual_distribution is external, skip comment check
if actual_distribution == INTERNAL_DISTRIBUTION:
row_comment = show_obj_row[COMMENT_COL]

if row_comment not in ALLOWED_SPECIAL_COMMENTS:
raise ApplicationPackageAlreadyExistsError(package_name)

return

# If no application package pre-exists, create an application package, with the specified distribution in the project definition file.
sql_executor = get_sql_executor()
with sql_executor.use_role(package_role):
console.step(f"Creating new application package {package_name} in account.")
sql_executor.execute_query(
dedent(
f"""\
create application package {package_name}
comment = {SPECIAL_COMMENT}
distribution = {package_distribution}
"""
)
)
6 changes: 6 additions & 0 deletions src/snowflake/cli/api/entities/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Generic, Type, TypeVar, get_args

from snowflake.cli._plugins.workspace.action_context import ActionContext
from snowflake.cli.api.sql_execution import SqlExecutor


class EntityActions(str, Enum):
Expand Down Expand Up @@ -39,3 +40,8 @@ def perform(self, action: EntityActions, action_ctx: ActionContext):
Performs the requested action.
"""
return getattr(self, action)(action_ctx)


def get_sql_executor() -> SqlExecutor:
sfc-gh-gbloom marked this conversation as resolved.
Show resolved Hide resolved
"""Returns an SQL Executor that uses the connection from the current CLI context"""
return SqlExecutor()
Loading
Loading