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

Make taggers manifest functions #2252

Merged
merged 2 commits into from
Mar 12, 2025
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.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All image manifests can be found in [the wiki](https://github.com/jupyter/docker
Affected: all images.

- **Non-breaking:** Add `conda` and `mamba` version taggers ([#2251](https://github.com/jupyter/docker-stacks/pull/2251)).
- **Non-breaking:** Make taggers and manifests functions ([#2252](https://github.com/jupyter/docker-stacks/pull/2252)).

## 2025-02-21

Expand Down
33 changes: 17 additions & 16 deletions docs/maintaining/tagging.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Tagging and manifest creation

The main purpose of the source code in [the `tagging` folder](https://github.com/jupyter/docker-stacks/tree/main/tagging) is to properly write tag files and manifests for single-platform images,
apply these tags, and merge single-platform images into one multi-arch image.
The main purpose of the source code in [the `tagging` folder](https://github.com/jupyter/docker-stacks/tree/main/tagging) is to
properly write tags file, build history line and manifest for a single-platform image,
apply these tags, and then merge single-platform images into one multi-arch image.

## What is a tag and a manifest

Expand All @@ -16,9 +17,9 @@ For example, we dump all `conda` packages with their versions into the manifest.

- All images are organized in a hierarchical tree.
More info on [image relationships](../using/selecting.md#image-relationships).
- Classes inherit from `TaggerInterface` and `ManifestInterface` to generate tags and manifest pieces by running commands in Docker containers.
- `TaggerInterface` and `ManifestInterface` are interfaces for functions to generate tags and manifest pieces by running commands in Docker containers.
- Tags and manifests are reevaluated for each image in the hierarchy since values may change between parent and child images.
- To tag an image and create its manifest, run `make hook/<somestack>` (e.g., `make hook/base-notebook`).
- To tag an image and create its manifest and build history line, run `make hook/<somestack>` (e.g., `make hook/base-notebook`).

## Utils

Expand Down Expand Up @@ -46,42 +47,42 @@ The prefix of commit hash (namely, 12 letters) is used as an image tag to make i

### Tagger

`Tagger` is a class that can be run inside a docker container to calculate a tag for an image.
`Tagger` is a function that runs commands inside a docker container to calculate a tag for an image.

All the taggers are inherited from `TaggerInterface`:
All the taggers follow `TaggerInterface`:

```{literalinclude} ../../tagging/taggers/tagger_interface.py
:language: py
:start-at: class TaggerInterface
:start-at: TaggerInterface
```

So, the `tag_value(container)` method gets a docker container as an input and returns a tag.
So, the `tagger(container)` gets a docker container as an input and returns a tag.

`SHATagger` example:
`commit_sha_tagger` example:

```{literalinclude} ../../tagging/taggers/sha.py
:language: py
:start-at: class SHATagger
:start-at: def
```

- `taggers/` subdirectory contains all the taggers.
- `taggers/` subdirectory contains all taggers.
- `apps/write_tags_file.py`, `apps/apply_tags.py`, and `apps/merge_tags.py` are Python executable used to write tags for an image, apply tags from a file, and create multi-arch images.

### Manifest

All manifest classes except `BuildInfo` are inherited from `ManifestInterface`
and `markdown_piece(container)` method returns a piece of the build manifest.
All manifest functions except `build_info_manifest` follow `ManifestInterface`
and `manifest(container)` method returns a piece of the build manifest.

```{literalinclude} ../../tagging/manifests/manifest_interface.py
:language: py
:start-at: class ManifestInterface
:start-at: ManifestInterface
```

`AptPackagesManifest` example:
`apt_packages_manifest` example:

```{literalinclude} ../../tagging/manifests/apt_packages.py
:language: py
:start-at: class AptPackagesManifest
:start-at: def
```

- `quoted_output(container, cmd)` simply runs the command inside a container using `DockerRunner.exec_cmd` and wraps it to triple quotes to create a valid markdown piece.
Expand Down
10 changes: 5 additions & 5 deletions tagging/apps/write_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from tagging.apps.config import Config
from tagging.hierarchy.get_manifests import get_manifests
from tagging.hierarchy.get_taggers import get_taggers
from tagging.manifests.build_info import BuildInfo, BuildInfoConfig
from tagging.manifests.build_info import BuildInfoConfig, build_info_manifest
from tagging.utils.docker_runner import DockerRunner
from tagging.utils.get_prefix import get_file_prefix, get_tag_prefix
from tagging.utils.git_helper import GitHelper
Expand All @@ -27,7 +27,7 @@ def get_build_history_line(config: Config, filename: str, container: Container)

taggers = get_taggers(config.image)
tags_prefix = get_tag_prefix(config.variant)
all_tags = [tags_prefix + "-" + tagger.tag_value(container) for tagger in taggers]
all_tags = [tags_prefix + "-" + tagger(container) for tagger in taggers]

date_column = f"`{BUILD_TIMESTAMP}`"
image_column = MARKDOWN_LINE_BREAK.join(
Expand Down Expand Up @@ -64,7 +64,7 @@ def get_manifest(config: Config, commit_hash_tag: str, container: Container) ->
LOGGER.info(f"Calculating manifest file for image: {config.image}")

manifests = get_manifests(config.image)
manifest_names = [manifest.__class__.__name__ for manifest in manifests]
manifest_names = [manifest.__name__ for manifest in manifests]
LOGGER.info(f"Using manifests: {manifest_names}")

build_info_config = BuildInfoConfig(
Expand All @@ -77,8 +77,8 @@ def get_manifest(config: Config, commit_hash_tag: str, container: Container) ->

markdown_pieces = [
f"# Build manifest for image: {config.image}:{commit_hash_tag}",
BuildInfo.markdown_piece(build_info_config).get_str(),
*(manifest.markdown_piece(container).get_str() for manifest in manifests),
build_info_manifest(build_info_config).get_str(),
*(manifest(container).get_str() for manifest in manifests),
]
markdown_content = "\n\n".join(markdown_pieces) + "\n"

Expand Down
4 changes: 2 additions & 2 deletions tagging/apps/write_tags_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ def get_tags(config: Config) -> list[str]:
tags = [f"{config.full_image()}:{tags_prefix}-latest"]
with DockerRunner(config.full_image()) as container:
for tagger in taggers:
tagger_name = tagger.__class__.__name__
tag_value = tagger.tag_value(container)
tagger_name = tagger.__name__
tag_value = tagger(container)
LOGGER.info(
f"Calculated tag, tagger_name: {tagger_name} tag_value: {tag_value}"
)
Expand Down
78 changes: 32 additions & 46 deletions tagging/hierarchy/images_hierarchy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,17 @@
# Distributed under the terms of the Modified BSD License.
from dataclasses import dataclass, field

from tagging.manifests.apt_packages import AptPackagesManifest
from tagging.manifests.conda_environment import CondaEnvironmentManifest
from tagging.manifests.julia_packages import JuliaPackagesManifest
from tagging.manifests.apt_packages import apt_packages_manifest
from tagging.manifests.conda_environment import conda_environment_manifest
from tagging.manifests.julia_packages import julia_packages_manifest
from tagging.manifests.manifest_interface import ManifestInterface
from tagging.manifests.r_packages import RPackagesManifest
from tagging.manifests.spark_info import SparkInfoManifest
from tagging.taggers.date import DateTagger
from tagging.taggers.sha import SHATagger
from tagging.manifests.r_packages import r_packages_manifest
from tagging.manifests.spark_info import spark_info_manifest
from tagging.taggers import versions
from tagging.taggers.date import date_tagger
from tagging.taggers.sha import commit_sha_tagger
from tagging.taggers.tagger_interface import TaggerInterface
from tagging.taggers.ubuntu_version import UbuntuVersionTagger
from tagging.taggers.versions import (
CondaVersionTagger,
JavaVersionTagger,
JuliaVersionTagger,
JupyterHubVersionTagger,
JupyterLabVersionTagger,
JupyterNotebookVersionTagger,
MambaVersionTagger,
PythonMajorMinorVersionTagger,
PythonVersionTagger,
PytorchVersionTagger,
RVersionTagger,
SparkVersionTagger,
TensorflowVersionTagger,
)
from tagging.taggers.ubuntu_version import ubuntu_version_tagger


@dataclass
Expand All @@ -40,55 +26,55 @@ class ImageDescription:
"docker-stacks-foundation": ImageDescription(
parent_image=None,
taggers=[
SHATagger(),
DateTagger(),
UbuntuVersionTagger(),
PythonMajorMinorVersionTagger(),
PythonVersionTagger(),
MambaVersionTagger(),
CondaVersionTagger(),
commit_sha_tagger,
date_tagger,
ubuntu_version_tagger,
versions.python_major_minor_tagger,
versions.python_tagger,
versions.mamba_tagger,
versions.conda_tagger,
],
manifests=[CondaEnvironmentManifest(), AptPackagesManifest()],
manifests=[conda_environment_manifest, apt_packages_manifest],
),
"base-notebook": ImageDescription(
parent_image="docker-stacks-foundation",
taggers=[
JupyterNotebookVersionTagger(),
JupyterLabVersionTagger(),
JupyterHubVersionTagger(),
versions.jupyter_notebook_tagger,
versions.jupyter_lab_tagger,
versions.jupyter_hub_tagger,
],
),
"minimal-notebook": ImageDescription(parent_image="base-notebook"),
"scipy-notebook": ImageDescription(parent_image="minimal-notebook"),
"r-notebook": ImageDescription(
parent_image="minimal-notebook",
taggers=[RVersionTagger()],
manifests=[RPackagesManifest()],
taggers=[versions.r_tagger],
manifests=[r_packages_manifest],
),
"julia-notebook": ImageDescription(
parent_image="minimal-notebook",
taggers=[JuliaVersionTagger()],
manifests=[JuliaPackagesManifest()],
taggers=[versions.julia_tagger],
manifests=[julia_packages_manifest],
),
"tensorflow-notebook": ImageDescription(
parent_image="scipy-notebook", taggers=[TensorflowVersionTagger()]
parent_image="scipy-notebook", taggers=[versions.tensorflow_tagger]
),
"pytorch-notebook": ImageDescription(
parent_image="scipy-notebook", taggers=[PytorchVersionTagger()]
parent_image="scipy-notebook", taggers=[versions.python_tagger]
),
"datascience-notebook": ImageDescription(
parent_image="scipy-notebook",
taggers=[RVersionTagger(), JuliaVersionTagger()],
manifests=[RPackagesManifest(), JuliaPackagesManifest()],
taggers=[versions.r_tagger, versions.julia_tagger],
manifests=[r_packages_manifest, julia_packages_manifest],
),
"pyspark-notebook": ImageDescription(
parent_image="scipy-notebook",
taggers=[SparkVersionTagger(), JavaVersionTagger()],
manifests=[SparkInfoManifest()],
taggers=[versions.spark_tagger, versions.java_tagger],
manifests=[spark_info_manifest],
),
"all-spark-notebook": ImageDescription(
parent_image="pyspark-notebook",
taggers=[RVersionTagger()],
manifests=[RPackagesManifest()],
taggers=[versions.r_tagger],
manifests=[r_packages_manifest],
),
}
14 changes: 6 additions & 8 deletions tagging/manifests/apt_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@
# Distributed under the terms of the Modified BSD License.
from docker.models.containers import Container

from tagging.manifests.manifest_interface import ManifestInterface, MarkdownPiece
from tagging.manifests.manifest_interface import MarkdownPiece
from tagging.utils.quoted_output import quoted_output


class AptPackagesManifest(ManifestInterface):
@staticmethod
def markdown_piece(container: Container) -> MarkdownPiece:
return MarkdownPiece(
title="## Apt Packages",
sections=[quoted_output(container, "apt list --installed")],
)
def apt_packages_manifest(container: Container) -> MarkdownPiece:
return MarkdownPiece(
title="## Apt Packages",
sections=[quoted_output(container, "apt list --installed")],
)
61 changes: 29 additions & 32 deletions tagging/manifests/build_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,36 +25,33 @@ def full_image(self) -> str:
return f"{self.registry}/{self.owner}/{self.image}"


class BuildInfo:
def build_info_manifest(config: BuildInfoConfig) -> MarkdownPiece:
"""BuildInfo doesn't fall under common interface, and we run it separately"""

@staticmethod
def markdown_piece(config: BuildInfoConfig) -> MarkdownPiece:
commit_hash = GitHelper.commit_hash()
commit_hash_tag = GitHelper.commit_hash_tag()
commit_message = GitHelper.commit_message()

# Unfortunately, `docker images` doesn't work when specifying `docker.io` as registry
fixed_registry = config.registry + "/" if config.registry != "docker.io" else ""

image_size = docker[
"images",
f"{fixed_registry}{config.owner}/{config.image}:latest",
"--format",
"{{.Size}}",
]().rstrip()

build_info = textwrap.dedent(
f"""\
- Build timestamp: {config.build_timestamp}
- Docker image: `{config.full_image()}:{commit_hash_tag}`
- Docker image size: {image_size}
- Git commit SHA: [{commit_hash}](https://github.com/{config.repository}/commit/{commit_hash})
- Git commit message:

```text
{{message}}
```"""
).format(message=commit_message)

return MarkdownPiece(title="## Build Info", sections=[build_info])
commit_hash = GitHelper.commit_hash()
commit_hash_tag = GitHelper.commit_hash_tag()
commit_message = GitHelper.commit_message()

# Unfortunately, `docker images` doesn't work when specifying `docker.io` as registry
fixed_registry = config.registry + "/" if config.registry != "docker.io" else ""

image_size = docker[
"images",
f"{fixed_registry}{config.owner}/{config.image}:latest",
"--format",
"{{.Size}}",
]().rstrip()

build_info = textwrap.dedent(
f"""\
- Build timestamp: {config.build_timestamp}
- Docker image: `{config.full_image()}:{commit_hash_tag}`
- Docker image size: {image_size}
- Git commit SHA: [{commit_hash}](https://github.com/{config.repository}/commit/{commit_hash})
- Git commit message:

```text
{{message}}
```"""
).format(message=commit_message)

return MarkdownPiece(title="## Build Info", sections=[build_info])
24 changes: 11 additions & 13 deletions tagging/manifests/conda_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,18 @@
# Distributed under the terms of the Modified BSD License.
from docker.models.containers import Container

from tagging.manifests.manifest_interface import ManifestInterface, MarkdownPiece
from tagging.manifests.manifest_interface import MarkdownPiece
from tagging.utils.docker_runner import DockerRunner
from tagging.utils.quoted_output import quoted_output


class CondaEnvironmentManifest(ManifestInterface):
@staticmethod
def markdown_piece(container: Container) -> MarkdownPiece:
return MarkdownPiece(
title="## Python Packages",
sections=[
DockerRunner.exec_cmd(container, "python --version"),
quoted_output(container, "conda info"),
quoted_output(container, "mamba info"),
quoted_output(container, "mamba list"),
],
)
def conda_environment_manifest(container: Container) -> MarkdownPiece:
return MarkdownPiece(
title="## Python Packages",
sections=[
DockerRunner.exec_cmd(container, "python --version"),
quoted_output(container, "conda info"),
quoted_output(container, "mamba info"),
quoted_output(container, "mamba list"),
],
)
Loading