diff --git a/python/Dockerfile b/python/Dockerfile index a051455..3867874 100644 --- a/python/Dockerfile +++ b/python/Dockerfile @@ -1,34 +1,28 @@ -FROM ubuntu:jammy as build-image +FROM ubuntu:noble as python-builder RUN apt-get update && \ - apt-get upgrade -y && \ - apt-get install --no-install-recommends python3.10-venv git -y && \ + apt-get install -y python3.12 python3.12-venv && \ rm -rf /var/lib/apt/lists/* -# build into a venv we can copy across -RUN python3 -m venv /opt/venv -ENV PATH="/opt/venv/bin:$PATH" +RUN python3.12 -m venv /venv && \ + /venv/bin/pip install -U pip setuptools -COPY . /perftest -RUN pip install -U pip setuptools -RUN pip install --no-deps --requirement /perftest/requirements.txt -RUN pip install -e /perftest +COPY requirements.txt /app/requirements.txt +RUN /venv/bin/pip install --no-deps --requirement /app/requirements.txt -# -# Now the image we run with -# -FROM ubuntu:jammy as run-image +# In order for template loading to work correctly, this has to be an editable mode install +COPY . /app +RUN /venv/bin/pip install --no-deps -e /app -RUN apt-get update && \ - apt-get upgrade -y && \ - apt-get install --no-install-recommends python3 tini ca-certificates -y && \ - rm -rf /var/lib/apt/lists/* -# Copy accross the venv -COPY --from=build-image /opt/venv /opt/venv -# Copy code to keep editable install working -COPY . /perftest -ENV PATH="/opt/venv/bin:$PATH" +FROM ubuntu:noble + +# Don't buffer stdout and stderr as it breaks realtime logging +ENV PYTHONUNBUFFERED 1 + +# Make httpx use the system trust roots +# By default, this means we use the roots baked into the image +ENV SSL_CERT_FILE /etc/ssl/certs/ca-certificates.crt # Create the user that will be used to run the app ENV APP_UID 1001 @@ -44,19 +38,17 @@ RUN groupadd --gid $APP_GID $APP_GROUP && \ --uid $APP_UID \ $APP_USER -# Install tini, which we will use to marshal the processes RUN apt-get update && \ - apt-get install -y tini && \ + apt-get install -y \ + --no-install-recommends \ + --no-install-suggests \ + ca-certificates python3.12 tini \ + && \ rm -rf /var/lib/apt/lists/* -# Don't buffer stdout and stderr as it breaks realtime logging -ENV PYTHONUNBUFFERED 1 - -# Make httpx use the system trust roots -# By default, this means we use the CAs from the ca-certificates package -ENV SSL_CERT_FILE /etc/ssl/certs/ca-certificates.crt +COPY --from=python-builder /venv /venv +COPY --from=python-builder /app /app -# By default, run the operator using kopf USER $APP_UID ENTRYPOINT ["tini", "-g", "--"] -CMD ["kopf", "run", "--module", "perftest.operator", "--all-namespaces"] +CMD ["/venv/bin/kopf", "run", "--module", "perftest.operator", "--all-namespaces"] diff --git a/python/perftest/models/v1alpha1/base.py b/python/perftest/models/v1alpha1/base.py index 4038710..1686a93 100644 --- a/python/perftest/models/v1alpha1/base.py +++ b/python/perftest/models/v1alpha1/base.py @@ -2,7 +2,7 @@ import ipaddress import typing as t -from pydantic import Field, constr +from pydantic import Field from kube_custom_resource import CustomResource, schema @@ -22,11 +22,11 @@ class ContainerResources(schema.BaseModel): """ Model for specifying container resources. """ - requests: schema.Dict[str, t.Any] = Field( + requests: schema.Dict[str, schema.Any] = Field( default_factory = dict, description = "The resource requests for the pod." ) - limits: schema.Dict[str, t.Any] = Field( + limits: schema.Dict[str, schema.Any] = Field( default_factory = dict, description = "The resource limits for the pod." ) @@ -40,18 +40,18 @@ class BenchmarkSpec(schema.BaseModel): False, description = "Indicates whether to use host networking or not." ) - network_name: t.Optional[constr(min_length = 1)] = Field( + network_name: schema.Optional[schema.constr(min_length = 1)] = Field( None, description = ( "The name of a Multus network over which to run the benchmark. " "Only used when host networking is false." ) ) - resources: t.Optional[ContainerResources] = Field( + resources: schema.Optional[ContainerResources] = Field( None, description = "The resources to use for benchmark containers." ) - mtu: t.Optional[schema.conint(gt = 0)] = Field( + mtu: schema.Optional[schema.conint(gt = 0)] = Field( None, description = ( "The MTU to use for the benchmark. " @@ -96,15 +96,15 @@ class ResourceRef(schema.BaseModel): """ Reference to a resource that is part of a benchmark. """ - api_version: constr(min_length = 1) = Field( + api_version: schema.constr(min_length = 1) = Field( ..., description = "The API version of the resource." ) - kind: constr(min_length = 1) = Field( + kind: schema.constr(min_length = 1) = Field( ..., description = "The kind of the resource." ) - name: constr(min_length = 1) = Field( + name: schema.constr(min_length = 1) = Field( ..., description = "The name of the resource." ) @@ -118,7 +118,7 @@ class PodInfo(schema.BaseModel): ..., description = "The IP of the pod." ) - node_name: constr(min_length = 1) = Field( + node_name: schema.constr(min_length = 1) = Field( ..., description = "The name of the node that the pod was scheduled on." ) @@ -147,7 +147,7 @@ class BenchmarkStatus(schema.BaseModel): BenchmarkPhase.UNKNOWN, description = "The phase of the benchmark." ) - priority_class_name: t.Optional[constr(min_length = 1)] = Field( + priority_class_name: schema.Optional[schema.constr(min_length = 1)] = Field( None, description = "The name of the priority class for the benchmark." ) @@ -155,11 +155,11 @@ class BenchmarkStatus(schema.BaseModel): default_factory = list, description = "List of references to the managed resources for this benchmark." ) - started_at: t.Optional[datetime.datetime] = Field( + started_at: schema.Optional[datetime.datetime] = Field( None, description = "The time at which the benchmark started." ) - finished_at: t.Optional[datetime.datetime] = Field( + finished_at: schema.Optional[datetime.datetime] = Field( None, description = "The time at which the benchmark finished." ) diff --git a/python/perftest/models/v1alpha1/fio.py b/python/perftest/models/v1alpha1/fio.py index 9980842..5adbbb4 100644 --- a/python/perftest/models/v1alpha1/fio.py +++ b/python/perftest/models/v1alpha1/fio.py @@ -1,9 +1,7 @@ -import itertools as it import json -import re import typing as t -from pydantic import Field, constr +from pydantic import Field from kube_custom_resource import schema @@ -13,6 +11,7 @@ from . import base + class FioRW(str, schema.Enum): """ Enumeration of supported Fio rw modes. @@ -24,6 +23,7 @@ class FioRW(str, schema.Enum): RW_READWRITE = "rw,readwrite" RANDRW = "randrw" + class FioDirect(int, schema.Enum): """ Enumeration of supported Fio direct Bools. @@ -31,17 +31,19 @@ class FioDirect(int, schema.Enum): TRUE = 1 FALSE = 0 + class FioIOEngine(str, schema.Enum): """ Enumeration of supported Fio ioengines. """ LIBAIO = "libaio" + class FioSpec(base.BenchmarkSpec): """ Defines the parameters for the Fio benchmark. """ - image: constr(min_length = 1) = Field( + image: schema.constr(min_length = 1) = Field( f"{settings.default_image_prefix}fio:{settings.default_image_tag}", description = "The image to use for the benchmark." ) @@ -53,7 +55,7 @@ class FioSpec(base.BenchmarkSpec): 8765, description = "The port that the Fio sever listens on." ) - volume_claim_template: schema.Dict[str, t.Any] = Field( + volume_claim_template: schema.Dict[str, schema.Any] = Field( default_factory = dict, description = "The template that describes the PVC to mount on workers." ) @@ -66,7 +68,7 @@ class FioSpec(base.BenchmarkSpec): FioRW.READ, description = "The value of the Fio rw config option." ) - bs: constr(regex = "\\d+(K|M|G|T|P)?") = Field( + bs: schema.constr(pattern = "\\d+(K|M|G|T|P)?") = Field( "4M", description = "The value of the Fio bs config option." ) @@ -98,7 +100,7 @@ class FioSpec(base.BenchmarkSpec): FioIOEngine.LIBAIO, description = "The value of the Fio ioengine config option." ) - runtime: constr(regex = "\\d+(D|H|M|s|ms|us)?") = Field( + runtime: schema.constr(pattern = "\\d+(D|H|M|s|ms|us)?") = Field( "30s", description = "The value of the Fio runtime config option." ) @@ -106,7 +108,7 @@ class FioSpec(base.BenchmarkSpec): 1, description = "The value of the Fio numjobs config option." ) - size: constr(regex = "\\d+(K|M|G|T|P)?") = Field( + size: schema.constr(pattern = "\\d+(K|M|G|T|P)?") = Field( "10G", description = "The value of the Fio size config option." ) @@ -152,32 +154,33 @@ class FioResult(schema.BaseModel): ..., description = "The aggregate read latency standard deviation." ) - + + class FioStatus(base.BenchmarkStatus): """ Represents the status of the Fio benchmark. """ - result: t.Optional[FioResult] = Field( + result: schema.Optional[FioResult] = Field( None, description = "The result of the benchmark." ) - read_bw_result: t.Optional[schema.IntOrString] = Field( + read_bw_result: schema.Optional[schema.IntOrString] = Field( None, description = "The summary result for read bw, used for display." ) - write_bw_result: t.Optional[schema.IntOrString] = Field( + write_bw_result: schema.Optional[schema.IntOrString] = Field( None, description = "The summary result for write bw, used for display." ) - read_iops_result: t.Optional[schema.confloat(ge = 0)] = Field( + read_iops_result: schema.Optional[schema.confloat(ge = 0)] = Field( None, description = "The summary result for read IOPs, used for display." ) - write_iops_result: t.Optional[schema.confloat(ge = 0)] = Field( + write_iops_result: schema.Optional[schema.confloat(ge = 0)] = Field( None, description = "The summary result for write IOPs, used for display." ) - master_pod: t.Optional[base.PodInfo] = Field( + master_pod: schema.Optional[base.PodInfo] = Field( None, description = "Pod information for the Fio master pod." ) @@ -185,11 +188,12 @@ class FioStatus(base.BenchmarkStatus): default_factory = dict, description = "Pod information for the worker pods, indexed by pod name." ) - client_log: t.Optional[constr(min_length = 1)] = Field( + client_log: schema.Optional[schema.constr(min_length = 1)] = Field( None, description = "The raw pod log of the client pod." ) + class Fio( base.Benchmark, subresources = {"status": {}}, diff --git a/python/perftest/models/v1alpha1/iperf.py b/python/perftest/models/v1alpha1/iperf.py index c1cdde0..5f690ad 100644 --- a/python/perftest/models/v1alpha1/iperf.py +++ b/python/perftest/models/v1alpha1/iperf.py @@ -2,7 +2,7 @@ import re import typing as t -from pydantic import Field, constr +from pydantic import Field from kube_custom_resource import schema @@ -17,7 +17,7 @@ class IPerfSpec(base.BenchmarkSpec): """ Defines the parameters for the iperf benchmark. """ - image: constr(min_length = 1) = Field( + image: schema.constr(min_length = 1) = Field( f"{settings.default_image_prefix}iperf:{settings.default_image_tag}", description = "The image to use for the benchmark." ) @@ -67,23 +67,23 @@ class IPerfStatus(base.BenchmarkStatus): """ Represents the status of the iperf benchmark. """ - summary_result: t.Optional[schema.IntOrString] = Field( + summary_result: schema.Optional[schema.IntOrString] = Field( None, description = "The summary result for the benchmark, used for display." ) - result: t.Optional[IPerfResult] = Field( + result: schema.Optional[IPerfResult] = Field( None, description = "The complete result for the benchmark." ) - client_log: t.Optional[constr(min_length = 1)] = Field( + client_log: schema.Optional[schema.constr(min_length = 1)] = Field( None, description = "The raw pod log of the client pod." ) - server_pod: t.Optional[base.PodInfo] = Field( + server_pod: schema.Optional[base.PodInfo] = Field( None, description = "Pod information for the server pod." ) - client_pod: t.Optional[base.PodInfo] = Field( + client_pod: schema.Optional[base.PodInfo] = Field( None, description = "Pod information for the client pod." ) diff --git a/python/perftest/models/v1alpha1/openfoam.py b/python/perftest/models/v1alpha1/openfoam.py index 6e9ab7d..8de2082 100644 --- a/python/perftest/models/v1alpha1/openfoam.py +++ b/python/perftest/models/v1alpha1/openfoam.py @@ -1,7 +1,7 @@ import re import typing as t -from pydantic import Field, constr +from pydantic import Field from kube_custom_resource import schema @@ -49,7 +49,7 @@ class OpenFOAMSpec(base.BenchmarkSpec): """ Defines the parameters for the openFOAM benchmark. """ - image: constr(min_length = 1) = Field( + image: schema.constr(min_length = 1) = Field( f"{settings.default_image_prefix}openfoam:{settings.default_image_tag}", description = "The image to use for the benchmark." ) @@ -105,11 +105,11 @@ class OpenFOAMStatus(base.BenchmarkStatus): """ Represents the status of the iperf benchmark. """ - result: t.Optional[OpenFOAMResult] = Field( + result: schema.Optional[OpenFOAMResult] = Field( None, description = "The result of the benchmark." ) - master_pod: t.Optional[base.PodInfo] = Field( + master_pod: schema.Optional[base.PodInfo] = Field( None, description = "Pod information for the MPI master pod." ) diff --git a/python/perftest/models/v1alpha1/pingpong.py b/python/perftest/models/v1alpha1/pingpong.py index 877d9f9..e3f9f3c 100644 --- a/python/perftest/models/v1alpha1/pingpong.py +++ b/python/perftest/models/v1alpha1/pingpong.py @@ -2,7 +2,7 @@ import re import typing as t -from pydantic import Field, constr +from pydantic import Field from kube_custom_resource import schema @@ -11,6 +11,7 @@ from . import base + MPI_PINGPONG_UNITS = re.compile( r"t\[(?P