Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] account params #134

Merged
merged 3 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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")

Check warning on line 977 in titan/blueprint.py

View check run for this annotation

Codecov / codecov/patch

titan/blueprint.py#L977

Added line #L977 was not covered by tests

elif isinstance(change, (UpdateResource, DropResource, TransferOwnership)):
if change_owner:
return change_owner, False
Expand Down Expand Up @@ -1300,8 +1305,11 @@
# 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 @@
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

Check warning on line 348 in titan/data_provider.py

View check run for this annotation

Codecov / codecov/patch

titan/data_provider.py#L348

Added line #L348 was not covered by tests


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_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}")

Check warning on line 630 in titan/data_provider.py

View check run for this annotation

Codecov / codecov/patch

titan/data_provider.py#L630

Added line #L630 was not covered by tests
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 @@
######## 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
Loading