Skip to content
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
2 changes: 1 addition & 1 deletion projects/fal/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 16 additions & 2 deletions projects/fal/src/fal/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
8 changes: 7 additions & 1 deletion projects/fal/src/fal/api/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
21 changes: 20 additions & 1 deletion projects/fal/src/fal/cli/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)

Expand All @@ -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
]
Expand All @@ -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)
Expand Down
19 changes: 17 additions & 2 deletions projects/fal/src/fal/sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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
]
Expand Down
8 changes: 8 additions & 0 deletions projects/fal/tests/unit/cli/test_secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
Loading