Skip to content

Commit

Permalink
[FEATURE] account params (#134)
Browse files Browse the repository at this point in the history
* account parameters

---------

Co-authored-by: TJ Murphy <[email protected]>
  • Loading branch information
teej and teej authored Oct 24, 2024
1 parent b575b38 commit 5f658f4
Show file tree
Hide file tree
Showing 21 changed files with 289 additions and 37 deletions.
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

* [APIAuthenticationSecurityIntegration](resources/api_authentication_security_integration.md)
* [APIIntegration](resources/api_integration.md)
* [AccountParameter](resources/account_parameter.md)
* [AggregationPolicy](resources/aggregation_policy.md)
* [Alert](resources/alert.md)
* [AuthenticationPolicy](resources/authentication_policy.md)
Expand Down
39 changes: 39 additions & 0 deletions docs/resources/account_parameter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
description: >-
---

# AccountParameter

[Snowflake Documentation](https://docs.snowflake.com/en/sql-reference/sql/alter-account)

An account parameter in Snowflake that allows you to set or alter account-level parameters.


## Examples

### Python

```python
account_parameter = AccountParameter(
name="some_parameter",
value="some_value",
)
```


### YAML

```yaml
account_parameters:
- name: some_parameter
value: some_value
```
## Fields
* `name` (string, required) - The name of the account parameter.
* `value` ([Any](any.md), required) - The value to set for the account parameter.


4 changes: 2 additions & 2 deletions docs/resources/javascript_udf.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ js_udf = JavascriptUDF(
name="some_function",
returns="STRING",
as_="function(x) { return x.toUpperCase(); }",
args=[{"name": "x", "type": "STRING"}],
args=[{"name": "x", "data_type": "STRING"}],
comment="Converts a string to uppercase",
)
```
Expand All @@ -34,7 +34,7 @@ functions:
as_: function(x) { return x.toUpperCase(); }
args:
- name: x
type: STRING
data_type: STRING
comment: Converts a string to uppercase
```
Expand Down
4 changes: 2 additions & 2 deletions docs/resources/python_udf.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ python_udf = PythonUDF(
returns="string",
runtime_version="3.8",
handler="process_data",
args=[{"name": "input_data", "type": "string"}],
args=[{"name": "input_data", "data_type": "string"}],
as_="process_data_function",
comment="This function processes data.",
copy_grants=False,
Expand All @@ -47,7 +47,7 @@ python_udfs:
handler: process_data
args:
- name: input_data
type: string
data_type: string
as_: process_data_function
comment: This function processes data.
copy_grants: false
Expand Down
12 changes: 12 additions & 0 deletions examples/account-parameters.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: account-parameters-titan-example
run_mode: sync
allowlist:
- account parameter

account_parameters:
- name: ALLOW_CLIENT_MFA_CACHING
value: true
- name: ALLOW_ID_TOKEN
value: true
- name: TIMEZONE
value: "America/New_York"
4 changes: 4 additions & 0 deletions tests/fixtures/json/account_parameter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "MAX_CONCURRENCY_LEVEL",
"value": 7
}
1 change: 1 addition & 0 deletions tests/fixtures/sql/account_parameter.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER ACCOUNT SET MAX_CONCURRENCY_LEVEL = 7;
5 changes: 2 additions & 3 deletions tests/integration/data_provider/test_list_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
from tests.helpers import get_json_fixtures
from titan import data_provider
from titan.client import UNSUPPORTED_FEATURE, reset_cache
from titan.enums import AccountEdition
from titan.identifiers import resource_label_for_type
from titan.resources import Database, Resource
from titan.resources import AccountParameter, Database, Resource
from titan.scope import DatabaseScope, SchemaScope

pytestmark = pytest.mark.requires_snowflake
Expand All @@ -27,7 +26,7 @@
)
def resource(request, suffix):
resource_cls, data = request.param
if "name" in data:
if "name" in data and resource_cls != AccountParameter:
data["name"] += f"_{suffix}_list_resources"
if "login_name" in data:
data["login_name"] += f"_{suffix}_list_resources"
Expand Down
22 changes: 22 additions & 0 deletions tests/integration/test_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ def test_blueprint_all_grant_triggers_create(cursor, test_db, role):
assert isinstance(plan[0], CreateResource)


@pytest.mark.skip(reason="This test requires blueprint scopes")
def test_blueprint_sync_dont_remove_system_schemas(cursor, suffix):
session = cursor.connection
db_name = f"BLUEPRINT_SYNC_DONT_REMOVE_SYSTEM_SCHEMAS_{suffix}"
Expand All @@ -261,6 +262,7 @@ def test_blueprint_sync_dont_remove_system_schemas(cursor, suffix):
cursor.execute(f"DROP DATABASE IF EXISTS {db_name}")


@pytest.mark.skip(reason="This test requires blueprint scopes")
def test_blueprint_sync_resource_missing_from_remote_state(cursor, test_db):
session = cursor.connection
blueprint = Blueprint(
Expand All @@ -278,6 +280,7 @@ def test_blueprint_sync_resource_missing_from_remote_state(cursor, test_db):
assert plan[0].urn.fqn.name == "ABSENT"


@pytest.mark.skip(reason="This test requires blueprint scopes")
def test_blueprint_sync_plan_matches_remote_state(cursor, test_db):
session = cursor.connection
cursor.execute(f"CREATE SCHEMA IF NOT EXISTS {test_db}.PRESENT")
Expand All @@ -294,6 +297,7 @@ def test_blueprint_sync_plan_matches_remote_state(cursor, test_db):
assert len(plan) == 0


@pytest.mark.skip(reason="This test requires blueprint scopes")
def test_blueprint_sync_remote_state_contains_extra_resource(cursor, test_db):
session = cursor.connection
cursor.execute(f"CREATE SCHEMA IF NOT EXISTS {test_db}.PRESENT")
Expand Down Expand Up @@ -470,3 +474,21 @@ def _database():
assert len(plan) == 0
finally:
cursor.execute(f"DROP DATABASE IF EXISTS {db_name}")


def test_blueprint_account_parameters_sync_drift(cursor):
cursor.execute("ALTER ACCOUNT SET MAX_CONCURRENCY_LEVEL = 7")
session = cursor.connection
try:
blueprint = Blueprint(
name="test_account_parameters_sync_drift",
run_mode="sync",
allowlist=[ResourceType.ACCOUNT_PARAMETER],
)
plan = blueprint.plan(session)
assert len(plan) > 0
max_concurrency_level = next((r for r in plan if r.urn.fqn.name == "MAX_CONCURRENCY_LEVEL"), None)
assert max_concurrency_level is not None
assert isinstance(max_concurrency_level, DropResource)
finally:
cursor.execute("ALTER ACCOUNT UNSET MAX_CONCURRENCY_LEVEL")
7 changes: 6 additions & 1 deletion tests/integration/test_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ def resource(request):
def test_create_drop_from_json(resource, cursor, suffix):

# Not easily testable without flakiness
if resource.__class__ in (res.Service,):
if resource.__class__ in (
res.Service,
res.Grant,
res.RoleGrant,
res.FutureGrant,
):
pytest.skip("Skipping")

lifecycle_db = f"LIFECYCLE_DB_{suffix}_{resource.__class__.__name__}"
Expand Down
4 changes: 3 additions & 1 deletion tests/test_identities.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest

from dataclasses import fields
from typing import get_args, get_origin
from typing import Any, get_args, get_origin

from tests.helpers import get_json_fixtures
from titan.data_types import convert_to_canonical_data_type
Expand Down Expand Up @@ -38,6 +38,8 @@ def resource(request):


def _field_type_is_serialized_as_resource_name(field):
if field.type is Any:
return False
if field.type is RoleRef:
return True
if field.type is ResourceName:
Expand Down
10 changes: 9 additions & 1 deletion titan/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,11 @@ def execution_strategy_for_change(
return ResourceName("ACCOUNTADMIN"), False
raise MissingPrivilegeException("ACCOUNTADMIN role is required to work with resource monitors")

elif change.urn.resource_type == ResourceType.ACCOUNT_PARAMETER:
if "ACCOUNTADMIN" in available_roles:
return ResourceName("ACCOUNTADMIN"), False
raise MissingPrivilegeException("ACCOUNTADMIN role is required to work with account parameters")

elif isinstance(change, (UpdateResource, DropResource, TransferOwnership)):
if change_owner:
return change_owner, False
Expand Down Expand Up @@ -1300,8 +1305,11 @@ def _sort_destructive_changes(
# Not quite right but close enough for now.
def sort_key(change: ResourceChange) -> tuple:
return (
# Put network policies first
change.urn.resource_type != ResourceType.NETWORK_POLICY,
change.urn.resource_type != ResourceType.ROLE,
# Put roles and role grants last
change.urn.resource_type == ResourceType.ROLE_GRANT,
change.urn.resource_type == ResourceType.ROLE,
change.urn.database is not None,
change.urn.schema is not None,
-1 * sort_order[change.urn],
Expand Down
45 changes: 37 additions & 8 deletions titan/data_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,17 +337,21 @@ def _parse_storage_location(storage_location_str: str) -> Optional[dict]:
return storage_location


def _cast_param_value(raw_value: str, param_type: str) -> Any:
if param_type == "BOOLEAN":
return raw_value == "true"
elif param_type == "NUMBER":
return int(raw_value)
elif param_type == "STRING":
return str(raw_value) if raw_value else None
else:
return raw_value


def params_result_to_dict(params_result):
params = {}
for param in params_result:
if param["type"] == "BOOLEAN":
typed_value = param["value"] == "true"
elif param["type"] == "NUMBER":
typed_value = int(param["value"])
elif param["type"] == "STRING":
typed_value = str(param["value"]) if param["value"] else None
else:
typed_value = param["value"]
typed_value = _cast_param_value(param["value"], param["type"])
params[param["key"].lower()] = typed_value
return params

Expand Down Expand Up @@ -617,6 +621,20 @@ def fetch_account(session: SnowflakeConnection, fqn: FQN):
}


def fetch_account_parameter(session: SnowflakeConnection, fqn: FQN):
show_result = execute(session, "SHOW PARAMETERS IN ACCOUNT", cacheable=True)
account_parameters = _filter_result(show_result, key=fqn.name, level="ACCOUNT")
if len(account_parameters) == 0:
return None
if len(account_parameters) > 1:
raise Exception(f"Found multiple account parameters matching {fqn}")
data = account_parameters[0]
return {
"name": ResourceName(data["key"]),
"value": _cast_param_value(data["value"], data["type"]),
}


def fetch_aggregation_policy(session: SnowflakeConnection, fqn: FQN):
show_result = _show_resources(session, "AGGREGATION POLICIES", fqn)
if len(show_result) == 0:
Expand Down Expand Up @@ -2217,6 +2235,17 @@ def list_schema_scoped_resource(session: SnowflakeConnection, resource) -> list[
######## List functions by resource


def list_account_parameters(session: SnowflakeConnection) -> list[FQN]:
show_result = execute(session, "SHOW PARAMETERS IN ACCOUNT")
account_parameters = []
for row in show_result:
# Skip system parameters and unset parameters
if row["level"] != "ACCOUNT":
continue
account_parameters.append(FQN(name=ResourceName(row["key"])))
return account_parameters


def list_alerts(session: SnowflakeConnection) -> list[FQN]:
return list_schema_scoped_resource(session, "ALERTS")

Expand Down
3 changes: 2 additions & 1 deletion titan/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ class RunMode(ParseableEnum):


class ResourceType(ParseableEnum):
EXTERNAL_VOLUME_STORAGE_LOCATION = "EXTERNAL VOLUME STORAGE LOCATION"
ACCOUNT = "ACCOUNT"
ACCOUNT_PARAMETER = "ACCOUNT PARAMETER"
AGGREGATION_POLICY = "AGGREGATION POLICY"
ALERT = "ALERT"
API_INTEGRATION = "API INTEGRATION"
Expand All @@ -62,6 +62,7 @@ class ResourceType(ParseableEnum):
EXTERNAL_ACCESS_INTEGRATION = "EXTERNAL ACCESS INTEGRATION"
EXTERNAL_FUNCTION = "EXTERNAL FUNCTION"
EXTERNAL_VOLUME = "EXTERNAL VOLUME"
EXTERNAL_VOLUME_STORAGE_LOCATION = "EXTERNAL VOLUME STORAGE LOCATION"
FAILOVER_GROUP = "FAILOVER GROUP"
FILE_FORMAT = "FILE FORMAT"
FUNCTION = "FUNCTION"
Expand Down
13 changes: 1 addition & 12 deletions titan/gitops.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

ALIASES = {
"grants_on_all": ResourceType.GRANT_ON_ALL,
"account_parameters": ResourceType.ACCOUNT_PARAMETER,
}


Expand Down Expand Up @@ -117,7 +118,6 @@ def _resources_for_config(config: dict):
# Special cases
database_config = config.pop("databases", [])
role_grants = config.pop("role_grants", [])
# grants = config.pop("grants", [])
users = config.pop("users", [])

resources = []
Expand Down Expand Up @@ -155,7 +155,6 @@ def _resources_for_config(config: dict):

resources.extend(_resources_from_database_config(database_config))
resources.extend(_resources_from_role_grants_config(role_grants))
# resources.extend(_resources_from_grants_config(grants))
resources.extend(_resources_from_users_config(users))

resource_cache = {}
Expand All @@ -169,16 +168,6 @@ def _resources_for_config(config: dict):
if cache_pointer in resource_cache:
resource._data.on = ResourceName(str(resource_cache[cache_pointer].fqn))

# TODO: investigate this
# for ref in resource.refs:
# cache_pointer = (ref.resource_type, ResourceName(ref.name))
# if (
# isinstance(ref, ResourcePointer)
# and cache_pointer in resource_cache
# and resource_cache[cache_pointer]._container is not None
# ):
# ref._container = resource_cache[cache_pointer]._container

return resources


Expand Down
Loading

0 comments on commit 5f658f4

Please sign in to comment.