Skip to content

Commit 439d505

Browse files
authored
Add 'container check' command for checking size and number of layers (#249)
1 parent ae8bc58 commit 439d505

File tree

4 files changed

+102
-23
lines changed

4 files changed

+102
-23
lines changed

.vscode/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@
33
"prettier.proseWrap": "always",
44
"python.testing.nosetestsEnabled": false,
55
"python.testing.pytestEnabled": true,
6-
"python.testing.unittestEnabled": false
6+
"python.testing.unittestEnabled": false,
7+
"[python]": {
8+
"editor.defaultFormatter": "ms-python.black-formatter"
9+
}
710
}

docs/tools.md

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,34 @@
11
# Supported Tools
22

3-
## make
3+
## Embedded commands
4+
5+
### changelog (github)
6+
7+
`changelog` command will produce a `CHANGELOG.md` file based on Github Releases.
8+
You can define `CHANGELOG_FILE` environment variable to make it generate the
9+
file in a different location.
10+
11+
This command is available only when `gh` command line utility is installed.
12+
13+
### containers check
14+
15+
You can use this command to verify if a specific container image has a maximum
16+
size or maximum number of layers. This is useful when you want to prevent
17+
accidental grow of an image your are producing.
18+
19+
```bash
20+
$ mk containers check your-image-id-or-name --max-size=200 --max-layers=1
21+
Image has too many layers: 3 > 1
22+
Image size exceeded the max required size (MB): 301 > 200
23+
FAIL: 1
24+
```
25+
26+
You can also specify the container engine to be used, the default is to use
27+
docker if found or podman if docker is not found.
28+
29+
## Recognized tools
30+
31+
### make
432

533
If a [makefile](https://www.gnu.org/software/make/manual/make.html) is found on
634
the repository root, the tool will expose all its targets that have a trailing
@@ -10,7 +38,7 @@ the command will not be exposed, as we assume that this is an internal target.
1038
A good example of a project using this pattern is
1139
[podman](https://github.com/containers/podman).
1240

13-
## npm
41+
### npm
1442

1543
If a [package.json](https://docs.npmjs.com/cli/v7/configuring-npm/package-json)
1644
file is found, the tool will expose all the scripts defined in the `scripts`.
@@ -21,28 +49,28 @@ files, we are unable to provide descriptions for exposed commands. Still, if
2149
others will find a good way to do it that gets some adoption, we will be more
2250
than happy to add support for loading descriptions too.
2351

24-
## shell
52+
### shell
2553

2654
All shell scripts found inside the repository root and`(scripts|tools|bin)/`
2755
sub-folders will be exposed as commands.
2856

29-
## taskfile
57+
### taskfile
3058

3159
[Taskfile](https://taskfile.dev/#/) is a task runner that uses YAML files. It is
3260
similar to make, but it is written in Go and it is more flexible.
3361

34-
## tox
62+
### tox
3563

3664
All tox environments will be exposed as commands and their descriptions will
3765
also be shown. Internally, the tool will run `tox -lav` to get the list of
3866
available environments and their descriptions.
3967

40-
## ansible
68+
### ansible
4169

4270
Any playbook found inside the `playbooks/` sub-folder will be exposed as a
4371
command.
4472

45-
## git
73+
### git
4674

4775
Inside git repositories, the tool will expose the `up` command which can be used
4876
to create an upstream pull request.
@@ -51,12 +79,12 @@ If the current git repository is using
5179
[Gerrit](https://www.gerritcodereview.com), it will run `git review` and if the
5280
repository is from GitHub, it will run `gh pr create` instead.
5381

54-
## pre-commit
82+
### pre-commit
5583

5684
If a [pre-commit](https://pre-commit.com/) configuration file is found, the tool
5785
will expose the `lint` command for running linting.
5886

59-
## py (python packages)
87+
### py (python packages)
6088

6189
If the current repository is a Python package, the tool will expose a set of
6290
basic commands:
@@ -65,15 +93,7 @@ basic commands:
6593
- `uninstall`: Uninstall the current package
6694
- `build`: Run `python -m build`
6795

68-
## pytest
96+
### pytest
6997

7098
If a [pytest](https://docs.pytest.org/en/stable/) configuration file is found, a
7199
`test` command will be exposed that runs `pytest`.
72-
73-
## changelog (github)
74-
75-
`changelog` command will produce a `CHANGELOG.md` file based on Github Releases.
76-
You can define `CHANGELOG_FILE` environment variable to make it generate the
77-
file in a different location.
78-
79-
This command is available only when `gh` command line utility is installed.

pyproject.toml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ repository = "https://github.com/pycontribs/mk"
6363
changelog = "https://github.com/pycontribs/mk/releases"
6464

6565
[tool.black]
66-
66+
# keep this value because typer does not accept new annotations such str | None
67+
# from https://peps.python.org/pep-0604/
68+
target-version = ["py39"]
6769

6870
# Keep this default because xml/report do not know to use load it from config file:
6971
# data_file = ".coverage"
@@ -74,7 +76,7 @@ source = ["src", ".tox/*/site-packages"]
7476
exclude_also = ["pragma: no cover", "if TYPE_CHECKING:"]
7577
omit = ["test/*"]
7678
# Increase it just so it would pass on any single-python run
77-
fail_under = 46
79+
fail_under = 45
7880
skip_covered = true
7981
skip_empty = true
8082
# During development we might remove code (files) with coverage data, and we dont want to fail:
@@ -135,10 +137,13 @@ disable = [
135137
]
136138

137139
[tool.ruff]
138-
target-version = "py310"
140+
# keep this as typer does not support new annotations format
141+
target-version = "py39"
139142
# Same as Black.
140143
line-length = 88
141144
lint.ignore = [
145+
# Disabled due to typer not supporting new annotations format
146+
"UP007",
142147
# temporary disabled until we fix them:
143148
"ANN",
144149
"B",

src/mk/__main__.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
import argparse
66
import itertools
7+
import json
78
import logging
89
import os
910
import shlex
10-
from typing import Any
11+
import shutil
12+
from typing import Annotated, Any, Optional
1113

1214
import typer
1315
from rich.console import Console
@@ -16,6 +18,7 @@
1618
from mk import __version__
1719
from mk._typer import CustomTyper
1820
from mk.ctx import ctx
21+
from mk.exec import run_or_fail
1922

2023
handlers: list[logging.Handler]
2124
console_err = Console(stderr=True)
@@ -87,6 +90,54 @@ def commands() -> None:
8790
print(action.name)
8891

8992

93+
@app.command()
94+
def containers(
95+
command: Annotated[
96+
Optional[str],
97+
typer.Argument(help="Command to run, possible values: check"),
98+
] = None,
99+
image: Annotated[
100+
str,
101+
typer.Argument(help="Specify image name or identifier"),
102+
] = "",
103+
engine: Annotated[
104+
str,
105+
typer.Option(help="Comma separated list of container engines to look for."),
106+
] = "docker,podman",
107+
max_size: Annotated[int, typer.Option(help="Maximum image size in MB")] = 0,
108+
max_layers: Annotated[int, typer.Option(help="Maximum number of layers")] = 0,
109+
) -> None:
110+
"""Provide some container related helpers."""
111+
if command != "check":
112+
typer.echo("Invalid command.")
113+
raise typer.Exit(code=1)
114+
if image:
115+
executable = None
116+
for v in engine.split(","):
117+
if shutil.which(v):
118+
executable = v
119+
break
120+
if not engine:
121+
typer.echo(f"Failed to find any container engine. ({engine})")
122+
raise typer.Exit(code=1)
123+
result = run_or_fail(f"{executable} image inspect {image}")
124+
inspect_json = json.loads(result.stdout)
125+
size = int(inspect_json[0]["Size"] / 1024 / 1024)
126+
layers = len(inspect_json[0]["RootFS"]["Layers"])
127+
failed = False
128+
if max_layers and layers > max_layers:
129+
typer.echo(f"Image has too many layers: {layers} > {max_layers}")
130+
failed = True
131+
if max_size and size > max_size:
132+
typer.echo(
133+
f"Image size exceeded the max required size (MB): {size} > {max_size}",
134+
)
135+
failed = True
136+
if failed:
137+
raise typer.Exit(code=1)
138+
typer.echo("Image check passed")
139+
140+
90141
def cli() -> None: # pylint: disable=too-many-locals
91142
parser = argparse.ArgumentParser(
92143
description="Preprocess arguments to set log level.",

0 commit comments

Comments
 (0)