Skip to content

feat: RESET_VALUE #46

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

Merged
merged 2 commits into from
Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
74 changes: 56 additions & 18 deletions src/firebase_functions/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
import firebase_functions.private.path_pattern as _path_pattern
from firebase_functions.params import SecretParam, Expression

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


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

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

max_instances: int | Expression[int] | _util.Sentinel | None = None
"""
Max number of instances to be running in parallel.
A value of USE_DEFAULT restores the default max instances.
A value of RESET_VALUE restores the default max instances.
"""

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

vpc_connector: str | None = None
vpc_connector: str | _util.Sentinel | None = None
"""
Connect cloud function to specified VPC connector.
A value of USE_DEFAULT removes the VPC connector.
A value of RESET_VALUE removes the VPC connector.
"""

vpc_connector_egress_settings: VpcEgressSetting | None = None
vpc_connector_egress_settings: VpcEgressSetting | _util.Sentinel | None = None
"""
Egress settings for VPC connector.
A value of USE_DEFAULT turns off VPC connector egress settings.
A value of RESET_VALUE turns off VPC connector egress settings.
"""

service_account: str | _util.Sentinel | None = None
"""
Specific service account for the function to run as.
A value of USE_DEFAULT restores the default service account.
A value of RESET_VALUE restores the default service account.
"""

ingress: IngressSetting | _util.Sentinel | None = None
"""
Ingress settings which control where this function can be called from.
A value of USE_DEFAULT turns off ingress settings.
A value of RESET_VALUE turns off ingress settings.
"""

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

preserve_external_changes: bool | None = None
"""
Controls whether function configuration modified outside of function source is preserved.
Internally defaults to false.

When setting configuration available in the underlying platform that is not yet available
in the Firebase Functions SDK, we highly recommend setting `preserve_external_changes` to
`True`. Otherwise, when the Firebase Functions SDK releases a new version of the SDK
with support for the missing configuration, your function's manually configured setting
may inadvertently be wiped out.
"""

def _asdict_with_global_options(self) -> dict:
"""
Returns the provider options merged with globally defined options.
Expand All @@ -222,7 +234,27 @@ def _asdict_with_global_options(self) -> dict:
merged_options["labels"] = {**_GLOBAL_OPTIONS.labels, **self.labels}
if "labels" not in merged_options:
merged_options["labels"] = {}

preserve_external_changes: bool = merged_options.get(
"preserve_external_changes",
False,
)
resettable_options = [
"memory",
"timeout_sec",
"min_instances",
"max_instances",
"ingress",
"concurrency",
"service_account",
"vpc_connector",
"vpc_connector_egress_settings",
]
if not preserve_external_changes:
for option in resettable_options:
if option not in merged_options:
merged_options[option] = RESET_VALUE
if "preserve_external_changes" in merged_options:
del merged_options["preserve_external_changes"]
# _util.Sentinel values are converted to `None` in ManifestEndpoint generation
# after other None values are removed - so as to keep them in the generated
# YAML output as 'null' values.
Expand Down Expand Up @@ -257,10 +289,14 @@ def convert_secret(
region = [_typing.cast(str, options.region)]

vpc: _manifest.VpcSettings | None = None
if options.vpc_connector is not None:
if isinstance(options.vpc_connector, str):
vpc = ({
"connector": options.vpc_connector,
"egressSettings": options.vpc_connector_egress_settings.value
"connector":
options.vpc_connector,
"egressSettings":
options.vpc_connector_egress_settings.value if isinstance(
options.vpc_connector_egress_settings, VpcEgressSetting)
else options.vpc_connector_egress_settings
} if options.vpc_connector_egress_settings is not None else {
"connector": options.vpc_connector
})
Expand Down Expand Up @@ -511,6 +547,7 @@ def set_global_options(
labels: dict[str, str] | None = None,
secrets: list[str] | list[SecretParam] | _util.Sentinel | None = None,
enforce_app_check: bool | None = None,
preserve_external_changes: bool | None = None,
):
"""
Sets default options for all functions.
Expand All @@ -531,4 +568,5 @@ def set_global_options(
labels=labels,
secrets=secrets,
enforce_app_check=enforce_app_check,
preserve_external_changes=preserve_external_changes,
)
2 changes: 1 addition & 1 deletion src/firebase_functions/private/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class BlockingTrigger(_typing.TypedDict):

class VpcSettings(_typing.TypedDict):
connector: _typing_extensions.Required[str]
egressSettings: _typing_extensions.NotRequired[str]
egressSettings: _typing_extensions.NotRequired[str | _util.Sentinel]


@_dataclasses.dataclass(frozen=True)
Expand Down
9 changes: 5 additions & 4 deletions src/firebase_functions/private/serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from flask import Response

from firebase_functions.private import manifest as _manifest
from firebase_functions import params as _params
from firebase_functions import params as _params, options as _options
from firebase_functions.private import util as _util


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

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

yaml.add_representer(_util.Sentinel, represent_sentinel)
Expand Down
6 changes: 5 additions & 1 deletion src/firebase_functions/private/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@


class Sentinel:
"""Internal class for USE_DEFAULT."""
"""Internal class for RESET_VALUE."""

def __init__(self, description):
self.description = description

def __eq__(self, other):
return isinstance(other,
Sentinel) and self.description == other.description


def copy_func_kwargs(
func_with_kwargs: _typing.Callable[P, _typing.Any], # pylint: disable=unused-argument
Expand Down
48 changes: 47 additions & 1 deletion tests/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,22 @@
"""
Options unit tests.
"""
from firebase_functions import options
from firebase_functions import options, https_fn
from firebase_functions import params
from firebase_functions.private.serving import functions_as_yaml
# pylint: disable=protected-access


@https_fn.on_call()
def asamplefunction(_):
return "hello world"


@https_fn.on_call(preserve_external_changes=True)
def asamplefunctionpreserved(_):
return "hello world"


def test_set_global_options():
"""
Testing if setting a global option internally change the values.
Expand Down Expand Up @@ -63,3 +74,38 @@ def test_options_asdict_uses_cel_representation():
min_instances=int_param)._asdict_with_global_options()
assert https_options_dict["min_instances"] == int_param.to_cel(
), "param was not converted to CEL string"


def test_options_preserve_external_changes():
"""
Testing if setting a global option internally change the values.
"""
assert (options._GLOBAL_OPTIONS.preserve_external_changes is
None), "option should not already be set"
options.set_global_options(
preserve_external_changes=False,
min_instances=5,
)
options_asdict = options._GLOBAL_OPTIONS._asdict_with_global_options()
assert ("preserve_external_changes"
not in options_asdict), "option is still set"

assert (options_asdict["max_instances"] is
options.RESET_VALUE), "option should be RESET_VALUE"
assert options_asdict["min_instances"] == 5, "option should be set"

firebase_functions = {
"asamplefunction": asamplefunction,
}
yaml = functions_as_yaml(firebase_functions)
# A quick check to make sure the yaml has null values
# where we expect.
assert " availableMemoryMb: null\n" in yaml, "availableMemoryMb not in yaml"
assert " serviceAccountEmail: null\n" in yaml, "serviceAccountEmail not in yaml"

firebase_functions2 = {
"asamplefunctionpreserved": asamplefunctionpreserved,
}
yaml = functions_as_yaml(firebase_functions2)
assert " availableMemoryMb: null\n" not in yaml, "availableMemoryMb found in yaml"
assert " serviceAccountEmail: null\n" not in yaml, "serviceAccountEmail found in yaml"