Skip to content

Commit

Permalink
Migrate main menu config to image display policy objects
Browse files Browse the repository at this point in the history
  • Loading branch information
athornton committed Feb 5, 2025
1 parent 4fed99b commit f433f9a
Show file tree
Hide file tree
Showing 18 changed files with 395 additions and 28 deletions.
4 changes: 4 additions & 0 deletions changelog.d/20250204_145210_athornton_DM_48682_config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!-- Delete the sections that don't apply -->
### Backwards-incompatible changes

- Changed `num_*` in config to image policy object.
53 changes: 44 additions & 9 deletions controller/src/controller/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,20 @@
RESERVED_ENV,
RESERVED_PATHS,
)
from .models.domain.imagepolicy import (
RSPImagePolicy,
)
from .models.domain.kubernetes import (
Affinity,
PullPolicy,
Toleration,
VolumeAccessMode,
)
from .models.domain.menu import ImageDisplayPolicy, SpawnerMenuOptions
from .models.v1.lab import LabResources, LabSize, ResourceQuantity
from .models.v1.prepuller import (
DockerSourceOptions,
GARSourceOptions,
PrepullerOptions,
)
from .units import memory_to_bytes

Expand All @@ -64,6 +67,7 @@
"NFSVolumeSource",
"PVCVolumeResources",
"PVCVolumeSource",
"SpawnerMenuConfig",
"TmpSource",
"UserHomeDirectorySchema",
"VolumeConfig",
Expand Down Expand Up @@ -505,26 +509,57 @@ class GARSourceConfig(GARSourceOptions):
"""Configuration for a Google Artifact Registry source.
This is identical to the API model used to return the prepuller
configuration to an API client except that camel-case aliases are enabled.
configuration to an API client except that camel-case aliases are
enabled.
"""

model_config = ConfigDict(
alias_generator=to_camel, extra="forbid", populate_by_name=True
)


class PrepullerConfig(PrepullerOptions):
"""Configuration for the prepuller.
class ImageDisplayPolicyConfig(ImageDisplayPolicy):
"""Configuration for ImageDisplayPolicy.
This is identical to the API model used to return the prepuller
configuration to an API client except that camel-case aliases are enabled.
This is identical to the model used for image display policy, except
that camel-case aliases are enabled.
"""

model_config = ConfigDict(
alias_generator=to_camel, extra="forbid", populate_by_name=True
)

main: RSPImagePolicyConfig | None = None
dropdown: RSPImagePolicyConfig | None = None


class RSPImagePolicyConfig(RSPImagePolicy):
"""Configuration for RSPImagePolicy.
This is identical to the model used for RSP image policy, except
that camel-case aliases are enabled.
"""

model_config = ConfigDict(
alias_generator=to_camel, extra="forbid", populate_by_name=True
)


class SpawnerMenuConfig(SpawnerMenuOptions):
"""Configuration for how images are presented on the spawner page.
It controls both the main menu and the dropdown menu.
This is identical to the model used for menu control, except that
camel-case aliases are enabled.
"""

model_config = ConfigDict(
alias_generator=to_camel, extra="forbid", populate_by_name=True
)

source: DockerSourceConfig | GARSourceConfig
display_policy: ImageDisplayPolicyConfig


class LabSizeDefinition(BaseModel):
Expand Down Expand Up @@ -1202,13 +1237,13 @@ class Config(BaseSettings):
] = DisabledFileserverConfig()

images: Annotated[
PrepullerConfig,
SpawnerMenuConfig,
Field(
title="Available lab images",
description=(
"Configuration for which images to prepull and which images to"
" display in the spawner menu for users to choose from when"
" spawning labs"
" display in the spawner menu and dropdown for users to choose"
" from when spawning labs"
),
),
]
Expand Down
9 changes: 9 additions & 0 deletions controller/src/controller/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"KubernetesError",
"LabDeletionError",
"LabExistsError",
"MissingImageCountError",
"MissingObjectError",
"MissingSecretError",
"NoOperationError",
Expand Down Expand Up @@ -437,6 +438,14 @@ class LabDeletionError(SlackException):
"""


class MissingImageCountError(SlackException):
"""Image count is not specified.
When constructing the spawner menu, we need to know how many daily,
weekly, and release images to display.
"""


class MissingObjectError(SlackException):
"""An expected Kubernetes object is missing.
Expand Down
7 changes: 5 additions & 2 deletions controller/src/controller/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
from .config import Config
from .events import LabEvents
from .exceptions import NotConfiguredError
from .models.v1.prepuller import DockerSourceOptions, GARSourceOptions
from .models.v1.prepuller import (
DockerSourceOptions,
GARSourceOptions,
)
from .services.builder.fileserver import FileserverBuilder
from .services.builder.lab import LabBuilder
from .services.builder.prepuller import PrepullerBuilder
Expand Down Expand Up @@ -145,7 +148,7 @@ async def from_config(cls, config: Config) -> Self:

metadata_storage = MetadataStorage(config.metadata_path)
image_service = ImageService(
config=config.images,
config=config.images.to_prepuller_options(),
node_selector=config.lab.node_selector,
tolerations=config.lab.tolerations,
source=source,
Expand Down
106 changes: 106 additions & 0 deletions controller/src/controller/models/domain/imagepolicy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Models for image display policy."""

import datetime
from typing import Annotated, Any

from pydantic import BaseModel, BeforeValidator, Field, model_validator
from safir.pydantic import validate_exactly_one_of


def _empty_str_is_none(inp: Any) -> Any:
if isinstance(inp, str) and inp == "":
return None
return inp


class IndividualImageClassPolicy(BaseModel):
"""Policy for images to display within a given class.
The policy has both a 'number' and an 'age' field.
'number' means: display that many of whatever image class this is
attached to. `-1` or `None` are interpreted as "do not filter
(i.e. show all of this image class)" and `0` means "display no
images of this class." This has historically been the only filter
option.
'age' means: display any items of the class whose age is the
specified age or less. This age is a duration, specified by a
string as accepted by Safir's HumanTimeDelta. The empty string
means "do not filter this class at all."
Exactly one of these must be specified.
"""

age: Annotated[
datetime.timedelta | None,
Field(
BeforeValidator(_empty_str_is_none),
title="Age",
description="Maximum age of image to retain.",
),
] = None

number: Annotated[
int | None,
Field(
BeforeValidator(_empty_str_is_none),
title="Number",
description="Number of images to retain.",
),
] = None

_validate_options = model_validator(mode="after")(
validate_exactly_one_of("number", "age")
)


class RSPImagePolicy(BaseModel):
"""Aliases are never filtered. Default for everything else is "do not
filter".
"""

release: Annotated[
IndividualImageClassPolicy | None,
Field(title="Release", description="Policy for releases to display."),
] = None

weekly: Annotated[
IndividualImageClassPolicy | None,
Field(
title="Weekly", description="Policy for weekly builds to display."
),
] = None

daily: Annotated[
IndividualImageClassPolicy | None,
Field(
title="Daily", description="Policy for daily builds to display."
),
] = None

release_candidate: Annotated[
IndividualImageClassPolicy | None,
Field(
title="Release Candidate",
description="Policy for release candidate builds to display.",
),
] = None

experimental: Annotated[
IndividualImageClassPolicy | None,
Field(
title="Experimental",
description="Policy for experimental builds to display.",
),
] = None

unknown: Annotated[
IndividualImageClassPolicy | None,
Field(
title="Unknown",
description=(
"Policy for builds without parseable RSP tags to display."
),
),
] = None
Loading

0 comments on commit f433f9a

Please sign in to comment.