Skip to content

Commit

Permalink
SNOW-1544012 Validate that account has an event table set up (#1337)
Browse files Browse the repository at this point in the history
Add hidden `snow app events` command that will be used to fetch events for the app from the account's event table in the future. For now, this command just validates that the account has an event table set up or prints instructions linking to the docs if it's not set up.
  • Loading branch information
sfc-gh-fcampbell authored Jul 22, 2024
1 parent 5c57ac9 commit 50d86ff
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 1 deletion.
17 changes: 17 additions & 0 deletions src/snowflake/cli/plugins/nativeapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,20 @@ def app_validate(**options):

manager.validate(use_scratch_stage=True)
return MessageResult("Snowflake Native App validation succeeded.")


@app.command("events", hidden=True, requires_connection=True)
@with_project_definition()
@nativeapp_definition_v2_to_v1
def app_events(**options):
"""Fetches events for this app from the event table configured in Snowflake."""
# WIP: only validates event table setup for now while the command is hidden
assert_project_type("native_app")

manager = NativeAppManager(
project_definition=cli_context.project_definition.native_app,
project_root=cli_context.project_root,
)
events = manager.get_events()
if not events:
return MessageResult("No events found.")
17 changes: 17 additions & 0 deletions src/snowflake/cli/plugins/nativeapp/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,20 @@ class SetupScriptFailedValidation(ClickException):

def __init__(self):
super().__init__(self.__doc__)


class NoEventTableForAccount(ClickException):
"""No event table was found for this Snowflake account."""

INSTRUCTIONS = dedent(
"""\
Ask your Snowflake administrator to set up an event table for your account by following the docs at
https://docs.snowflake.com/en/developer-guide/logging-tracing/event-table-setting-up.
If your account is configured to send events to an organization event account, create a new
connection to this account using `snow connection add` and re-run this command using the new connection.
More information on event accounts is available at https://docs.snowflake.com/en/developer-guide/native-apps/setting-up-logging-and-events#configure-an-account-to-store-shared-events."""
)

def __init__(self):
super().__init__(f"{self.__doc__}\n\n{self.INSTRUCTIONS}")
14 changes: 13 additions & 1 deletion src/snowflake/cli/plugins/nativeapp/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
ApplicationPackageDoesNotExistError,
InvalidScriptError,
MissingScriptError,
NoEventTableForAccount,
SetupScriptFailedValidation,
UnexpectedOwnerError,
)
Expand All @@ -81,7 +82,7 @@
to_stage_path,
)
from snowflake.cli.plugins.stage.manager import StageManager
from snowflake.connector import ProgrammingError
from snowflake.connector import DictCursor, ProgrammingError

ApplicationOwnedObject = TypedDict("ApplicationOwnedObject", {"name": str, "type": str})

Expand Down Expand Up @@ -313,6 +314,12 @@ def get_app_pkg_distribution_in_snowflake(self) -> str:
)
)

@cached_property
def account_event_table(self) -> str | None:
query = "show parameters like 'event_table' in account"
results = self._execute_query(query, cursor_class=DictCursor)
return next((r["value"] for r in results if r["key"] == "EVENT_TABLE"), None)

def verify_project_distribution(
self, expected_distribution: Optional[str] = None
) -> bool:
Expand Down Expand Up @@ -707,6 +714,11 @@ def get_validation_result(self, use_scratch_stage: bool):
f"drop stage if exists {self.scratch_stage_fqn}"
)

def get_events(self) -> list[dict]:
if self.account_event_table is None:
raise NoEventTableForAccount()
return []


def _validation_item_to_str(item: dict[str, str | int]):
s = item["message"]
Expand Down
72 changes: 72 additions & 0 deletions tests/__snapshots__/test_help_messages.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,78 @@
+------------------------------------------------------------------------------+


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

Usage: default app events [OPTIONS]

Fetches events for this app from the event table configured in Snowflake.

+- Options --------------------------------------------------------------------+
| --project -p TEXT Path where Snowflake project resides. Defaults to |
| current working directory. |
| --env TEXT String in format of key=value. Overrides variables |
| from env section used for templating. |
| --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. |
| --token-file-path TEXT Path to file with an OAuth token |
| that should be used when |
| connecting to Snowflake |
| --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
86 changes: 86 additions & 0 deletions tests/nativeapp/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from snowflake.cli.plugins.nativeapp.exceptions import (
ApplicationPackageAlreadyExistsError,
ApplicationPackageDoesNotExistError,
NoEventTableForAccount,
SetupScriptFailedValidation,
UnexpectedOwnerError,
)
Expand All @@ -56,6 +57,7 @@
mock_get_app_pkg_distribution_in_sf,
)
from tests.nativeapp.utils import (
NATIVEAPP_MANAGER_ACCOUNT_EVENT_TABLE,
NATIVEAPP_MANAGER_BUILD_BUNDLE,
NATIVEAPP_MANAGER_DEPLOY,
NATIVEAPP_MANAGER_EXECUTE,
Expand Down Expand Up @@ -1297,3 +1299,87 @@ def test_validate_raw_returns_data(mock_execute, temp_dir, mock_cursor):
== failure_data
)
assert mock_execute.mock_calls == expected


@mock.patch(NATIVEAPP_MANAGER_EXECUTE)
def test_account_event_table(mock_execute, temp_dir, mock_cursor):
create_named_file(
file_name="snowflake.yml",
dir_name=temp_dir,
contents=[mock_snowflake_yml_file],
)

event_table = "db.schema.event_table"
side_effects, expected = mock_execute_helper(
[
(
mock_cursor([dict(key="EVENT_TABLE", value=event_table)], []),
mock.call(
"show parameters like 'event_table' in account",
cursor_class=DictCursor,
),
),
]
)
mock_execute.side_effect = side_effects

native_app_manager = _get_na_manager()
assert native_app_manager.account_event_table == event_table


@mock.patch(NATIVEAPP_MANAGER_EXECUTE)
def test_account_event_table_not_set_up(mock_execute, temp_dir, mock_cursor):
create_named_file(
file_name="snowflake.yml",
dir_name=temp_dir,
contents=[mock_snowflake_yml_file],
)

side_effects, expected = mock_execute_helper(
[
(
mock_cursor([], []),
mock.call(
"show parameters like 'event_table' in account",
cursor_class=DictCursor,
),
),
]
)
mock_execute.side_effect = side_effects

native_app_manager = _get_na_manager()
assert native_app_manager.account_event_table is None


@mock.patch(
NATIVEAPP_MANAGER_ACCOUNT_EVENT_TABLE,
return_value="db.schema.event_table",
new_callable=mock.PropertyMock,
)
def test_get_events(mock_account_event_table, temp_dir, mock_cursor):
create_named_file(
file_name="snowflake.yml",
dir_name=temp_dir,
contents=[mock_snowflake_yml_file],
)

native_app_manager = _get_na_manager()
assert native_app_manager.get_events() == []


@mock.patch(
NATIVEAPP_MANAGER_ACCOUNT_EVENT_TABLE,
return_value=None,
new_callable=mock.PropertyMock,
)
def test_get_events_no_event_table(mock_account_event_table, temp_dir, mock_cursor):
create_named_file(
file_name="snowflake.yml",
dir_name=temp_dir,
contents=[mock_snowflake_yml_file],
)

native_app_manager = _get_na_manager()
with pytest.raises(NoEventTableForAccount):
native_app_manager.get_events()
1 change: 1 addition & 0 deletions tests/nativeapp/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
NATIVEAPP_MANAGER_APP_PKG_DISTRIBUTION_IN_SF = (
f"{NATIVEAPP_MANAGER}.get_app_pkg_distribution_in_snowflake"
)
NATIVEAPP_MANAGER_ACCOUNT_EVENT_TABLE = f"{NATIVEAPP_MANAGER}.account_event_table"
NATIVEAPP_MANAGER_IS_APP_PKG_DISTRIBUTION_SAME = (
f"{NATIVEAPP_MANAGER}.verify_project_distribution"
)
Expand Down
45 changes: 45 additions & 0 deletions tests_integration/nativeapp/test_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright (c) 2024 Snowflake Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import uuid
from pathlib import Path

import pytest

from snowflake.cli.api.project.util import generate_user_env
from tests_integration.test_utils import pushd


USER_NAME = f"user_{uuid.uuid4().hex}"
TEST_ENV = generate_user_env(USER_NAME)


@pytest.mark.integration
def test_app_events(runner, temporary_working_directory):
project_name = "myapp"
result = runner.invoke_json(
["app", "init", project_name],
env=TEST_ENV,
)
assert result.exit_code == 0, result.output

with pushd(Path(os.getcwd(), project_name)):
# validate the account's event table
result = runner.invoke_with_connection(
["app", "events"],
env=TEST_ENV,
)
assert result.exit_code == 0, result.output
assert "No events found." in result.output

0 comments on commit 50d86ff

Please sign in to comment.