Skip to content
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

Allow deletion images without tag #2010

Merged
merged 4 commits into from
Feb 23, 2021
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
1 change: 1 addition & 0 deletions CHANGELOG.D/2010.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow deletion images without tag. Support multiple arguments for `neuro image rm` command.
2 changes: 1 addition & 1 deletion CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -1260,7 +1260,7 @@ Remove image from platform registry.<br/><br/>Image name must be URL with image:
**Usage:**

```bash
neuro image rm [OPTIONS] IMAGE
neuro image rm [OPTIONS] IMAGES...
```

**Examples:**
Expand Down
2 changes: 1 addition & 1 deletion neuro-cli/docs/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Remove image from platform registry
#### Usage

```bash
neuro image rm [OPTIONS] IMAGE
neuro image rm [OPTIONS] IMAGES...
```

Remove image from platform registry.
Expand Down
17 changes: 4 additions & 13 deletions neuro-cli/src/neuro_cli/click_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,6 @@ def convert(
return client.parse.local_image(value)


class ImageType(click.ParamType):
name = "image"

def convert(
self, value: str, param: Optional[click.Parameter], ctx: Optional[click.Context]
) -> RemoteImage:
assert ctx is not None
root = cast(Root, ctx.obj)
client = root.run(root.init_client())
return client.parse.remote_image(value)


class RemoteTaglessImageType(click.ParamType):
name = "image"

Expand All @@ -104,13 +92,16 @@ def convert(
class RemoteImageType(click.ParamType):
name = "image"

def __init__(self, tag_option: TagOption = TagOption.DEFAULT) -> None:
self.tag_option = tag_option

def convert(
self, value: str, param: Optional[click.Parameter], ctx: Optional[click.Context]
) -> RemoteImage:
assert ctx is not None
root = cast(Root, ctx.obj)
client = root.run(root.init_client())
return client.parse.remote_image(value)
return client.parse.remote_image(value, tag_option=self.tag_option)


class LocalRemotePortParamType(click.ParamType):
Expand Down
70 changes: 44 additions & 26 deletions neuro-cli/src/neuro_cli/image.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import contextlib
import logging
from dataclasses import replace
from typing import Optional
from typing import Optional, Sequence

from rich.markup import escape as rich_escape
from rich.progress import Progress

from neuro_sdk import LocalImage, RemoteImage
from neuro_sdk.parsing_utils import Tag
from neuro_sdk import LocalImage, RemoteImage, Tag, TagOption

from neuro_cli.formatters.images import (
BaseImagesFormatter,
Expand All @@ -21,7 +20,7 @@
)
from neuro_cli.formatters.utils import ImageFormatter, image_formatter, uri_formatter

from .click_types import RemoteImageType, RemoteTaglessImageType
from .click_types import RemoteImageType
from .root import Root
from .utils import (
argument,
Expand Down Expand Up @@ -140,7 +139,7 @@ async def ls(root: Root, format_long: bool, full_uri: bool) -> None:
@option(
"-l", "format_long", is_flag=True, help="List in long format, with image sizes."
)
@argument("image", type=RemoteTaglessImageType())
@argument("image", type=RemoteImageType(tag_option=TagOption.DENY))
async def tags(root: Root, format_long: bool, image: RemoteImage) -> None:
"""
List tags for image in platform registry.
Expand Down Expand Up @@ -182,8 +181,10 @@ async def tags(root: Root, format_long: bool, image: RemoteImage) -> None:
is_flag=True,
help="Force deletion of all tags referencing the image.",
)
@argument("image", type=RemoteImageType())
async def rm(root: Root, force: bool, image: RemoteImage) -> None:
@argument(
"images", nargs=-1, required=True, type=RemoteImageType(tag_option=TagOption.ALLOW)
)
async def rm(root: Root, force: bool, images: Sequence[RemoteImage]) -> None:
"""
Remove image from platform registry.

Expand All @@ -195,25 +196,11 @@ async def rm(root: Root, force: bool, image: RemoteImage) -> None:
neuro image rm image://myfriend/alpine:shared
neuro image rm image:myimage:latest
"""
digest = await root.client.images.digest(image)
root.print(
f"Deleting image identified by [bold]{rich_escape(digest)}[/bold]", markup=True
)
tags = await root.client.images.tags(replace(image, tag=None))
# Collect all tags referencing the image to be deleted
if not force and len(tags) > 1:
tags_for_image = []
for tag in tags:
tag_digest = await root.client.images.digest(tag)
if tag_digest == digest:
tags_for_image.append(tag_digest)
if len(tags_for_image) > 1:
raise ValueError(
f"There's more than one tag referencing this digest: "
f"{', '.join(tags_for_image)}.\n"
f"Please use -f to force deletion for all of them."
)
await root.client.images.rm(image, digest)
for image in images:
if image.tag is None:
await remove_image(root, image)
else:
await remove_tag(root, image, force=force)


@command()
Expand Down Expand Up @@ -259,3 +246,34 @@ async def digest(root: Root, image: RemoteImage) -> None:
image.add_command(size)
image.add_command(digest)
image.add_command(tags)


async def remove_image(root: Root, image: RemoteImage) -> None:
assert image.tag is None
images = await root.client.images.tags(image)
for img in images:
await remove_tag(root, img, force=True)


async def remove_tag(root: Root, image: RemoteImage, *, force: bool) -> None:
assert image.tag is not None
digest = await root.client.images.digest(image)
root.print(
f"Deleting {image} identified by [bold]{rich_escape(digest)}[/bold]",
markup=True,
)
tags = await root.client.images.tags(replace(image, tag=None))
# Collect all tags referencing the image to be deleted
if not force and len(tags) > 1:
tags_for_image = []
for tag in tags:
tag_digest = await root.client.images.digest(tag)
if tag_digest == digest:
tags_for_image.append(tag_digest)
if len(tags_for_image) > 1:
raise ValueError(
f"There's more than one tag referencing this digest: "
f"{', '.join(tags_for_image)}.\n"
f"Please use -f to force deletion for all of them."
)
await root.client.images.rm(image, digest)
6 changes: 3 additions & 3 deletions neuro-cli/src/neuro_cli/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
LOCAL_REMOTE_PORT,
PRESET,
TOP_COLUMNS,
ImageType,
RemoteImageType,
)
from .const import EX_PLATFORMERROR
from .formatters.jobs import (
Expand Down Expand Up @@ -592,7 +592,7 @@ async def renderer() -> None:

@command()
@argument("job", type=JOB)
@argument("image", type=ImageType())
@argument("image", type=RemoteImageType())
async def save(root: Root, job: str, image: RemoteImage) -> None:
"""
Save job's state to an image.
Expand Down Expand Up @@ -640,7 +640,7 @@ async def kill(root: Root, jobs: Sequence[str]) -> None:


@command(context_settings=dict(allow_interspersed_args=False))
@argument("image", type=ImageType())
@argument("image", type=RemoteImageType())
@argument("cmd", nargs=-1, type=click.UNPROCESSED)
@option(
"-s",
Expand Down
3 changes: 2 additions & 1 deletion neuro-sdk/src/neuro_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
StdStream,
)
from .parser import DiskVolume, Parser, SecretFile, Volume
from .parsing_utils import LocalImage, RemoteImage, TagOption
from .parsing_utils import LocalImage, RemoteImage, Tag, TagOption
from .plugins import ConfigBuilder, PluginManager
from .secrets import Secret, Secrets
from .server_cfg import Cluster
Expand Down Expand Up @@ -140,6 +140,7 @@
"StorageProgressLeaveDir",
"StorageProgressStart",
"StorageProgressStep",
"Tag",
"TagOption",
"Users",
"Volume",
Expand Down