Skip to content

Commit 2711740

Browse files
committed
Add env vars to program + improve UX
1 parent 3aa896c commit 2711740

File tree

4 files changed

+82
-24
lines changed

4 files changed

+82
-24
lines changed

src/aleph_client/commands/help_strings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
IMMUTABLE_VOLUME = """Immutable volumes are pinned on the network and can be used by multiple VMs at the same time. They are read-only and useful for setting up libraries or other dependencies.\n
1919
Requires at least "name", "ref" (message hash) and "mount" path. "use_latest" is True by default, to use the latest version of the volume, if it has been amended. See the docs for more info: https://docs.aleph.im/computing/volumes/immutable/\n
2020
Example: --immutable-volume name=libs,ref=25a393222692c2f73489dc6710ae87605a96742ceef7b91de4d7ec34bb688d94,mount=/lib/python3.8/site-packages"""
21+
SKIP_ENV_VAR = "Skip prompt to set environment variables"
22+
ENVIRONMENT_VARIABLES = """Environment variables to pass. They will be public and visible in the message, so don't include secrets. Must be a comma separated list. Example: `KEY=value` or `KEY=value,KEY=value`"""
2123
ASK_FOR_CONFIRMATION = "Prompt user for confirmation"
2224
IPFS_CATCH_ALL_PATH = "Choose a relative path to catch all unmatched route or a 404 error"
2325
PAYMENT_TYPE = "Payment method, either holding tokens, NFTs, or Pay-As-You-Go via token streaming"
@@ -58,3 +60,7 @@
5860
CREATE_ACTIVE = "Loads the new private key after creation"
5961
PROMPT_CRN_URL = "URL of the CRN (Compute node) on which the instance is running"
6062
PROMPT_PROGRAM_CRN_URL = "URL of the CRN (Compute node) on which the program is running"
63+
PROGRAM_PATH = "Path to your source code. Can be a directory, a .squashfs file or a .zip archive"
64+
PROGRAM_ENTRYPOINT = "Your program entrypoint. Example: `main:app` for Python programs, else `run.sh` for a script containing your launch command"
65+
PROGRAM_RUNTIME = "Hash of the runtime to use for your program. You can also create your own runtime and pin it. Currently defaults to `{runtime_id}` (Use `aleph program runtime-checker` to inspect it)"
66+
PROGRAM_BETA = "If true, you will be prompted to add message subscriptions to your program"

src/aleph_client/commands/instance/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ async def create(
369369
# User has ctrl-c
370370
raise typer.Exit(1)
371371
crn.display_crn_specs()
372-
if not Confirm.ask("\nDeploy on this node ?"):
372+
if not Confirm.ask("\nDeploy on this node?"):
373373
crn = None
374374
continue
375375
elif crn_url or crn_hash:

src/aleph_client/commands/program.py

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from aleph_client.commands import help_strings
3434
from aleph_client.commands.utils import (
3535
filter_only_valid_messages,
36+
get_or_prompt_environment_variables,
3637
get_or_prompt_volumes,
3738
input_multiline,
3839
safe_getattr,
@@ -49,28 +50,26 @@
4950

5051
@app.command()
5152
async def upload(
52-
path: Path = typer.Argument(
53-
..., help="Path to your source code. Can be a directory, a .squashfs file or a .zip archive"
54-
),
53+
path: Path = typer.Argument(..., help=help_strings.PROGRAM_PATH),
5554
entrypoint: str = typer.Argument(
5655
...,
57-
help="Your program entrypoint. E.g. `main:app` for Python programs, or `run.sh` for a script containing your launch command",
56+
help=help_strings.PROGRAM_ENTRYPOINT,
5857
),
5958
channel: Optional[str] = typer.Option(default=settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL),
60-
memory: int = typer.Option(settings.DEFAULT_VM_MEMORY, help="Maximum memory allocation on vm in MiB"),
61-
vcpus: int = typer.Option(settings.DEFAULT_VM_VCPUS, help="Number of virtual cpus to allocate."),
59+
memory: int = typer.Option(settings.DEFAULT_VM_MEMORY, help=help_strings.MEMORY),
60+
vcpus: int = typer.Option(settings.DEFAULT_VM_VCPUS, help=help_strings.VCPUS),
6261
timeout_seconds: float = typer.Option(
6362
settings.DEFAULT_VM_TIMEOUT,
64-
help="If vm is not called after [timeout_seconds] it will shutdown",
63+
help=help_strings.TIMEOUT_SECONDS,
6564
),
6665
name: Optional[str] = typer.Option(None, help="Name for your program"),
6766
runtime: str = typer.Option(
6867
None,
69-
help=f"Hash of the runtime to use for your program. You can also create your own runtime and pin it. Currently defaults to `{settings.DEFAULT_RUNTIME_ID}`",
68+
help=help_strings.PROGRAM_RUNTIME.format(runtime_id=settings.DEFAULT_RUNTIME_ID),
7069
),
7170
beta: bool = typer.Option(
7271
False,
73-
help="If true, you will be prompted to add message subscriptions to your program",
72+
help=help_strings.PROGRAM_BETA,
7473
),
7574
persistent: bool = False,
7675
skip_volume: bool = typer.Option(False, help=help_strings.SKIP_VOLUME),
@@ -80,14 +79,16 @@ async def upload(
8079
None,
8180
help=help_strings.IMMUTABLE_VOLUME,
8281
),
82+
skip_env_var: bool = typer.Option(False, help=help_strings.SKIP_ENV_VAR),
83+
env_vars: Optional[str] = typer.Option(None, help=help_strings.ENVIRONMENT_VARIABLES),
8384
private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY),
8485
private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE),
8586
print_messages: bool = typer.Option(False),
8687
print_code_message: bool = typer.Option(False),
8788
print_program_message: bool = typer.Option(False),
8889
verbose: bool = True,
8990
debug: bool = False,
90-
):
91+
) -> Optional[str]:
9192
"""Register a program to run on aleph.im. For more information, see https://docs.aleph.im/computing/"""
9293

9394
setup_logging(debug)
@@ -106,7 +107,7 @@ async def upload(
106107
account: AccountFromPrivateKey = _load_account(private_key, private_key_file)
107108

108109
name = name or validated_prompt("Program name", lambda x: len(x) < 65)
109-
runtime = runtime or input(f"Ref of runtime ? [{settings.DEFAULT_RUNTIME_ID}] ") or settings.DEFAULT_RUNTIME_ID
110+
runtime = runtime or input(f"Ref of runtime? [{settings.DEFAULT_RUNTIME_ID}] ") or settings.DEFAULT_RUNTIME_ID
110111

111112
volumes = []
112113
if not skip_volume:
@@ -116,8 +117,12 @@ async def upload(
116117
immutable_volume=immutable_volume,
117118
)
118119

120+
environment_variables = None
121+
if not skip_env_var:
122+
environment_variables = get_or_prompt_environment_variables(env_vars)
123+
119124
subscriptions: Optional[List[Mapping]] = None
120-
if beta and yes_no_input("Subscribe to messages ?", default=False):
125+
if beta and yes_no_input("Subscribe to messages?", default=False):
121126
content_raw = input_multiline()
122127
try:
123128
subscriptions = json.loads(content_raw)
@@ -163,23 +168,46 @@ async def upload(
163168
persistent=persistent,
164169
encoding=encoding,
165170
volumes=volumes,
171+
environment_variables=environment_variables,
166172
subscriptions=subscriptions,
167173
)
168174
logger.debug("Upload finished")
169175
if print_messages or print_program_message:
170176
typer.echo(f"{message.json(indent=4)}")
171177

172178
item_hash: ItemHash = message.item_hash
173-
hash_base32 = b32encode(b16decode(item_hash.upper())).strip(b"=").lower().decode()
174-
175179
if verbose:
176-
typer.echo(
177-
f"Your program has been uploaded on aleph.im\n\n"
178-
"Available on:\n"
179-
f" {settings.VM_URL_PATH.format(hash=item_hash)}\n"
180-
f" {settings.VM_URL_HOST.format(hash_base32=hash_base32)}\n"
181-
"Visualise on:\n https://explorer.aleph.im/address/"
182-
f"{message.chain.value}/{message.sender}/message/PROGRAM/{item_hash}\n"
180+
hash_base32 = b32encode(b16decode(item_hash.upper())).strip(b"=").lower().decode()
181+
func_url_1 = f"{settings.VM_URL_PATH.format(hash=item_hash)}"
182+
func_url_2 = f"{settings.VM_URL_HOST.format(hash_base32=hash_base32)}"
183+
184+
console = Console()
185+
infos = [
186+
Text.from_markup(f"Your program [bright_cyan]{item_hash}[/bright_cyan] has been uploaded on aleph.im."),
187+
Text.assemble(
188+
"\n\nAvailable on:\n",
189+
Text.from_markup(
190+
f"↳ [bright_yellow][link={func_url_1}]{func_url_1}[/link][/bright_yellow]\n",
191+
style="italic",
192+
),
193+
Text.from_markup(
194+
f"↳ [dark_olive_green2][link={func_url_2}]{func_url_2}[/link][/dark_olive_green2]",
195+
style="italic",
196+
),
197+
"\n\nVisualise on:\n",
198+
Text.from_markup(
199+
f"[blue]https://explorer.aleph.im/address/{message.chain.value}/{message.sender}/message/PROGRAM/{item_hash}[/blue]"
200+
),
201+
),
202+
]
203+
console.print(
204+
Panel(
205+
Text.assemble(*infos),
206+
title="Program Created",
207+
border_style="green",
208+
expand=False,
209+
title_align="left",
210+
)
183211
)
184212
return item_hash
185213

@@ -499,6 +527,7 @@ async def runtime_checker(
499527
beta=False,
500528
persistent=False,
501529
skip_volume=True,
530+
skip_env_var=True,
502531
private_key=private_key,
503532
private_key_file=private_key_file,
504533
print_messages=False,
@@ -507,6 +536,8 @@ async def runtime_checker(
507536
verbose=verbose,
508537
debug=debug,
509538
)
539+
if not program_hash:
540+
raise Exception("No program hash")
510541
except Exception as e:
511542
echo(f"Failed to deploy the runtime checker program: {e}")
512543
raise typer.Exit(code=1)

src/aleph_client/commands/utils.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def yes_no_input(text: str, default: str | bool) -> bool:
8080

8181

8282
def prompt_for_volumes():
83-
while yes_no_input("Add volume ?", default=False):
83+
while yes_no_input("Add volume?", default=False):
8484
mount = validated_prompt("Mount path (ex: /opt/data): ", lambda text: len(text) > 0)
8585
name = validated_prompt("Name: ", lambda text: len(text) > 0)
8686
comment = Prompt.ask("Comment: ")
@@ -96,7 +96,7 @@ def prompt_for_volumes():
9696
}
9797
else:
9898
ref = validated_prompt("Item hash: ", lambda text: len(text) == 64)
99-
use_latest = yes_no_input("Use latest version ?", default=True)
99+
use_latest = yes_no_input("Use latest version?", default=True)
100100
yield {
101101
"comment": comment,
102102
"mount": mount,
@@ -151,6 +151,27 @@ def get_or_prompt_volumes(ephemeral_volume, immutable_volume, persistent_volume)
151151
return volumes
152152

153153

154+
def env_vars_to_dict(env_vars: Optional[str]) -> Dict[str, str]:
155+
dict_store: Dict[str, str] = {}
156+
if env_vars:
157+
for env_var in env_vars.split(","):
158+
label, value = env_var.split("=", 1)
159+
dict_store[label.strip()] = value.strip()
160+
return dict_store
161+
162+
163+
def get_or_prompt_environment_variables(env_vars: Optional[str]) -> Optional[Dict[str, str]]:
164+
environment_variables: Dict[str, str] = {}
165+
if not env_vars:
166+
while yes_no_input("Add environment variable?", default=False):
167+
label = validated_prompt("Label: ", lambda text: len(text) > 0)
168+
value = validated_prompt("Value: ", lambda text: len(text) > 0)
169+
environment_variables[label] = value
170+
else:
171+
environment_variables = env_vars_to_dict(env_vars)
172+
return environment_variables if environment_variables else None
173+
174+
154175
def str_to_datetime(date: Optional[str]) -> Optional[datetime]:
155176
"""
156177
Converts a string representation of a date/time to a datetime object.

0 commit comments

Comments
 (0)