Skip to content
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
4 changes: 2 additions & 2 deletions kube_custom_resource/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .custom_resource import CustomResource, Scope
from .registry import CustomResourceRegistry
from .custom_resource import CustomResource, Scope # noqa: F401
from .registry import CustomResourceRegistry # noqa: F401
168 changes: 74 additions & 94 deletions kube_custom_resource/custom_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
import re
import typing

from pydantic_core import CoreSchema

from pydantic import Field, GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import CoreSchema

from .schema import BaseModel, Enum


#: Regex used for extracting versions from module names
VERSION_REGEX = re.compile(r"^v\d+((alpha|beta)\d+)?$")

Expand All @@ -19,7 +17,8 @@ class Scope(str, Enum):
"""
Enum representing the possible scopes for a custom resource.
"""
CLUSTER = "Cluster"

CLUSTER = "Cluster"
NAMESPACED = "Namespaced"


Expand All @@ -28,17 +27,18 @@ class CustomResourceMeta:
"""
Dataclass to hold metadata for the custom resource.
"""

# The API subgroup for the custom resource
api_subgroup: typing.Optional[str]
# The version of the custom resource
version: str
# The scope of the custom resource
scope: Scope
# The kind of the resource
# The kind of the resource
kind: str
# The singular name of the resource
# The singular name of the resource
singular_name: str
# The plural name of the resource
# The plural name of the resource
plural_name: str
# A list of short names for the resource
short_names: typing.List[str]
Expand All @@ -54,6 +54,7 @@ class CustomResourceMetaclass(type(BaseModel)):
"""
Metaclass for the custom resource class.
"""

def __new__(
cls,
name,
Expand All @@ -63,39 +64,42 @@ def __new__(
# Indicates that the resource is abstract and is not actually a CRD
abstract: bool = False,
# The API subgroup for the custom resource
# If not given, the resource is placed in the root api group
# If not given, the resource is placed in the root api group
api_subgroup: typing.Optional[str] = None,
# The version of the custom resource
# If not given, it is inferred from the module if possible
# If not given, it is inferred from the module if possible
version: typing.Optional[str] = None,
# The scope of the resource, either 'Namespaced' or 'Cluster'
# If not given, Namespaced is used by default
# If not given, Namespaced is used by default
scope: Scope = Scope.NAMESPACED,
# The kind of the resource
# If not given, the model class name is used
# The kind of the resource
# If not given, the model class name is used
kind: typing.Optional[str] = None,
# The singular name of the resource
# If not given, the lower-cased kind is used
# The singular name of the resource
# If not given, the lower-cased kind is used
singular_name: typing.Optional[str] = None,
# The plural name of the resource
# If not given, the singular name with 's' appended is used
# The plural name of the resource
# If not given, the singular name with 's' appended is used
plural_name: typing.Optional[str] = None,
# A list of short names for the resource
# If not given, no short names are used
# If not given, no short names are used
short_names: typing.Optional[typing.List[str]] = None,
# The subresources for the CRD
# If not given, no subresources are defined
# If not given, no subresources are defined
subresources: typing.Optional[typing.Dict[str, typing.Any]] = None,
# A list of printer column definitions for the version
# An "age" column is always appended to the given list
printer_columns: typing.Optional[typing.List[typing.Dict[str, typing.Any]]] = None,
printer_columns: typing.Optional[
typing.List[typing.Dict[str, typing.Any]]
] = None,
# Indicates if this is the storage version
storage_version: bool = True,
# Other kwargs, passed to Pydantic
**kwargs
**kwargs,
):
if not abstract:
# If no version is given, find the first module component that looks like a version
# If no version is given, find the first module component that looks like a
# version
if not version:
version = next(
part
Expand All @@ -107,8 +111,10 @@ def __new__(
singular_name = singular_name or kind.lower()
plural_name = plural_name or f"{singular_name}s"
# Add the metadata to the attrs
# We also add an annotation so that Pydantic leaves it alone
attrs.setdefault("__annotations__", {})["_meta"] = typing.ClassVar[CustomResourceMeta]
# We also add an annotation so that Pydantic leaves it alone
attrs.setdefault("__annotations__", {})["_meta"] = typing.ClassVar[
CustomResourceMeta
]
attrs["_meta"] = CustomResourceMeta(
api_subgroup,
version,
Expand All @@ -118,14 +124,15 @@ def __new__(
plural_name,
short_names or [],
subresources or {},
(printer_columns or []) + [
(printer_columns or [])
+ [
{
"name": "Age",
"type": "date",
"jsonPath": ".metadata.creationTimestamp",
},
],
storage_version
storage_version,
)
return super().__new__(cls, name, bases, attrs, **kwargs)

Expand All @@ -134,81 +141,68 @@ class OwnerReference(BaseModel):
"""
Model for an owner reference.
"""
api_version: str = Field(
...,
description = "The API version of the owner."
)
kind: str = Field(
...,
description = "The kind of the owner."
)
name: str = Field(
...,
description = "The name of the owner."
)
uid: str = Field(
...,
description = "The UID of the owner."
)

api_version: str = Field(..., description="The API version of the owner.")
kind: str = Field(..., description="The kind of the owner.")
name: str = Field(..., description="The name of the owner.")
uid: str = Field(..., description="The UID of the owner.")
block_owner_deletion: bool = Field(
False,
description = "If true, the owner cannot be removed until this resource is removed."
description=(
"If true, the owner cannot be removed until this resource is removed."
),
)
controller: bool = Field(
False,
description = "Indicates if this resource points to the managing controller."
description="Indicates if this resource points to the managing controller.",
)


class Metadata(BaseModel):
"""
Model for the metadata associated with a resource instance.
"""
name: str = Field(
...,
description = "The name of the resource."
)

name: str = Field(..., description="The name of the resource.")
namespace: typing.Optional[str] = Field(
None,
description = "The namespace for the resource."
None, description="The namespace for the resource."
)
labels: typing.Dict[str, str] = Field(
default_factory = dict,
description = "The labels for the resource. Can be used to match selectors."
default_factory=dict,
description="The labels for the resource. Can be used to match selectors.",
)
annotations: typing.Dict[str, str] = Field(
default_factory = dict,
description = "Annotations for the resource. Can be used to store arbitrary metadata."
default_factory=dict,
description=(
"Annotations for the resource. Can be used to store arbitrary metadata."
),
)
owner_references: typing.List[OwnerReference] = Field(
default_factory = list,
description = "List of resources that this resource depends on."
default_factory=list,
description="List of resources that this resource depends on.",
)
finalizers: typing.List[str] = Field(
default_factory = list,
description = "List of identifiers blocking removal of the resource."
)
uid: typing.Optional[str] = Field(
None,
description = "The UID for the resource."
default_factory=list,
description="List of identifiers blocking removal of the resource.",
)
uid: typing.Optional[str] = Field(None, description="The UID for the resource.")
creation_timestamp: typing.Optional[datetime.datetime] = Field(
None,
description = "The timestamp at which the resource was created."
None, description="The timestamp at which the resource was created."
)
deletion_timestamp: typing.Optional[datetime.datetime] = Field(
None,
description = "The timestamp at which the resource was deleted."
None, description="The timestamp at which the resource was deleted."
)
resource_version: typing.Optional[str] = Field(
None,
description = (
description=(
"The internal version of the resource. "
"Used to implement optimistic concurrency and resumable watches."
)
),
)

def add_owner_reference(self, owner, /, block_owner_deletion = False, controller = False):
def add_owner_reference(
self, owner, /, block_owner_deletion=False, controller=False
):
"""
Adds the given owner as a reference if it doesn't already exist.

Expand All @@ -220,43 +214,29 @@ def add_owner_reference(self, owner, /, block_owner_deletion = False, controller
else:
self.owner_references.append(
OwnerReference(
api_version = owner["apiVersion"],
kind = owner["kind"],
name = owner["metadata"]["name"],
uid = owner["metadata"]["uid"],
block_owner_deletion = block_owner_deletion,
controller = controller
api_version=owner["apiVersion"],
kind=owner["kind"],
name=owner["metadata"]["name"],
uid=owner["metadata"]["uid"],
block_owner_deletion=block_owner_deletion,
controller=controller,
)
)
return True


class CustomResource(
BaseModel,
metaclass = CustomResourceMetaclass,
abstract = True
):
class CustomResource(BaseModel, metaclass=CustomResourceMetaclass, abstract=True):
"""
Base class for defining custom resources.
"""
api_version: str = Field(
...,
description = "The API version of the resource."
)
kind: str = Field(
...,
description = "The kind of the resource."
)
metadata: Metadata = Field(
...,
description = "The metadata for the resource."
)

api_version: str = Field(..., description="The API version of the resource.")
kind: str = Field(..., description="The kind of the resource.")
metadata: Metadata = Field(..., description="The metadata for the resource.")

@classmethod
def __get_pydantic_json_schema__(
cls,
core_schema: CoreSchema,
handler: GetJsonSchemaHandler
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
json_schema = handler(core_schema)
json_schema = handler.resolve_ref_schema(json_schema)
Expand Down
10 changes: 5 additions & 5 deletions kube_custom_resource/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
@click.option(
"--category",
"categories",
multiple = True,
help = "Category to place CRDs in. Can be specified multiple times."
multiple=True,
help="Category to place CRDs in. Can be specified multiple times.",
)
@click.argument("models_mod")
@click.argument("api_group")
@click.argument("output_dir", type = click.Path())
def main(models_mod, api_group, output_dir, categories = None):
@click.argument("output_dir", type=click.Path())
def main(models_mod, api_group, output_dir, categories=None):
"""
Generate CRDs from the models in the given module with the given API group.

Expand All @@ -31,7 +31,7 @@ def main(models_mod, api_group, output_dir, categories = None):
registry.discover_models(models_module)
# Produce the CRDs in the output directory
output_dir = pathlib.Path(output_dir)
output_dir.mkdir(parents = True, exist_ok = True)
output_dir.mkdir(parents=True, exist_ok=True)
for crd in registry:
output_path = output_dir / f"{crd.plural_name}.{crd.api_group}.yaml"
with output_path.open("w") as fh:
Expand Down
Loading