diff --git a/projects/fal/pyproject.toml b/projects/fal/pyproject.toml index 563544912..e197e705b 100644 --- a/projects/fal/pyproject.toml +++ b/projects/fal/pyproject.toml @@ -23,7 +23,7 @@ readme = "README.md" requires-python = ">=3.8" dependencies = [ "isolate[build]>=0.26.9,<0.27.0", - "isolate-proto>=0.34.0,<1", + "isolate-proto>=0.34.1,<1", "grpcio>=1.64.0,<2", "dill==0.3.7", "cloudpickle>=3.0.0,<3.2", diff --git a/projects/fal/src/fal/api/client.py b/projects/fal/src/fal/api/client.py index 00913871a..e39b120f8 100644 --- a/projects/fal/src/fal/api/client.py +++ b/projects/fal/src/fal/api/client.py @@ -296,15 +296,29 @@ class SecretsNamespace: def __init__(self, client: SyncServerlessClient): self.client = client - def set(self, name: str, value: str, environment_name: str | None = None) -> None: + def set( + self, + name: str, + value: str, + environment_name: str | None = None, + default_exposed: bool | None = None, + ) -> None: """Set a secret value. Args: name: Name of the secret. value: Value to store. + environment_name: Optional environment name. + default_exposed: Whether the secret is exposed to apps that do not + explicitly list their secrets. None keeps the account-level + default (and preserves the current setting on updates). """ return secrets_api.set_secret( - self.client, name, value, environment_name=environment_name + self.client, + name, + value, + environment_name=environment_name, + default_exposed=default_exposed, ) def list(self, environment_name: str | None = None) -> List[ServerlessSecret]: diff --git a/projects/fal/src/fal/api/secrets.py b/projects/fal/src/fal/api/secrets.py index 57a86d904..157312ad4 100644 --- a/projects/fal/src/fal/api/secrets.py +++ b/projects/fal/src/fal/api/secrets.py @@ -13,11 +13,17 @@ def set_secret( name: str, value: str, environment_name: str | None = None, + default_exposed: bool | None = None, ) -> None: from fal.sdk import FalServerlessClient with FalServerlessClient(client._grpc_host, client._credentials).connect() as conn: - conn.set_secret(name, value, environment_name=environment_name) + conn.set_secret( + name, + value, + environment_name=environment_name, + default_exposed=default_exposed, + ) def list_secrets( diff --git a/projects/fal/src/fal/cli/secrets.py b/projects/fal/src/fal/cli/secrets.py index 60aca42bd..a30da9cab 100644 --- a/projects/fal/src/fal/cli/secrets.py +++ b/projects/fal/src/fal/cli/secrets.py @@ -6,7 +6,12 @@ def _set(args): client = SyncServerlessClient(host=args.host, team=args.team) for name, value in args.secrets.items(): - client.secrets.set(name, value, environment_name=args.env) + client.secrets.set( + name, + value, + environment_name=args.env, + default_exposed=not args.not_exposed_by_default, + ) def _add_set_parser(subparsers, parents): @@ -27,6 +32,14 @@ def _add_set_parser(subparsers, parents): action=DictAction, help="Secret NAME=VALUE pairs.", ) + parser.add_argument( + "--not-exposed-by-default", + action="store_true", + help=( + "Do not expose the secret to apps by default; it is only " + "injected into apps that explicitly list it in their secrets." + ), + ) add_env_argument(parser) parser.set_defaults(func=_set) @@ -43,6 +56,7 @@ def _list(args): "name": secret.name, "environment": secret.environment_name, "created_at": str(secret.created_at), + "default_exposed": secret.default_exposed, } for secret in secrets ] @@ -54,12 +68,17 @@ def _list(args): table.add_column("Name") table.add_column("Env") table.add_column("Created At") + table.add_column("Exposed By Default") for secret in secrets: table.add_row( secret.name, secret.environment_name or "main", str(secret.created_at), + # None means the account-level default decides. + "account default" + if secret.default_exposed is None + else ("yes" if secret.default_exposed else "no"), ) args.console.print(table) diff --git a/projects/fal/src/fal/sdk.py b/projects/fal/src/fal/sdk.py index 6401bcbd2..8d70c34ee 100644 --- a/projects/fal/src/fal/sdk.py +++ b/projects/fal/src/fal/sdk.py @@ -281,6 +281,8 @@ class ServerlessSecret: name: str created_at: datetime environment_name: str | None = None + # None means the account-level default exposure applies. + default_exposed: bool | None = None @dataclass @@ -1405,10 +1407,18 @@ def list_alias_runners( return [from_grpc(runner) for runner in response.runners] def set_secret( - self, name: str, value: str, *, environment_name: str | None = None + self, + name: str, + value: str, + *, + environment_name: str | None = None, + default_exposed: bool | None = None, ) -> None: request = isolate_proto.SetSecretRequest( - name=name, value=value, environment_name=environment_name + name=name, + value=value, + environment_name=environment_name, + default_exposed=default_exposed, ) self.stub.SetSecret(request) @@ -1428,6 +1438,11 @@ def list_secrets( name=secret.name, created_at=isolate_proto.datetime_from_timestamp(secret.created_time), environment_name=secret.environment_name, + default_exposed=( + secret.default_exposed + if secret.HasField("default_exposed") + else None + ), ) for secret in response.secrets ] diff --git a/projects/fal/tests/unit/cli/test_secrets.py b/projects/fal/tests/unit/cli/test_secrets.py index 5bc23631d..858e5c6ff 100644 --- a/projects/fal/tests/unit/cli/test_secrets.py +++ b/projects/fal/tests/unit/cli/test_secrets.py @@ -8,6 +8,14 @@ def test_set(): args = parse_args(["secrets", "set", "secret1=value1", "secret2=value2"]) assert args.func == _set assert args.secrets == {"secret1": "value1", "secret2": "value2"} + assert args.not_exposed_by_default is False + + +def test_set_not_exposed_by_default(): + args = parse_args(["secrets", "set", "secret1=value1", "--not-exposed-by-default"]) + assert args.func == _set + assert args.secrets == {"secret1": "value1"} + assert args.not_exposed_by_default is True def test_set_with_env():