Skip to content

Commit eb1bb15

Browse files
authored
Merge pull request #46 from invertase/reset-value
feat: RESET_VALUE
2 parents b8d3e0d + 3016a49 commit eb1bb15

File tree

5 files changed

+109
-25
lines changed

5 files changed

+109
-25
lines changed

Diff for: src/firebase_functions/options.py

+54-18
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@
2626
import firebase_functions.private.path_pattern as _path_pattern
2727
from firebase_functions.params import SecretParam, Expression
2828

29-
USE_DEFAULT = _util.Sentinel(
30-
"Value used to reset an option to factory defaults")
31-
"""Used to reset an option to its factory default."""
29+
RESET_VALUE = _util.Sentinel(
30+
"Special configuration value to reset configuration to platform default.")
31+
"""Special configuration value to reset configuration to platform default."""
3232

3333

3434
class VpcEgressSetting(str, _enum.Enum):
@@ -115,14 +115,14 @@ class RuntimeOptions:
115115
memory: int | MemoryOption | Expression[int] | _util.Sentinel | None = None
116116
"""
117117
Amount of memory to allocate to a function.
118-
A value of USE_DEFAULT restores the defaults of 256MB.
118+
A value of RESET_VALUE restores the defaults of 256MB.
119119
"""
120120

121121
timeout_sec: int | Expression[int] | _util.Sentinel | None = None
122122
"""
123123
Timeout for the function in sections, possible values are 0 to 540.
124124
HTTPS functions can specify a higher timeout.
125-
A value of USE_DEFAULT restores the default of 60s
125+
A value of RESET_VALUE restores the default of 60s
126126
The minimum timeout for a gen 2 function is 1s. The maximum timeout for a
127127
function depends on the type of function: Event handling functions have a
128128
maximum timeout of 540s (9 minutes). HTTPS and callable functions have a
@@ -135,20 +135,20 @@ class RuntimeOptions:
135135
Min number of actual instances to be running at a given time.
136136
Instances will be billed for memory allocation and 10% of CPU allocation
137137
while idle.
138-
A value of USE_DEFAULT restores the default min instances.
138+
A value of RESET_VALUE restores the default min instances.
139139
"""
140140

141141
max_instances: int | Expression[int] | _util.Sentinel | None = None
142142
"""
143143
Max number of instances to be running in parallel.
144-
A value of USE_DEFAULT restores the default max instances.
144+
A value of RESET_VALUE restores the default max instances.
145145
"""
146146

147147
concurrency: int | Expression[int] | _util.Sentinel | None = None
148148
"""
149149
Number of requests a function can serve at once.
150150
Can only be applied to functions running on Cloud Functions v2.
151-
A value of USE_DEFAULT restores the default concurrency (80 when CPU >= 1, 1 otherwise).
151+
A value of RESET_VALUE restores the default concurrency (80 when CPU >= 1, 1 otherwise).
152152
Concurrency cannot be set to any value other than 1 if `cpu` is less than 1.
153153
The maximum value for concurrency is 1,000.
154154
"""
@@ -163,28 +163,28 @@ class RuntimeOptions:
163163
to the value "gcf_gen1"
164164
"""
165165

166-
vpc_connector: str | None = None
166+
vpc_connector: str | _util.Sentinel | None = None
167167
"""
168168
Connect cloud function to specified VPC connector.
169-
A value of USE_DEFAULT removes the VPC connector.
169+
A value of RESET_VALUE removes the VPC connector.
170170
"""
171171

172-
vpc_connector_egress_settings: VpcEgressSetting | None = None
172+
vpc_connector_egress_settings: VpcEgressSetting | _util.Sentinel | None = None
173173
"""
174174
Egress settings for VPC connector.
175-
A value of USE_DEFAULT turns off VPC connector egress settings.
175+
A value of RESET_VALUE turns off VPC connector egress settings.
176176
"""
177177

178178
service_account: str | _util.Sentinel | None = None
179179
"""
180180
Specific service account for the function to run as.
181-
A value of USE_DEFAULT restores the default service account.
181+
A value of RESET_VALUE restores the default service account.
182182
"""
183183

184184
ingress: IngressSetting | _util.Sentinel | None = None
185185
"""
186186
Ingress settings which control where this function can be called from.
187-
A value of USE_DEFAULT turns off ingress settings.
187+
A value of RESET_VALUE turns off ingress settings.
188188
"""
189189

190190
labels: dict[str, str] | None = None
@@ -205,6 +205,18 @@ class RuntimeOptions:
205205
When false, requests with invalid tokens set event.app to None.
206206
"""
207207

208+
preserve_external_changes: bool | None = None
209+
"""
210+
Controls whether function configuration modified outside of function source is preserved.
211+
Internally defaults to false.
212+
213+
When setting configuration available in the underlying platform that is not yet available
214+
in the Firebase Functions SDK, we highly recommend setting `preserve_external_changes` to
215+
`True`. Otherwise, when the Firebase Functions SDK releases a new version of the SDK
216+
with support for the missing configuration, your function's manually configured setting
217+
may inadvertently be wiped out.
218+
"""
219+
208220
def _asdict_with_global_options(self) -> dict:
209221
"""
210222
Returns the provider options merged with globally defined options.
@@ -222,7 +234,25 @@ def _asdict_with_global_options(self) -> dict:
222234
merged_options["labels"] = {**_GLOBAL_OPTIONS.labels, **self.labels}
223235
if "labels" not in merged_options:
224236
merged_options["labels"] = {}
225-
237+
preserve_external_changes: bool = merged_options.get(
238+
"preserve_external_changes",
239+
False,
240+
)
241+
resettable_options = [
242+
"memory",
243+
"timeout_sec",
244+
"min_instances",
245+
"max_instances",
246+
"ingress",
247+
"concurrency",
248+
"service_account",
249+
"vpc_connector",
250+
"vpc_connector_egress_settings",
251+
]
252+
if not preserve_external_changes:
253+
for option in resettable_options:
254+
if option not in merged_options:
255+
merged_options[option] = RESET_VALUE
226256
# _util.Sentinel values are converted to `None` in ManifestEndpoint generation
227257
# after other None values are removed - so as to keep them in the generated
228258
# YAML output as 'null' values.
@@ -257,10 +287,14 @@ def convert_secret(
257287
region = [_typing.cast(str, options.region)]
258288

259289
vpc: _manifest.VpcSettings | None = None
260-
if options.vpc_connector is not None:
290+
if isinstance(options.vpc_connector, str):
261291
vpc = ({
262-
"connector": options.vpc_connector,
263-
"egressSettings": options.vpc_connector_egress_settings.value
292+
"connector":
293+
options.vpc_connector,
294+
"egressSettings":
295+
options.vpc_connector_egress_settings.value if isinstance(
296+
options.vpc_connector_egress_settings, VpcEgressSetting)
297+
else options.vpc_connector_egress_settings
264298
} if options.vpc_connector_egress_settings is not None else {
265299
"connector": options.vpc_connector
266300
})
@@ -511,6 +545,7 @@ def set_global_options(
511545
labels: dict[str, str] | None = None,
512546
secrets: list[str] | list[SecretParam] | _util.Sentinel | None = None,
513547
enforce_app_check: bool | None = None,
548+
preserve_external_changes: bool | None = None,
514549
):
515550
"""
516551
Sets default options for all functions.
@@ -531,4 +566,5 @@ def set_global_options(
531566
labels=labels,
532567
secrets=secrets,
533568
enforce_app_check=enforce_app_check,
569+
preserve_external_changes=preserve_external_changes,
534570
)

Diff for: src/firebase_functions/private/manifest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class BlockingTrigger(_typing.TypedDict):
8787

8888
class VpcSettings(_typing.TypedDict):
8989
connector: _typing_extensions.Required[str]
90-
egressSettings: _typing_extensions.NotRequired[str]
90+
egressSettings: _typing_extensions.NotRequired[str | _util.Sentinel]
9191

9292

9393
@_dataclasses.dataclass(frozen=True)

Diff for: src/firebase_functions/private/serving.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from flask import Response
2828

2929
from firebase_functions.private import manifest as _manifest
30-
from firebase_functions import params as _params
30+
from firebase_functions import params as _params, options as _options
3131
from firebase_functions.private import util as _util
3232

3333

@@ -76,9 +76,10 @@ def functions_as_yaml(functions: dict) -> str:
7676
manifest_spec = _manifest.manifest_to_spec_dict(manifest_stack)
7777
manifest_spec_with_sentinels = to_spec(manifest_spec)
7878

79-
def represent_sentinel(self, _):
80-
# TODO distinguishing between RESET_VALUE or DEFAULT_VALUE
81-
# TODO can be done here
79+
def represent_sentinel(self, value):
80+
if value == _options.RESET_VALUE:
81+
return self.represent_scalar("tag:yaml.org,2002:null", "null")
82+
# Other sentinel types in the future can be added here.
8283
return self.represent_scalar("tag:yaml.org,2002:null", "null")
8384

8485
yaml.add_representer(_util.Sentinel, represent_sentinel)

Diff for: src/firebase_functions/private/util.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,15 @@
3030

3131

3232
class Sentinel:
33-
"""Internal class for USE_DEFAULT."""
33+
"""Internal class for RESET_VALUE."""
3434

3535
def __init__(self, description):
3636
self.description = description
3737

38+
def __eq__(self, other):
39+
return isinstance(other,
40+
Sentinel) and self.description == other.description
41+
3842

3943
def copy_func_kwargs(
4044
func_with_kwargs: _typing.Callable[P, _typing.Any], # pylint: disable=unused-argument

Diff for: tests/test_options.py

+44-1
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,22 @@
1414
"""
1515
Options unit tests.
1616
"""
17-
from firebase_functions import options
17+
from firebase_functions import options, https_fn
1818
from firebase_functions import params
19+
from firebase_functions.private.serving import functions_as_yaml
1920
# pylint: disable=protected-access
2021

2122

23+
@https_fn.on_call()
24+
def asamplefunction(_):
25+
return "hello world"
26+
27+
28+
@https_fn.on_call(preserve_external_changes=True)
29+
def asamplefunctionpreserved(_):
30+
return "hello world"
31+
32+
2233
def test_set_global_options():
2334
"""
2435
Testing if setting a global option internally change the values.
@@ -63,3 +74,35 @@ def test_options_asdict_uses_cel_representation():
6374
min_instances=int_param)._asdict_with_global_options()
6475
assert https_options_dict["min_instances"] == int_param.to_cel(
6576
), "param was not converted to CEL string"
77+
78+
79+
def test_options_preserve_external_changes():
80+
"""
81+
Testing if setting a global option internally change the values.
82+
"""
83+
assert (options._GLOBAL_OPTIONS.preserve_external_changes is
84+
None), "option should not already be set"
85+
options.set_global_options(
86+
preserve_external_changes=False,
87+
min_instances=5,
88+
)
89+
options_asdict = options._GLOBAL_OPTIONS._asdict_with_global_options()
90+
assert (options_asdict["max_instances"] is
91+
options.RESET_VALUE), "option should be RESET_VALUE"
92+
assert options_asdict["min_instances"] == 5, "option should be set"
93+
94+
firebase_functions = {
95+
"asamplefunction": asamplefunction,
96+
}
97+
yaml = functions_as_yaml(firebase_functions)
98+
# A quick check to make sure the yaml has null values
99+
# where we expect.
100+
assert " availableMemoryMb: null\n" in yaml, "availableMemoryMb not in yaml"
101+
assert " serviceAccountEmail: null\n" in yaml, "serviceAccountEmail not in yaml"
102+
103+
firebase_functions2 = {
104+
"asamplefunctionpreserved": asamplefunctionpreserved,
105+
}
106+
yaml = functions_as_yaml(firebase_functions2)
107+
assert " availableMemoryMb: null\n" not in yaml, "availableMemoryMb found in yaml"
108+
assert " serviceAccountEmail: null\n" not in yaml, "serviceAccountEmail found in yaml"

0 commit comments

Comments
 (0)