Skip to content

Commit

Permalink
add app deploy command
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-gbloom committed Mar 20, 2024
1 parent ef1c404 commit 54de298
Show file tree
Hide file tree
Showing 7 changed files with 405 additions and 303 deletions.
19 changes: 19 additions & 0 deletions src/snowflake/cli/plugins/nativeapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,22 @@ def app_teardown(
)
processor.process(force)
return MessageResult(f"Teardown is now complete.")


@app.command("deploy", requires_connection=True)
@with_project_definition("native_app")
def app_deploy(
**options,
) -> CommandResult:
"""
Syncs the local changes to the stage without creating or updating the application.
"""
manager = NativeAppManager(
project_definition=cli_context.project_definition,
project_root=cli_context.project_root,
)

manager.build_bundle()
manager.deploy()

return MessageResult(f"Deployed successfully.")
110 changes: 109 additions & 1 deletion src/snowflake/cli/plugins/nativeapp/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from textwrap import dedent
from typing import List, Optional

import jinja2
from snowflake.cli.api.console import cli_console as cc
from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
from snowflake.cli.api.project.definition import (
Expand All @@ -27,12 +28,21 @@
translate_artifact,
)
from snowflake.cli.plugins.nativeapp.constants import (
ALLOWED_SPECIAL_COMMENTS,
COMMENT_COL,
ERROR_MESSAGE_606,
ERROR_MESSAGE_2043,
INTERNAL_DISTRIBUTION,
NAME_COL,
OWNER_COL,
SPECIAL_COMMENT,
)
from snowflake.cli.plugins.nativeapp.exceptions import (
ApplicationPackageAlreadyExistsError,
InvalidPackageScriptError,
MissingPackageScriptError,
UnexpectedOwnerError,
)
from snowflake.cli.plugins.nativeapp.exceptions import UnexpectedOwnerError
from snowflake.cli.plugins.object.stage.diff import (
DiffResult,
stage_diff,
Expand Down Expand Up @@ -327,3 +337,101 @@ def get_snowsight_url(self) -> str:
"""Returns the URL that can be used to visit this app via Snowsight."""
name = unquote_identifier(self.app_name)
return make_snowsight_url(self._conn, f"/#/apps/application/{name}")

def create_app_package(self) -> None:
"""
Creates the application package with our up-to-date stage if none exists.
"""

# 1. Check for existing existing application package
show_obj_row = self.get_existing_app_pkg_info()

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

# 2. Check distribution of the existing application package
actual_distribution = self.get_app_pkg_distribution_in_snowflake
if not self.verify_project_distribution(actual_distribution):
cc.warning(
f"Continuing to execute `snow app run` on application package {self.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(self.package_name)

return

# If no application package pre-exists, create an application package, with the specified distribution in the project definition file.
with self.use_role(self.package_role):
cc.step(f"Creating new application package {self.package_name} in account.")
self._execute_query(
dedent(
f"""\
create application package {self.package_name}
comment = {SPECIAL_COMMENT}
distribution = {self.package_distribution}
"""
)
)

def _apply_package_scripts(self) -> None:
"""
Assuming the application package exists and we are using the correct role,
applies all package scripts in-order to the application package.
"""
env = jinja2.Environment(
loader=jinja2.loaders.FileSystemLoader(self.project_root),
keep_trailing_newline=True,
undefined=jinja2.StrictUndefined,
)

queued_queries = []
for relpath in self.package_scripts:
try:
template = env.get_template(relpath)
result = template.render(dict(package_name=self.package_name))
queued_queries.append(result)

except jinja2.TemplateNotFound as e:
raise MissingPackageScriptError(e.name)

except jinja2.TemplateSyntaxError as e:
raise InvalidPackageScriptError(e.name, e)

except jinja2.UndefinedError as e:
raise InvalidPackageScriptError(relpath, e)

# once we're sure all the templates expanded correctly, execute all of them
try:
if self.package_warehouse:
self._execute_query(f"use warehouse {self.package_warehouse}")

for i, queries in enumerate(queued_queries):
cc.step(f"Applying package script: {self.package_scripts[i]}")
self._execute_queries(queries)
except ProgrammingError as err:
generic_sql_error_handler(
err, role=self.package_role, warehouse=self.package_warehouse
)

def deploy(self) -> DiffResult:
"""app deploy process"""

# 1. Create an empty application package, if none exists
self.create_app_package()

with self.use_role(self.package_role):
# 2. now that the application package exists, create shared data
self._apply_package_scripts()

# 3. Upload files from deploy root local folder to the above stage
diff = self.sync_deploy_root_with_stage(self.package_role)

return diff
100 changes: 1 addition & 99 deletions src/snowflake/cli/plugins/nativeapp/run_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from textwrap import dedent
from typing import Optional

import jinja2
import typer
from click import UsageError
from snowflake.cli.api.console import cli_console as cc
Expand All @@ -11,17 +10,13 @@
from snowflake.cli.plugins.nativeapp.constants import (
ALLOWED_SPECIAL_COMMENTS,
COMMENT_COL,
INTERNAL_DISTRIBUTION,
LOOSE_FILES_MAGIC_VERSION,
SPECIAL_COMMENT,
VERSION_COL,
)
from snowflake.cli.plugins.nativeapp.exceptions import (
ApplicationAlreadyExistsError,
ApplicationPackageAlreadyExistsError,
ApplicationPackageDoesNotExistError,
InvalidPackageScriptError,
MissingPackageScriptError,
)
from snowflake.cli.plugins.nativeapp.manager import (
NativeAppCommandProcessor,
Expand All @@ -42,89 +37,6 @@ class NativeAppRunProcessor(NativeAppManager, NativeAppCommandProcessor):
def __init__(self, project_definition: NativeApp, project_root: Path):
super().__init__(project_definition, project_root)

def create_app_package(self) -> None:
"""
Creates the application package with our up-to-date stage if none exists.
"""

# 1. Check for existing existing application package
show_obj_row = self.get_existing_app_pkg_info()

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

# 2. Check distribution of the existing application package
actual_distribution = self.get_app_pkg_distribution_in_snowflake
if not self.verify_project_distribution(actual_distribution):
cc.warning(
f"Continuing to execute `snow app run` on application package {self.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(self.package_name)

return

# If no application package pre-exists, create an application package, with the specified distribution in the project definition file.
with self.use_role(self.package_role):
cc.step(f"Creating new application package {self.package_name} in account.")
self._execute_query(
dedent(
f"""\
create application package {self.package_name}
comment = {SPECIAL_COMMENT}
distribution = {self.package_distribution}
"""
)
)

def _apply_package_scripts(self) -> None:
"""
Assuming the application package exists and we are using the correct role,
applies all package scripts in-order to the application package.
"""
env = jinja2.Environment(
loader=jinja2.loaders.FileSystemLoader(self.project_root),
keep_trailing_newline=True,
undefined=jinja2.StrictUndefined,
)

queued_queries = []
for relpath in self.package_scripts:
try:
template = env.get_template(relpath)
result = template.render(dict(package_name=self.package_name))
queued_queries.append(result)

except jinja2.TemplateNotFound as e:
raise MissingPackageScriptError(e.name)

except jinja2.TemplateSyntaxError as e:
raise InvalidPackageScriptError(e.name, e)

except jinja2.UndefinedError as e:
raise InvalidPackageScriptError(relpath, e)

# once we're sure all the templates expanded correctly, execute all of them
try:
if self.package_warehouse:
self._execute_query(f"use warehouse {self.package_warehouse}")

for i, queries in enumerate(queued_queries):
cc.step(f"Applying package script: {self.package_scripts[i]}")
self._execute_queries(queries)
except ProgrammingError as err:
generic_sql_error_handler(
err, role=self.package_role, warehouse=self.package_warehouse
)

def _create_dev_app(self, diff: DiffResult) -> None:
"""
(Re-)creates the application object with our up-to-date stage.
Expand Down Expand Up @@ -385,15 +297,5 @@ def process(
)
return

# 1. Create an empty application package, if none exists
self.create_app_package()

with self.use_role(self.package_role):
# 2. now that the application package exists, create shared data
self._apply_package_scripts()

# 3. Upload files from deploy root local folder to the above stage
diff = self.sync_deploy_root_with_stage(self.package_role)

# 4. Create an application if none exists, else upgrade the application
diff = self.deploy()
self._create_dev_app(diff)
70 changes: 70 additions & 0 deletions tests/__snapshots__/test_help_messages.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,74 @@
╰──────────────────────────────────────────────────────────────────────────────╯


'''
# ---
# name: test_help_messages[app.deploy]
'''

Usage: default app deploy [OPTIONS]

Syncs the local changes to the stage without creating or updating the
application.

╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --project -p TEXT Path where the Snowflake Native App project │
│ resides. Defaults to current working directory. │
│ --help -h Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Connection configuration ───────────────────────────────────────────────────╮
│ --connection,--environment -c TEXT Name of the connection, as defined │
│ in your `config.toml`. Default: │
│ `default`. │
│ --account,--accountname TEXT Name assigned to your Snowflake │
│ account. Overrides the value │
│ specified for the connection. │
│ --user,--username TEXT Username to connect to Snowflake. │
│ Overrides the value specified for │
│ the connection. │
│ --password TEXT Snowflake password. Overrides the │
│ value specified for the │
│ connection. │
│ --authenticator TEXT Snowflake authenticator. Overrides │
│ the value specified for the │
│ connection. │
│ --private-key-path TEXT Snowflake private key path. │
│ Overrides the value specified for │
│ the connection. │
│ --database,--dbname TEXT Database to use. Overrides the │
│ value specified for the │
│ connection. │
│ --schema,--schemaname TEXT Database schema to use. Overrides │
│ the value specified for the │
│ connection. │
│ --role,--rolename TEXT Role to use. Overrides the value │
│ specified for the connection. │
│ --warehouse TEXT Warehouse to use. Overrides the │
│ value specified for the │
│ connection. │
│ --temporary-connection -x Uses connection defined with │
│ command line parameters, instead │
│ of one defined in config │
│ --mfa-passcode TEXT Token to use for multi-factor │
│ authentication (MFA) │
│ --enable-diag Run python connector diagnostic │
│ test │
│ --diag-log-path TEXT Diagnostic report path │
│ --diag-allowlist-path TEXT Diagnostic report path to optional │
│ allowlist │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Global configuration ───────────────────────────────────────────────────────╮
│ --format [TABLE|JSON] Specifies the output format. │
│ [default: TABLE] │
│ --verbose -v Displays log entries for log levels `info` │
│ and higher. │
│ --debug Displays log entries for log levels `debug` │
│ and higher; debug logs contains additional │
│ information. │
│ --silent Turns off intermediate output to console. │
╰──────────────────────────────────────────────────────────────────────────────╯


'''
# ---
# name: test_help_messages[app.init]
Expand Down Expand Up @@ -670,6 +738,8 @@
│ --help -h Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────╮
│ deploy Syncs the local changes to the stage without creating or updating │
│ the application. │
│ init Initializes a Snowflake Native App project. │
│ open Opens the Snowflake Native App inside of your browser, once it has │
│ been installed in your account. │
Expand Down
Loading

0 comments on commit 54de298

Please sign in to comment.