Skip to content

Commit

Permalink
Append SNOWFLAKE_CLI_RESOURCE_SUFFIX value to app and package name
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-fcampbell committed Aug 14, 2024
1 parent 7ed0a38 commit 4125642
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 26 deletions.
71 changes: 57 additions & 14 deletions src/snowflake/cli/_plugins/nativeapp/project_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,31 @@

from __future__ import annotations

import os
from functools import cached_property
from pathlib import Path
from typing import List, Optional

from snowflake.cli._plugins.nativeapp.artifacts import resolve_without_follow
from snowflake.cli._plugins.nativeapp.bundle_context import BundleContext
from snowflake.cli.api.cli_global_context import get_cli_context
from snowflake.cli.api.project.definition import (
default_app_package,
default_application,
default_role,
)
from snowflake.cli.api.project.definition import DEFAULT_USERNAME, default_role
from snowflake.cli.api.project.schemas.native_app.application 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 extract_schema, to_identifier
from snowflake.cli.api.project.util import (
clean_identifier,
concat_identifiers,
extract_schema,
get_env_username,
to_identifier,
)
from snowflake.connector import DictCursor

RESOURCE_SUFFIX_VAR = "SNOWFLAKE_CLI_TEST_RESOURCE_SUFFIX"


def current_role() -> str:
conn = get_cli_context().connection
Expand Down Expand Up @@ -129,12 +134,24 @@ def project_identifier(self) -> str:
# sometimes strip out double quotes, so we try to get them back here.
return to_identifier(self.definition.name)

@cached_property
@property
def package_name(self) -> str:
# If an explicit package name is set, use it and append the resource suffix
# In this case, if the suffix is empty we don't append the default suffix
# since we want to honor the user's chosen package name
suffix = resource_suffix()
if self.definition.package and self.definition.package.name:
return to_identifier(self.definition.package.name)
else:
return to_identifier(default_app_package(self.project_identifier))
return concat_identifiers(
[to_identifier(self.definition.package.name), suffix]
)

# If there's no explicit package name set in the project definition,
# generate a name for the package from the project identifier and
# append the resource suffix
# If we don't have a resource suffix specified, use the default one
return concat_identifiers(
[self.project_identifier, "_pkg", suffix or default_resource_suffix()]
)

@cached_property
def package_role(self) -> str:
Expand All @@ -150,12 +167,24 @@ def package_distribution(self) -> str:
else:
return "internal"

@cached_property
@property
def app_name(self) -> str:
# If an explicit app name is set, use it and append the resource suffix
# In this case, if the suffix is empty we don't append the default suffix
# since we want to honor the user's chosen app name
suffix = resource_suffix()
if self.definition.application and self.definition.application.name:
return to_identifier(self.definition.application.name)
else:
return to_identifier(default_application(self.project_identifier))
return concat_identifiers(
[to_identifier(self.definition.application.name), suffix]
)

# If there's no explicit package name set in the project definition,
# generate a name for the package from the project identifier and
# append the resource suffix.
# If we don't have a resource suffix specified, use the default one
return concat_identifiers(
[self.project_identifier, suffix or default_resource_suffix()]
)

@cached_property
def app_role(self) -> str:
Expand Down Expand Up @@ -206,3 +235,17 @@ def get_bundle_context(self) -> BundleContext:
deploy_root=self.deploy_root,
generated_root=self.generated_root,
)


def resource_suffix() -> str:
"""
A suffix that should be added to account-level Native App resources.
This is an internal concern that is currently only used in tests.
"""
return os.environ.get(RESOURCE_SUFFIX_VAR, "")


def default_resource_suffix():
user = clean_identifier(get_env_username() or DEFAULT_USERNAME)
return f"_{user}"
10 changes: 0 additions & 10 deletions src/snowflake/cli/api/project/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,6 @@ def generate_local_override_yml(
return project.update_from_dict(local)


def default_app_package(project_name: str):
user = clean_identifier(get_env_username() or DEFAULT_USERNAME)
return append_to_identifier(to_identifier(project_name), f"_pkg_{user}")


def default_role():
conn = get_cli_context().connection
return conn.role


def default_application(project_name: str):
user = clean_identifier(get_env_username() or DEFAULT_USERNAME)
return append_to_identifier(to_identifier(project_name), f"_{user}")
87 changes: 85 additions & 2 deletions tests/nativeapp/test_project_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
import yaml
from snowflake.cli._plugins.nativeapp.bundle_context import BundleContext
from snowflake.cli._plugins.nativeapp.project_model import NativeAppProjectModel
from snowflake.cli.api.project.definition import default_app_package, load_project
from snowflake.cli.api.project.definition import (
RESOURCE_SUFFIX_VAR,
load_project,
)
from snowflake.cli.api.project.schemas.native_app.application import SqlScriptHookType
from snowflake.cli.api.project.schemas.native_app.path_mapping import PathMapping
from snowflake.cli.api.project.schemas.project_definition import (
Expand Down Expand Up @@ -79,6 +82,31 @@ def test_project_model_all_defaults(
assert project.debug_mode is None


@pytest.mark.parametrize("project_definition_files", ["minimal"], indirect=True)
@mock.patch("snowflake.cli._app.snow_connector.connect_to_snowflake")
@mock.patch.dict(
os.environ,
{"USER": "test_user", RESOURCE_SUFFIX_VAR: "_suffix!"},
clear=True,
)
def test_project_model_default_package_app_name_with_suffix(
mock_connect, project_definition_files: List[Path], mock_ctx
):
ctx = mock_ctx()
mock_connect.return_value = ctx

project_defn = load_project(project_definition_files).project_definition

project_dir = Path().resolve()
project = NativeAppProjectModel(
project_definition=project_defn.native_app,
project_root=project_dir,
)

assert project.package_name == '"minimal_pkg_suffix!"'
assert project.app_name == '"minimal_suffix!"'


@mock.patch("snowflake.cli._app.snow_connector.connect_to_snowflake")
@mock.patch.dict(os.environ, {"USER": "test_user"}, clear=True)
def test_project_model_all_explicit(mock_connect, mock_ctx):
Expand Down Expand Up @@ -153,6 +181,61 @@ def test_project_model_all_explicit(mock_connect, mock_ctx):
assert project.debug_mode is False


@pytest.mark.parametrize("project_definition_files", ["minimal"], indirect=True)
@mock.patch("snowflake.cli._app.snow_connector.connect_to_snowflake")
@mock.patch.dict(
os.environ,
{"USER": "test_user", RESOURCE_SUFFIX_VAR: "_suffix!"},
clear=True,
)
def test_project_model_explicit_package_app_name_with_suffix(
mock_connect, project_definition_files: List[Path], mock_ctx
):
ctx = mock_ctx()
mock_connect.return_value = ctx

project_defition_file_yml = dedent(
f"""
definition_version: 1.1
native_app:
name: minimal
artifacts:
- setup.sql
- README.md
package:
name: minimal_test_pkg
role: PkgRole
distribution: external
warehouse: PkgWarehouse
scripts:
- scripts/package_setup.sql
application:
name: minimal_test_app
warehouse: AppWarehouse
role: AppRole
debug: false
post_deploy:
- sql_script: scripts/app_setup.sql
"""
)

project_defn = build_project_definition(
**yaml.load(project_defition_file_yml, Loader=yaml.BaseLoader)
)
project_dir = Path().resolve()
project = NativeAppProjectModel(
project_definition=project_defn.native_app,
project_root=project_dir,
)

assert project.package_name == '"minimal_test_pkg_suffix!"'
assert project.app_name == '"minimal_test_app_suffix!"'


@pytest.mark.parametrize("project_definition_files", ["minimal"], indirect=True)
@mock.patch("snowflake.cli._app.snow_connector.connect_to_snowflake")
@mock.patch.dict(os.environ, {"USER": "test_user"}, clear=True)
Expand Down Expand Up @@ -188,7 +271,7 @@ def test_bundle_context_from_project_model(project_definition_files: List[Path])
actual_bundle_ctx = project.get_bundle_context()

expected_bundle_ctx = BundleContext(
package_name=default_app_package("minimal"),
package_name=project.package_name,
artifacts=[
PathMapping(src="setup.sql", dest=None),
PathMapping(src="README.md", dest=None),
Expand Down
82 changes: 82 additions & 0 deletions tests_integration/nativeapp/test_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import os
import uuid

from snowflake.cli._plugins.nativeapp.project_model import RESOURCE_SUFFIX_VAR
from snowflake.cli.api.project.util import generate_user_env


Expand Down Expand Up @@ -112,6 +113,87 @@ def test_nativeapp_deploy(
assert result.exit_code == 0


@pytest.mark.integration
@enable_definition_v2_feature_flag
@pytest.mark.parametrize("test_project", ["napp_init_v1", "napp_init_v2"])
def test_nativeapp_deploy_with_resource_suffix(
test_project,
project_directory,
runner,
snowflake_session,
):
suffix = f"_some_suffix_{uuid.uuid4().hex}"
test_env_with_suffix = TEST_ENV | {RESOURCE_SUFFIX_VAR: suffix}
with project_directory(test_project):
result = runner.invoke_with_connection(
["app", "deploy"],
env=test_env_with_suffix,
)
assert result.exit_code == 0

try:
# package exist
assert row_from_snowflake_session(
snowflake_session.execute_string(
f"show application packages like '%{suffix}'",
)
)

# make sure we always delete the app
result = runner.invoke_with_connection_json(
["app", "teardown"],
env=test_env_with_suffix,
)
assert result.exit_code == 0
finally:
# teardown is idempotent, so we can execute it again with no ill effects
result = runner.invoke_with_connection_json(
["app", "teardown", "--force"],
env=test_env_with_suffix,
)
assert result.exit_code == 0


@pytest.mark.integration
@enable_definition_v2_feature_flag
@pytest.mark.parametrize("test_project", ["napp_init_v1", "napp_init_v2"])
def test_nativeapp_deploy_with_resource_suffix_quoted(
test_project,
project_directory,
runner,
snowflake_session,
):
suffix = f"_must.be.quoted!!!_{uuid.uuid4().hex}"
test_env_with_quoted_suffix = TEST_ENV | {RESOURCE_SUFFIX_VAR: suffix}
with project_directory(test_project):
result = runner.invoke_with_connection(
["app", "deploy"],
env=test_env_with_quoted_suffix,
)
assert result.exit_code == 0

try:
# package exist
assert row_from_snowflake_session(
snowflake_session.execute_string(
f"show application packages like '%{suffix}'",
)
)
# make sure we always delete the app
result = runner.invoke_with_connection_json(
["app", "teardown"],
env=test_env_with_quoted_suffix,
)
assert result.exit_code == 0
finally:
# teardown is idempotent, so we can execute it again with no ill effects
result = runner.invoke_with_connection_json(
["app", "teardown", "--force"],
env=test_env_with_quoted_suffix,
)
assert result.exit_code == 0


@pytest.mark.integration
@enable_definition_v2_feature_flag
@pytest.mark.parametrize(
Expand Down
Loading

0 comments on commit 4125642

Please sign in to comment.