Skip to content

Commit ec2cc02

Browse files
feat: [SNOW-2155083] spcs service: support auto-suspend-secs (#2487)
* feat: [SNOW-2155083] add auto_suspend_secs parameter support Added entry for new auto_suspend_secs parameter functionality in SPCS service commands (deploy, set, unset) that allows configuring automatic service suspension after inactivity period. * feat: [SNOW-2155083] add support for auto-suspend-secs in spcs services
1 parent 8ddec6c commit ec2cc02

File tree

10 files changed

+241
-25
lines changed

10 files changed

+241
-25
lines changed

RELEASE-NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
## New additions
2222
* Added global option `--decimal-precision` allowing setting arbitrary precision for Python's `Decimal` type.
23+
* Added support for `auto_suspend_secs` parameter in SPCS service commands (`deploy`, `set`, `unset`) to configure automatic service suspension after inactivity period.
2324

2425
## Fixes and improvements
2526
* Bumped `snowflake-connector-python==3.18.0`

src/snowflake/cli/_plugins/spcs/services/commands.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,14 @@ def _service_name_callback(name: FQN) -> FQN:
151151
help=_AUTO_RESUME_HELP,
152152
)
153153

154+
_AUTO_SUSPEND_SECS_HELP = "Number of seconds of inactivity after which the service will be automatically suspended."
155+
AutoSuspendSecsOption = OverrideableOption(
156+
None,
157+
"--auto-suspend-secs",
158+
help=_AUTO_SUSPEND_SECS_HELP,
159+
min=0,
160+
)
161+
154162
_COMMENT_HELP = "Comment for the service."
155163

156164
add_object_command_aliases(
@@ -217,7 +225,7 @@ def deploy(
217225
upgrade: bool = typer.Option(
218226
False,
219227
"--upgrade",
220-
help="Updates the existing service. Can update min_instances, max_instances, query_warehouse, auto_resume, external_access_integrations and comment.",
228+
help="Updates the existing service. Can update min_instances, max_instances, query_warehouse, auto_resume, auto_suspend_secs, external_access_integrations and comment.",
221229
),
222230
**options,
223231
) -> CommandResult:
@@ -241,6 +249,7 @@ def deploy(
241249
min_instances=service.min_instances,
242250
max_instances=max_instances,
243251
auto_resume=service.auto_resume,
252+
auto_suspend_secs=service.auto_suspend_secs,
244253
external_access_integrations=service.external_access_integrations,
245254
query_warehouse=service.query_warehouse,
246255
tags=service.tags,
@@ -529,6 +538,7 @@ def set_property(
529538
max_instances: Optional[int] = MaxInstancesOption(show_default=False),
530539
query_warehouse: Optional[str] = QueryWarehouseOption(show_default=False),
531540
auto_resume: Optional[bool] = AutoResumeOption(default=None, show_default=False),
541+
auto_suspend_secs: Optional[int] = AutoSuspendSecsOption(show_default=False),
532542
external_access_integrations: Optional[List[str]] = typer.Option(
533543
None,
534544
"--eai-name",
@@ -546,6 +556,7 @@ def set_property(
546556
max_instances=max_instances,
547557
query_warehouse=query_warehouse,
548558
auto_resume=auto_resume,
559+
auto_suspend_secs=auto_suspend_secs,
549560
external_access_integrations=external_access_integrations,
550561
comment=comment,
551562
)
@@ -576,6 +587,12 @@ def unset_property(
576587
help=f"Reset the AUTO_RESUME property - {_AUTO_RESUME_HELP}",
577588
show_default=False,
578589
),
590+
auto_suspend_secs: bool = AutoSuspendSecsOption(
591+
default=False,
592+
param_decls=["--auto-suspend-secs"],
593+
help=f"Reset the AUTO_SUSPEND_SECS property - {_AUTO_SUSPEND_SECS_HELP}",
594+
show_default=False,
595+
),
579596
comment: bool = CommentOption(
580597
default=False,
581598
help=f"Reset the COMMENT property - {_COMMENT_HELP}",
@@ -593,6 +610,7 @@ def unset_property(
593610
max_instances=max_instances,
594611
query_warehouse=query_warehouse,
595612
auto_resume=auto_resume,
613+
auto_suspend_secs=auto_suspend_secs,
596614
comment=comment,
597615
)
598616
return SingleQueryResult(cursor)

src/snowflake/cli/_plugins/spcs/services/manager.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ def deploy(
114114
min_instances: int,
115115
max_instances: int,
116116
auto_resume: bool,
117+
auto_suspend_secs: Optional[int],
117118
external_access_integrations: Optional[List[str]],
118119
query_warehouse: Optional[str],
119120
tags: Optional[List[Tag]],
@@ -139,6 +140,7 @@ def deploy(
139140
max_instances=max_instances,
140141
query_warehouse=query_warehouse,
141142
auto_resume=auto_resume,
143+
auto_suspend_secs=auto_suspend_secs,
142144
external_access_integrations=external_access_integrations,
143145
comment=comment,
144146
)
@@ -163,6 +165,9 @@ def deploy(
163165
if max_instances:
164166
query.append(f"MAX_INSTANCES = {max_instances}")
165167

168+
if auto_suspend_secs is not None:
169+
query.append(f"AUTO_SUSPEND_SECS = {auto_suspend_secs}")
170+
166171
if query_warehouse:
167172
query.append(f"QUERY_WAREHOUSE = {query_warehouse}")
168173

@@ -532,6 +537,7 @@ def set_property(
532537
max_instances: Optional[int],
533538
query_warehouse: Optional[str],
534539
auto_resume: Optional[bool],
540+
auto_suspend_secs: Optional[int],
535541
external_access_integrations: Optional[List[str]],
536542
comment: Optional[str],
537543
):
@@ -540,6 +546,7 @@ def set_property(
540546
("max_instances", max_instances),
541547
("query_warehouse", query_warehouse),
542548
("auto_resume", auto_resume),
549+
("auto_suspend_secs", auto_suspend_secs),
543550
("external_access_integrations", external_access_integrations),
544551
("comment", comment),
545552
]
@@ -563,6 +570,9 @@ def set_property(
563570
if auto_resume is not None:
564571
query.append(f" auto_resume = {auto_resume}")
565572

573+
if auto_suspend_secs is not None:
574+
query.append(f" auto_suspend_secs = {auto_suspend_secs}")
575+
566576
if external_access_integrations is not None:
567577
external_access_integration_list = ",".join(
568578
f"{e}" for e in external_access_integrations
@@ -583,13 +593,15 @@ def unset_property(
583593
max_instances: bool,
584594
query_warehouse: bool,
585595
auto_resume: bool,
596+
auto_suspend_secs: bool,
586597
comment: bool,
587598
):
588599
property_pairs = [
589600
("min_instances", min_instances),
590601
("max_instances", max_instances),
591602
("query_warehouse", query_warehouse),
592603
("auto_resume", auto_resume),
604+
("auto_suspend_secs", auto_suspend_secs),
593605
("comment", comment),
594606
]
595607

src/snowflake/cli/_plugins/spcs/services/service_entity_model.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ class ServiceEntityModel(EntityModelBaseWithArtifacts, ExternalAccessBaseModel):
3030
title="The service will automatically resume when a service function or ingress is called.",
3131
default=True,
3232
)
33+
auto_suspend_secs: Optional[int] = Field(
34+
title="Number of seconds of inactivity after which the service is automatically suspended.",
35+
default=None,
36+
ge=0,
37+
)
3338
query_warehouse: Optional[str] = Field(
3439
title="Warehouse to use if a service container connects to Snowflake to execute a query without explicitly specifying a warehouse to use",
3540
default=None,

tests/__snapshots__/test_help_messages.ambr

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16583,8 +16583,8 @@
1658316583
+- Options --------------------------------------------------------------------+
1658416584
| --upgrade Updates the existing service. Can update |
1658516585
| min_instances, max_instances, query_warehouse, |
16586-
| auto_resume, external_access_integrations and |
16587-
| comment. |
16586+
| auto_resume, auto_suspend_secs, |
16587+
| external_access_integrations and comment. |
1658816588
| --project -p TEXT Path where the Snowflake project is stored. |
1658916589
| Defaults to the current working directory. |
1659016590
| --env TEXT String in the format key=value. Overrides variables |
@@ -18335,6 +18335,13 @@
1833518335
| service function |
1833618336
| or ingress is |
1833718337
| called. |
18338+
| --auto-suspend-s… INTEGER RANGE Number of |
18339+
| [x>=0] seconds of |
18340+
| inactivity after |
18341+
| which the |
18342+
| service will be |
18343+
| automatically |
18344+
| suspended. |
1833818345
| --eai-name TEXT Identifies |
1833918346
| external access |
1834018347
| integrations |
@@ -18774,20 +18781,24 @@
1877418781
| [required] |
1877518782
+------------------------------------------------------------------------------+
1877618783
+- Options --------------------------------------------------------------------+
18777-
| --min-instances Reset the MIN_INSTANCES property - Minimum |
18778-
| number of service instances to run. |
18779-
| --max-instances Reset the MAX_INSTANCES property - Maximum |
18780-
| number of service instances to run. |
18781-
| --query-warehouse Reset the QUERY_WAREHOUSE property - Warehouse |
18782-
| to use if a service container connects to |
18783-
| Snowflake to execute a query without explicitly |
18784-
| specifying a warehouse to use. |
18785-
| --auto-resume Reset the AUTO_RESUME property - The service |
18786-
| will automatically resume when a service |
18787-
| function or ingress is called. |
18788-
| --comment Reset the COMMENT property - Comment for the |
18789-
| service. |
18790-
| --help -h Show this message and exit. |
18784+
| --min-instances Reset the MIN_INSTANCES property - Minimum |
18785+
| number of service instances to run. |
18786+
| --max-instances Reset the MAX_INSTANCES property - Maximum |
18787+
| number of service instances to run. |
18788+
| --query-warehouse Reset the QUERY_WAREHOUSE property - |
18789+
| Warehouse to use if a service container |
18790+
| connects to Snowflake to execute a query |
18791+
| without explicitly specifying a warehouse to |
18792+
| use. |
18793+
| --auto-resume Reset the AUTO_RESUME property - The service |
18794+
| will automatically resume when a service |
18795+
| function or ingress is called. |
18796+
| --auto-suspend-secs Reset the AUTO_SUSPEND_SECS property - Number |
18797+
| of seconds of inactivity after which the |
18798+
| service will be automatically suspended. |
18799+
| --comment Reset the COMMENT property - Comment for the |
18800+
| service. |
18801+
| --help -h Show this message and exit. |
1879118802
+------------------------------------------------------------------------------+
1879218803
+- Connection configuration ---------------------------------------------------+
1879318804
| --connection,--environment -c TEXT Name of the connection, as |

tests/spcs/test_services.py

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,6 +1531,7 @@ def test_set_property(mock_execute_query):
15311531
max_instances = 3
15321532
query_warehouse = "test_warehouse"
15331533
auto_resume = False
1534+
auto_suspend_secs = 600
15341535
external_access_integrations = [
15351536
"google_apis_access_integration",
15361537
"salesforce_api_access_integration",
@@ -1544,6 +1545,7 @@ def test_set_property(mock_execute_query):
15441545
max_instances=max_instances,
15451546
query_warehouse=query_warehouse,
15461547
auto_resume=auto_resume,
1548+
auto_suspend_secs=auto_suspend_secs,
15471549
external_access_integrations=external_access_integrations,
15481550
comment=comment,
15491551
)
@@ -1555,6 +1557,7 @@ def test_set_property(mock_execute_query):
15551557
f"max_instances = {max_instances}",
15561558
f"query_warehouse = {query_warehouse}",
15571559
f"auto_resume = {auto_resume}",
1560+
f"auto_suspend_secs = {auto_suspend_secs}",
15581561
f"external_access_integrations = ({eai_list})",
15591562
f"comment = {comment}",
15601563
]
@@ -1566,7 +1569,9 @@ def test_set_property(mock_execute_query):
15661569
def test_set_property_no_properties():
15671570
service_name = "test_service"
15681571
with pytest.raises(NoPropertiesProvidedError) as e:
1569-
ServiceManager().set_property(service_name, None, None, None, None, None, None)
1572+
ServiceManager().set_property(
1573+
service_name, None, None, None, None, None, None, None
1574+
)
15701575
assert (
15711576
e.value.message
15721577
== f"No properties specified for service '{service_name}'. Please provide at least one property to set."
@@ -1582,6 +1587,7 @@ def test_set_property_cli(mock_set, mock_statement_success, runner):
15821587
max_instances = 3
15831588
query_warehouse = "test_warehouse"
15841589
auto_resume = False
1590+
auto_suspend_secs = 600
15851591
external_access_integrations = [
15861592
"google_apis_access_integration",
15871593
"salesforce_api_access_integration",
@@ -1600,6 +1606,8 @@ def test_set_property_cli(mock_set, mock_statement_success, runner):
16001606
"--query-warehouse",
16011607
query_warehouse,
16021608
"--no-auto-resume",
1609+
"--auto-suspend-secs",
1610+
str(auto_suspend_secs),
16031611
"--eai-name",
16041612
"google_apis_access_integration",
16051613
"--eai-name",
@@ -1614,6 +1622,7 @@ def test_set_property_cli(mock_set, mock_statement_success, runner):
16141622
max_instances=max_instances,
16151623
query_warehouse=query_warehouse,
16161624
auto_resume=auto_resume,
1625+
auto_suspend_secs=auto_suspend_secs,
16171626
external_access_integrations=external_access_integrations,
16181627
comment=to_string_literal(comment),
16191628
)
@@ -1636,6 +1645,7 @@ def test_set_property_no_properties_cli(mock_set, runner):
16361645
max_instances=None,
16371646
query_warehouse=None,
16381647
auto_resume=None,
1648+
auto_suspend_secs=None,
16391649
external_access_integrations=None,
16401650
comment=None,
16411651
)
@@ -1646,16 +1656,20 @@ def test_unset_property(mock_execute_query):
16461656
service_name = "test_service"
16471657
cursor = Mock(spec=SnowflakeCursor)
16481658
mock_execute_query.return_value = cursor
1649-
result = ServiceManager().unset_property(service_name, True, True, True, True, True)
1650-
expected_query = "alter service test_service unset min_instances,max_instances,query_warehouse,auto_resume,comment"
1659+
result = ServiceManager().unset_property(
1660+
service_name, True, True, True, True, True, True
1661+
)
1662+
expected_query = "alter service test_service unset min_instances,max_instances,query_warehouse,auto_resume,auto_suspend_secs,comment"
16511663
mock_execute_query.assert_called_once_with(expected_query)
16521664
assert result == cursor
16531665

16541666

16551667
def test_unset_property_no_properties():
16561668
service_name = "test_service"
16571669
with pytest.raises(NoPropertiesProvidedError) as e:
1658-
ServiceManager().unset_property(service_name, False, False, False, False, False)
1670+
ServiceManager().unset_property(
1671+
service_name, False, False, False, False, False, False
1672+
)
16591673
assert (
16601674
e.value.message
16611675
== f"No properties specified for service '{service_name}'. Please provide at least one property to reset to its default value."
@@ -1677,6 +1691,7 @@ def test_unset_property_cli(mock_unset, mock_statement_success, runner):
16771691
"--max-instances",
16781692
"--query-warehouse",
16791693
"--auto-resume",
1694+
"--auto-suspend-secs",
16801695
"--comment",
16811696
]
16821697
)
@@ -1686,6 +1701,7 @@ def test_unset_property_cli(mock_unset, mock_statement_success, runner):
16861701
max_instances=True,
16871702
query_warehouse=True,
16881703
auto_resume=True,
1704+
auto_suspend_secs=True,
16891705
comment=True,
16901706
)
16911707
assert result.exit_code == 0, result.output
@@ -1707,10 +1723,47 @@ def test_unset_property_no_properties_cli(mock_unset, runner):
17071723
max_instances=False,
17081724
query_warehouse=False,
17091725
auto_resume=False,
1726+
auto_suspend_secs=False,
17101727
comment=False,
17111728
)
17121729

17131730

1731+
@patch(EXECUTE_QUERY)
1732+
def test_set_property_auto_suspend_secs_only(mock_execute_query):
1733+
service_name = "test_service"
1734+
auto_suspend_secs = 300
1735+
cursor = Mock(spec=SnowflakeCursor)
1736+
mock_execute_query.return_value = cursor
1737+
result = ServiceManager().set_property(
1738+
service_name=service_name,
1739+
min_instances=None,
1740+
max_instances=None,
1741+
query_warehouse=None,
1742+
auto_resume=None,
1743+
auto_suspend_secs=auto_suspend_secs,
1744+
external_access_integrations=None,
1745+
comment=None,
1746+
)
1747+
expected_query = (
1748+
f"alter service {service_name} set\nauto_suspend_secs = {auto_suspend_secs}"
1749+
)
1750+
mock_execute_query.assert_called_once_with(expected_query)
1751+
assert result == cursor
1752+
1753+
1754+
@patch(EXECUTE_QUERY)
1755+
def test_unset_property_auto_suspend_secs_only(mock_execute_query):
1756+
service_name = "test_service"
1757+
cursor = Mock(spec=SnowflakeCursor)
1758+
mock_execute_query.return_value = cursor
1759+
result = ServiceManager().unset_property(
1760+
service_name, False, False, False, False, True, False
1761+
)
1762+
expected_query = f"alter service {service_name} unset auto_suspend_secs"
1763+
mock_execute_query.assert_called_once_with(expected_query)
1764+
assert result == cursor
1765+
1766+
17141767
def test_unset_property_with_args(runner):
17151768
service_name = "test_service"
17161769
result = runner.invoke(
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
definition_version: "2"
2+
3+
entities:
4+
service:
5+
type: service
6+
identifier:
7+
name:
8+
stage:
9+
compute_pool: snowcli_compute_pool
10+
spec_file: spec.yml
11+
min_instances: 1
12+
max_instances: 1
13+
query_warehouse: xsmall
14+
auto_suspend_secs: 600
15+
comment: "Test service without public endpoint"
16+
artifacts:
17+
- spec.yml

0 commit comments

Comments
 (0)