Skip to content

Commit

Permalink
Add package post-deploy scripts in project definition file (#1399)
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-melnacouzi authored and sfc-gh-pczajka committed Aug 22, 2024
1 parent c81bf00 commit 3519b10
Show file tree
Hide file tree
Showing 42 changed files with 886 additions and 235 deletions.
4 changes: 4 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@
## Backward incompatibility

## Deprecations
* Added deprecation warning for `native_app.package.scripts` in project definition file.

## New additions
* Added support for project definition file defaults in templates
* Added support for `native_app.package.post_deploy` scripts in project definition file.
* These scripts will execute whenever a Native App Package is created or updated.
* Currently only supports SQL scripts: `post_deploy: [{sql_script: script.sql}]`

## Fixes and improvements

Expand Down
4 changes: 2 additions & 2 deletions src/snowflake/cli/api/project/schemas/entities/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from pydantic import Field
from snowflake.cli.api.project.schemas.native_app.application import (
ApplicationPostDeployHook,
PostDeployHook,
)
from snowflake.cli.api.project.schemas.updatable_model import (
IdentifierField,
Expand All @@ -35,7 +35,7 @@ class MetaField(UpdatableModel):
title="Role to use when creating the entity object",
default=None,
)
post_deploy: Optional[List[ApplicationPostDeployHook]] = Field(
post_deploy: Optional[List[PostDeployHook]] = Field(
title="Actions that will be executed after the application object is created/upgraded",
default=None,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class SqlScriptHookType(UpdatableModel):


# Currently sql_script is the only supported hook type. Change to a Union once other hook types are added
ApplicationPostDeployHook = SqlScriptHookType
PostDeployHook = SqlScriptHookType


class Application(UpdatableModel):
Expand All @@ -48,7 +48,7 @@ class Application(UpdatableModel):
title="When set, forces debug_mode on/off for the deployed application object",
default=None,
)
post_deploy: Optional[List[ApplicationPostDeployHook]] = Field(
post_deploy: Optional[List[PostDeployHook]] = Field(
title="Actions that will be executed after the application object is created/upgraded",
default=None,
)
Expand Down
17 changes: 16 additions & 1 deletion src/snowflake/cli/api/project/schemas/native_app/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

from typing import List, Literal, Optional

from pydantic import Field, field_validator
from pydantic import Field, field_validator, model_validator
from snowflake.cli.api.project.schemas.native_app.application import PostDeployHook
from snowflake.cli.api.project.schemas.updatable_model import (
IdentifierField,
UpdatableModel,
Expand Down Expand Up @@ -44,6 +45,10 @@ class Package(UpdatableModel):
title="Distribution of the application package created by the Snowflake CLI",
default="internal",
)
post_deploy: Optional[List[PostDeployHook]] = Field(
title="Actions that will be executed after the application package object is created/updated",
default=None,
)

@field_validator("scripts")
@classmethod
Expand All @@ -54,6 +59,16 @@ def validate_scripts(cls, input_list):
)
return input_list

@model_validator(mode="after")
@classmethod
def validate_no_scripts_and_post_deploy(cls, value: Package):
if value.scripts and value.post_deploy:
raise ValueError(
"package.scripts and package.post_deploy fields cannot be used together. "
"We recommend using package.post_deploy for all post package deploy scripts"
)
return value


class PackageV11(Package):
# Templated defaults only supported in v1.1+
Expand Down
83 changes: 81 additions & 2 deletions src/snowflake/cli/plugins/nativeapp/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import jinja2
from click import ClickException
from snowflake.cli.api.cli_global_context import get_cli_context
from snowflake.cli.api.console import cli_console as cc
from snowflake.cli.api.errno import (
DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED,
Expand All @@ -33,14 +34,17 @@
)
from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
from snowflake.cli.api.project.schemas.native_app.application import (
ApplicationPostDeployHook,
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 (
identifier_for_url,
unquote_identifier,
)
from snowflake.cli.api.rendering.sql_templates import (
get_sql_cli_jinja_env,
)
from snowflake.cli.api.sql_execution import SqlExecutionMixin
from snowflake.cli.plugins.connection.util import make_snowsight_url
from snowflake.cli.plugins.nativeapp.artifacts import (
Expand Down Expand Up @@ -279,9 +283,13 @@ def app_role(self) -> str:
return self.na_project.app_role

@property
def app_post_deploy_hooks(self) -> Optional[List[ApplicationPostDeployHook]]:
def app_post_deploy_hooks(self) -> Optional[List[PostDeployHook]]:
return self.na_project.app_post_deploy_hooks

@property
def package_post_deploy_hooks(self) -> Optional[List[PostDeployHook]]:
return self.na_project.package_post_deploy_hooks

@property
def debug_mode(self) -> bool:
return self.na_project.debug_mode
Expand Down Expand Up @@ -603,6 +611,12 @@ 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.
"""

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

env = jinja2.Environment(
loader=jinja2.loaders.FileSystemLoader(self.project_root),
keep_trailing_newline=True,
Expand All @@ -624,6 +638,67 @@ def _apply_package_scripts(self) -> None:
err, role=self.package_role, warehouse=self.package_warehouse
)

def _execute_sql_script(
self, script_content: str, database_name: Optional[str] = None
) -> None:
"""
Executing the provided SQL script content.
This assumes that a relevant warehouse is already active.
If database_name is passed in, it will be used first.
"""
try:
if database_name is not None:
self._execute_query(f"use database {database_name}")

self._execute_queries(script_content)
except ProgrammingError as err:
generic_sql_error_handler(err)

def _execute_post_deploy_hooks(
self,
post_deploy_hooks: Optional[List[PostDeployHook]],
deployed_object_type: str,
database_name: str,
) -> None:
"""
Executes post-deploy hooks for the given object type.
While executing SQL post deploy hooks, it first switches to the database provided in the input.
All post deploy scripts templates will first be expanded using the global template context.
"""
if not post_deploy_hooks:
return

with cc.phase(f"Executing {deployed_object_type} post-deploy actions"):
sql_scripts_paths = []
for hook in post_deploy_hooks:
if hook.sql_script:
sql_scripts_paths.append(hook.sql_script)
else:
raise ValueError(
f"Unsupported {deployed_object_type} post-deploy hook type: {hook}"
)

env = get_sql_cli_jinja_env(
loader=jinja2.loaders.FileSystemLoader(self.project_root)
)
scripts_content_list = self._expand_script_templates(
env, get_cli_context().template_context, sql_scripts_paths
)

for index, sql_script_path in enumerate(sql_scripts_paths):
cc.step(f"Executing SQL script: {sql_script_path}")
self._execute_sql_script(scripts_content_list[index], database_name)

def execute_package_post_deploy_hooks(self) -> None:
self._execute_post_deploy_hooks(
self.package_post_deploy_hooks, "application package", self.package_name
)

def execute_app_post_deploy_hooks(self) -> None:
self._execute_post_deploy_hooks(
self.app_post_deploy_hooks, "application", self.app_name
)

def deploy(
self,
bundle_map: BundleMap,
Expand Down Expand Up @@ -655,6 +730,10 @@ def deploy(
print_diff=print_diff,
)

# 4. Execute post-deploy hooks
with self.use_package_warehouse():
self.execute_package_post_deploy_hooks()

if validate:
self.validate(use_scratch_stage=False)

Expand Down
16 changes: 13 additions & 3 deletions src/snowflake/cli/plugins/nativeapp/project_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
default_role,
)
from snowflake.cli.api.project.schemas.native_app.application import (
ApplicationPostDeployHook,
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
Expand Down Expand Up @@ -162,15 +162,25 @@ def app_role(self) -> str:
return self._default_role

@cached_property
def app_post_deploy_hooks(self) -> Optional[List[ApplicationPostDeployHook]]:
def app_post_deploy_hooks(self) -> Optional[List[PostDeployHook]]:
"""
List of application post deploy hooks.
List of application instance post deploy hooks.
"""
if self.definition.application and self.definition.application.post_deploy:
return self.definition.application.post_deploy
else:
return None

@cached_property
def package_post_deploy_hooks(self) -> Optional[List[PostDeployHook]]:
"""
List of application package post deploy hooks.
"""
if self.definition.package and self.definition.package.post_deploy:
return self.definition.package.post_deploy
else:
return None

@cached_property
def _default_role(self) -> str:
role = default_role()
Expand Down
8 changes: 2 additions & 6 deletions src/snowflake/cli/plugins/nativeapp/run_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from textwrap import dedent
from typing import Optional

import jinja2
import typer
from click import UsageError
from snowflake.cli.api.cli_global_context import cli_context
Expand All @@ -37,9 +36,6 @@
identifier_to_show_like_pattern,
unquote_identifier,
)
from snowflake.cli.api.rendering.sql_templates import (
get_sql_cli_jinja_env,
)
from snowflake.cli.api.utils.cursor import find_all_rows
from snowflake.cli.plugins.nativeapp.artifacts import BundleMap
from snowflake.cli.plugins.nativeapp.constants import (
Expand Down Expand Up @@ -309,7 +305,7 @@ def create_or_upgrade_app(
)

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

except ProgrammingError as err:
Expand Down Expand Up @@ -356,7 +352,7 @@ def create_or_upgrade_app(
)

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

except ProgrammingError as err:
generic_sql_error_handler(err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from functools import wraps
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, Optional, Union

from click import ClickException
from snowflake.cli.api.cli_global_context import cli_context, cli_context_manager
Expand All @@ -26,10 +26,6 @@
from snowflake.cli.api.project.schemas.entities.application_package_entity import (
ApplicationPackageEntity,
)
from snowflake.cli.api.project.schemas.native_app.application import (
ApplicationPostDeployHook,
SqlScriptHookType,
)
from snowflake.cli.api.project.schemas.native_app.path_mapping import PathMapping
from snowflake.cli.api.project.schemas.project_definition import (
DefinitionV11,
Expand All @@ -50,14 +46,6 @@ def _convert_v2_artifact_to_v1_dict(
return str(v2_artifact)


def _convert_v2_post_deploy_hook_to_v1_scripts(
v2_post_deploy_hook: ApplicationPostDeployHook,
) -> List[str]:
if isinstance(v2_post_deploy_hook, SqlScriptHookType):
return v2_post_deploy_hook.sql_script
raise ValueError(f"Unsupported post deploy hook type: {v2_post_deploy_hook}")


def _pdf_v2_to_v1(v2_definition: DefinitionV20) -> DefinitionV11:
pdfv1: Dict[str, Any] = {"definition_version": "1.1", "native_app": {}}

Expand Down Expand Up @@ -103,10 +91,16 @@ def _pdf_v2_to_v1(v2_definition: DefinitionV20) -> DefinitionV11:
"distribution"
] = app_package_definition.distribution
if app_package_definition.meta and app_package_definition.meta.post_deploy:
pdfv1["native_app"]["package"]["scripts"] = [
_convert_v2_post_deploy_hook_to_v1_scripts(s)
for s in app_package_definition.meta.post_deploy
]
pdfv1["native_app"]["package"][
"post_deploy"
] = app_package_definition.meta.post_deploy
if app_package_definition.meta:
if app_package_definition.meta.role:
pdfv1["native_app"]["package"]["role"] = app_package_definition.meta.role
if app_package_definition.meta.warehouse:
pdfv1["native_app"]["package"][
"warehouse"
] = app_package_definition.meta.warehouse

# Application
if app_definition:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ def process(
is_interactive=is_interactive,
)

# TODO: consider using self.deploy() instead

try:
self.create_app_package()
except ApplicationPackageAlreadyExistsError as e:
Expand All @@ -234,6 +236,8 @@ def process(
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 = (
Expand Down
Loading

0 comments on commit 3519b10

Please sign in to comment.