Skip to content

Commit

Permalink
Fix upgradability, add/fix persist/unpersist
Browse files Browse the repository at this point in the history
  • Loading branch information
philogicae committed Jan 8, 2025
1 parent cf9021e commit d112ae7
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 13 deletions.
3 changes: 3 additions & 0 deletions src/aleph_client/commands/help_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,6 @@
PROGRAM_ENTRYPOINT = "Your program entrypoint. Example: `main:app` for Python programs, else `run.sh` for a script containing your launch command"
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)"
PROGRAM_BETA = "If true, you will be prompted to add message subscriptions to your program"
PROGRAM_UPDATABLE = "Allow program updates. By default, only the source code can be modified without requiring redeployement (same item hash). When enabled (set to True), this option allows to update any other field. However, such modifications will require a program redeployment (new item hash)"
PROGRAM_KEEP_CODE = "Keep the source code intact instead of deleting it"
PROGRAM_KEEP_PREV = "Keep the previous program intact instead of deleting it"
182 changes: 170 additions & 12 deletions src/aleph_client/commands/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ async def upload(
help=help_strings.PROGRAM_BETA,
),
persistent: bool = False,
updatable: bool = typer.Option(False, help=help_strings.PROGRAM_UPDATABLE),
skip_volume: bool = typer.Option(False, help=help_strings.SKIP_VOLUME),
persistent_volume: Optional[List[str]] = typer.Option(None, help=help_strings.PERSISTENT_VOLUME),
ephemeral_volume: Optional[List[str]] = typer.Option(None, help=help_strings.EPHEMERAL_VOLUME),
Expand All @@ -90,7 +91,7 @@ async def upload(
verbose: bool = True,
debug: bool = False,
) -> Optional[str]:
"""Register a program to run on aleph.im. For more information, see https://docs.aleph.im/computing/"""
"""Register a program to run on aleph.im. For more information, see https://docs.aleph.im/computing"""

setup_logging(debug)

Expand Down Expand Up @@ -160,6 +161,7 @@ async def upload(
program_ref=program_ref,
entrypoint=entrypoint,
metadata=dict(name=name),
allow_amend=updatable,
runtime=runtime,
storage_engine=StorageEnum.storage,
channel=channel,
Expand Down Expand Up @@ -223,7 +225,7 @@ async def update(
verbose: bool = True,
debug: bool = False,
):
"""Update the code of an existing program"""
"""Update the code of an existing program. The item hash will not change"""

setup_logging(debug)

Expand All @@ -247,7 +249,7 @@ async def update(
typer.echo("Program does not exist")
return 1
except ForgottenMessageError:
typer.echo("Program already forgotten")
typer.echo("Program has been forgotten")
return 1

Check warning on line 253 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L248-L253

Added lines #L248 - L253 were not covered by tests
if program_message.sender != account.get_address():
typer.echo("You are not the owner of this program")
Expand Down Expand Up @@ -318,7 +320,7 @@ async def update(
async def delete(
item_hash: str = typer.Argument(..., help="Item hash to unpersist"),
reason: str = typer.Option("User deletion", help="Reason for deleting the program"),
delete_code: bool = typer.Option(True, help="Also delete the code"),
keep_code: bool = typer.Option(False, help=help_strings.PROGRAM_KEEP_CODE),
private_key: Optional[str] = settings.PRIVATE_KEY_STRING,
private_key_file: Optional[Path] = settings.PRIVATE_KEY_FILE,
print_message: bool = typer.Option(False),
Expand Down Expand Up @@ -347,7 +349,7 @@ async def delete(
return 1

Check warning on line 349 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L348-L349

Added lines #L348 - L349 were not covered by tests

message, _ = await client.forget(hashes=[ItemHash(item_hash)], reason=reason)
if delete_code:
if not keep_code:
try:
code_volume: StoreMessage = await client.get_message(
item_hash=existing_message.content.code.ref, message_type=StoreMessage
Expand Down Expand Up @@ -446,7 +448,9 @@ async def list_programs(
f"vCPU: [magenta3]{message.content.resources.vcpus}[/magenta3]\n",
f"RAM: [magenta3]{message.content.resources.memory / 1_024:.2f} GiB[/magenta3]\n",
"HyperV: [magenta3]Firecracker[/magenta3]\n",
f"Timeout: [magenta3]{message.content.resources.seconds}s[/magenta3]",
f"Timeout: [orange3]{message.content.resources.seconds}s[/orange3]\n",
f"Persistent: {'[green]Yes[/green]' if message.content.on.persistent else '[red]No[/red]'}\n",
f"Updatable: {'[green]Yes[/green]' if message.content.allow_amend else '[red]No[/red]'}",
]
specifications = Text.from_markup("".join(specs))
volumes = ""
Expand Down Expand Up @@ -486,23 +490,137 @@ async def list_programs(
)


@app.command()
async def persist(
item_hash: str = typer.Argument(..., help="Item hash to persist"),
keep_prev: bool = typer.Option(
False,
help=help_strings.PROGRAM_KEEP_PREV,
),
private_key: Optional[str] = settings.PRIVATE_KEY_STRING,
private_key_file: Optional[Path] = settings.PRIVATE_KEY_FILE,
print_message: bool = typer.Option(False),
verbose: bool = True,
debug: bool = False,
):
"""Recreate a non-persistent program as persistent"""

setup_logging(debug)

Check warning on line 508 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L508

Added line #L508 was not covered by tests

account = _load_account(private_key, private_key_file)

Check warning on line 510 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L510

Added line #L510 was not covered by tests

async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client:
try:
message: ProgramMessage = await client.get_message(item_hash=item_hash, message_type=ProgramMessage)
except MessageNotFoundError:
typer.echo("Program does not exist")
return 1
except ForgottenMessageError:
typer.echo("Program has been forgotten")
return 1

Check warning on line 520 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L512-L520

Added lines #L512 - L520 were not covered by tests
if message.sender != account.get_address():
typer.echo("You are not the owner of this program")
return 1

Check warning on line 523 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L522-L523

Added lines #L522 - L523 were not covered by tests
if not message.content.allow_amend:
typer.echo("Program is not updatable")
return 1

Check warning on line 526 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L525-L526

Added lines #L525 - L526 were not covered by tests
if message.content.on.persistent:
typer.echo("Program is already persistent")
return 1

Check warning on line 529 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L528-L529

Added lines #L528 - L529 were not covered by tests

# Update content
content: ProgramContent = message.content.copy()
content.on.persistent = True
content.replaces = message.item_hash

Check warning on line 534 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L533-L534

Added lines #L533 - L534 were not covered by tests

message, _status, _ = await client.submit(

Check warning on line 536 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L536

Added line #L536 was not covered by tests
content=content.dict(exclude_none=True),
message_type=message.type,
channel=message.channel,
)

if print_message:
typer.echo(f"{message.json(indent=4)}")

Check warning on line 543 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L543

Added line #L543 was not covered by tests

# Delete previous non-persistent program
prev_label, prev_color = "INTACT", "orange3"

Check warning on line 546 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L546

Added line #L546 was not covered by tests
if not keep_prev:
await client.forget(hashes=[ItemHash(item_hash)], reason="Program persisted")
prev_label, prev_color = "DELETED", "red"

Check warning on line 549 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L548-L549

Added lines #L548 - L549 were not covered by tests

if verbose:
hash_base32 = b32encode(b16decode(item_hash.upper())).strip(b"=").lower().decode()
func_url_1 = f"{settings.VM_URL_PATH.format(hash=item_hash)}"
func_url_2 = f"{settings.VM_URL_HOST.format(hash_base32=hash_base32)}"
console = Console()
infos = [

Check warning on line 556 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L552-L556

Added lines #L552 - L556 were not covered by tests
Text.from_markup("Your program is now [green]persistent[/green]. It implies a new item hash.\n"),
Text.from_markup(
f"\n\n[{prev_color}]- Previous non-persistent program: {item_hash} -> {prev_label}[/{prev_color}]\n[green]- New persistent program: {message.item_hash}[/green]."
),
Text.assemble(
"\n\nAvailable on:\n",
Text.from_markup(
f"↳ [bright_yellow][link={func_url_1}]{func_url_1}[/link][/bright_yellow]\n",
style="italic",
),
Text.from_markup(
f"↳ [dark_olive_green2][link={func_url_2}]{func_url_2}[/link][/dark_olive_green2]",
style="italic",
),
),
]
console.print(

Check warning on line 573 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L573

Added line #L573 was not covered by tests
Panel(
Text.assemble(*infos),
title="Program: Persist",
border_style="orchid",
expand=False,
title_align="left",
)
)


@app.command()
async def unpersist(
item_hash: str = typer.Argument(..., help="Item hash to unpersist"),
keep_prev: bool = typer.Option(
False,
help=help_strings.PROGRAM_KEEP_PREV,
),
private_key: Optional[str] = settings.PRIVATE_KEY_STRING,
private_key_file: Optional[Path] = settings.PRIVATE_KEY_FILE,
print_message: bool = typer.Option(False),
verbose: bool = True,
debug: bool = False,
):
"""Stop a persistent program by making it non-persistent"""
"""Recreate a persistent program as non-persistent"""

setup_logging(debug)

Check warning on line 599 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L599

Added line #L599 was not covered by tests

account = _load_account(private_key, private_key_file)

Check warning on line 601 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L601

Added line #L601 was not covered by tests

async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client:
message: ProgramMessage = await client.get_message(item_hash=item_hash, message_type=ProgramMessage)
content: ProgramContent = message.content.copy()
try:
message: ProgramMessage = await client.get_message(item_hash=item_hash, message_type=ProgramMessage)
except MessageNotFoundError:
typer.echo("Program does not exist")
return 1
except ForgottenMessageError:
typer.echo("Program has been forgotten")
return 1

Check warning on line 611 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L603-L611

Added lines #L603 - L611 were not covered by tests
if message.sender != account.get_address():
typer.echo("You are not the owner of this program")
return 1

Check warning on line 614 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L613-L614

Added lines #L613 - L614 were not covered by tests
if not message.content.allow_amend:
typer.echo("Program is not updatable")
return 1

Check warning on line 617 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L616-L617

Added lines #L616 - L617 were not covered by tests
if not message.content.on.persistent:
typer.echo("Program is already unpersistent")
return 1

Check warning on line 620 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L619-L620

Added lines #L619 - L620 were not covered by tests

# Update content
content: ProgramContent = message.content.copy()

Check warning on line 623 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L623

Added line #L623 was not covered by tests
content.on.persistent = False
content.replaces = message.item_hash

Expand All @@ -511,7 +629,47 @@ async def unpersist(
message_type=message.type,
channel=message.channel,
)
typer.echo(f"{message.json(indent=4)}")

if print_message:
typer.echo(f"{message.json(indent=4)}")

Check warning on line 634 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L634

Added line #L634 was not covered by tests

# Delete previous persistent program
prev_label, prev_color = "INTACT", "orange3"

Check warning on line 637 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L637

Added line #L637 was not covered by tests
if not keep_prev:
await client.forget(hashes=[ItemHash(item_hash)], reason="Program unpersisted")
prev_label, prev_color = "DELETED", "red"

Check warning on line 640 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L639-L640

Added lines #L639 - L640 were not covered by tests

if verbose:
hash_base32 = b32encode(b16decode(item_hash.upper())).strip(b"=").lower().decode()
func_url_1 = f"{settings.VM_URL_PATH.format(hash=item_hash)}"
func_url_2 = f"{settings.VM_URL_HOST.format(hash_base32=hash_base32)}"
console = Console()
infos = [

Check warning on line 647 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L643-L647

Added lines #L643 - L647 were not covered by tests
Text.from_markup("Your program is now [red]unpersistent[/red]. It implies a new item hash.\n"),
Text.from_markup(
f"\n\n[{prev_color}]- Previous persistent program: {item_hash} -> {prev_label}[/{prev_color}]\n[green]- New non-persistent program: {message.item_hash}[/green]."
),
Text.assemble(
"\n\nAvailable on:\n",
Text.from_markup(
f"↳ [bright_yellow][link={func_url_1}]{func_url_1}[/link][/bright_yellow]\n",
style="italic",
),
Text.from_markup(
f"↳ [dark_olive_green2][link={func_url_2}]{func_url_2}[/link][/dark_olive_green2]",
style="italic",
),
),
]
console.print(

Check warning on line 664 in src/aleph_client/commands/program.py

View check run for this annotation

Codecov / codecov/patch

src/aleph_client/commands/program.py#L664

Added line #L664 was not covered by tests
Panel(
Text.assemble(*infos),
title="Program: Unpersist",
border_style="orchid",
expand=False,
title_align="left",
)
)


@app.command()
Expand All @@ -523,7 +681,7 @@ async def logs(
chain: Chain = typer.Option(None, help=help_strings.ADDRESS_CHAIN),
debug: bool = False,
):
"""Display the logs of a program.
"""Display the logs of a program
Will only show logs from the selected CRN"""

Expand Down Expand Up @@ -613,7 +771,7 @@ async def runtime_checker(
await delete(
item_hash=program_hash,
reason="Automatic deletion of the runtime checker program",
delete_code=True,
keep_code=True,
private_key=private_key,
private_key_file=private_key_file,
print_message=False,
Expand Down
11 changes: 10 additions & 1 deletion tests/unit/test_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ def create_mock_program_message(mock_account):
],
code=Dict(encoding="squashfs", entrypoint="main:app", ref=FAKE_STORE_HASH),
runtime=Dict(ref=FAKE_STORE_HASH),
on=Dict(http=True, persistent=False),
allow_amend=False,
),
)
return program
Expand Down Expand Up @@ -199,7 +201,7 @@ async def delete_program():
print() # For better display when pytest -v -s
await delete(
item_hash=FAKE_PROGRAM_HASH,
delete_code=True,
keep_code=False,
private_key=None,
private_key_file=None,
print_message=False,
Expand Down Expand Up @@ -238,6 +240,13 @@ async def list_program():
await list_program()


@pytest.mark.asyncio
async def test_persist_program():
mock_load_account = create_mock_load_account()
mock_account = mock_load_account.return_value
assert True


@pytest.mark.asyncio
async def test_unpersist_program():
mock_load_account = create_mock_load_account()
Expand Down

0 comments on commit d112ae7

Please sign in to comment.