diff --git a/api/v1alpha1/ironic_types.go b/api/v1alpha1/ironic_types.go index bcb634f4..72a31bf0 100644 --- a/api/v1alpha1/ironic_types.go +++ b/api/v1alpha1/ironic_types.go @@ -169,6 +169,32 @@ type Networking struct { RPCPort int32 `json:"rpcPort,omitempty"` } +// CPUArchitecture represents a CPU architecture supported by IPA. +// +kubebuilder:validation:Enum=x86_64;aarch64 +type CPUArchitecture string + +const ( + // ArchX86_64 represents the x86_64 (amd64) architecture. + ArchX86_64 CPUArchitecture = "x86_64" + // ArchAarch64 represents the aarch64 (arm64) architecture. + ArchAarch64 CPUArchitecture = "aarch64" +) + +// AgentImages defines a single IPA (Ironic Python Agent) image configuration. +type AgentImages struct { + // Kernel is the URL of the IPA kernel image. + // Example: "file:///shared/html/images/ironic-python-agent.kernel" + Kernel string `json:"kernel"` + + // Initramfs is the URL of the IPA initramfs/ramdisk image. + // Example: "file:///shared/html/images/ironic-python-agent.initramfs" + Initramfs string `json:"initramfs"` + + // Architecture is the target CPU architecture for this image. + // Each image must have a unique architecture. + Architecture CPUArchitecture `json:"architecture"` +} + // DeployRamdisk defines IPA ramdisk settings. type DeployRamdisk struct { // DisableDownloader tells the operator not to start the IPA downloader as the init container. @@ -250,7 +276,6 @@ type Images struct { // Warning: modifying arbitrary options may cause your Ironic installation to // fail or misbehave. Do not modify anything you don't understand well. type ExtraConfig struct { - // The group that config belongs to. // +optional Group string `json:"group,omitempty"` @@ -292,11 +317,28 @@ type Overrides struct { // +optional Containers []corev1.Container `json:"containers,omitempty"` + // HttpdLivenessProbe configures the httpd container liveness probe. + // If not set and AgentImages is not specified, defaults to checking /images/ironic-python-agent.kernel exists. + // When AgentImages is specified, no default probe is configured. + // +optional + HttpdLivenessProbe *corev1.Probe `json:"httpdLivenessProbe,omitempty"` + + // HttpdReadinessProbe configures the httpd container readiness probe. + // If not set and AgentImages is not specified, defaults to checking /images/ironic-python-agent.kernel exists. + // When AgentImages is specified, no default probe is configured. + // +optional + HttpdReadinessProbe *corev1.Probe `json:"httpdReadinessProbe,omitempty"` + // InitContainers to append to the main Ironic pod. // If a container name matches an existing init container, the existing init container is replaced. // +optional InitContainers []corev1.Container `json:"initContainers,omitempty"` + // AgentImages overrides the default IPA (Ironic Python Agent) images provided by the downloader. + // Each image must have a unique architecture (rendered as DEPLOY_KERNEL_BY_ARCH/DEPLOY_RAMDISK_BY_ARCH). + // +optional + AgentImages []AgentImages `json:"agentImages,omitempty"` + // Extra labels to add to each pod (including upgrade jobs). // +optional Labels map[string]string `json:"labels,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index d23ec762..23641a16 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -26,6 +26,21 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentImages) DeepCopyInto(out *AgentImages) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentImages. +func (in *AgentImages) DeepCopy() *AgentImages { + if in == nil { + return nil + } + out := new(AgentImages) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DHCP) DeepCopyInto(out *DHCP) { *out = *in @@ -306,6 +321,16 @@ func (in *Overrides) DeepCopyInto(out *Overrides) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.HttpdLivenessProbe != nil { + in, out := &in.HttpdLivenessProbe, &out.HttpdLivenessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.HttpdReadinessProbe != nil { + in, out := &in.HttpdReadinessProbe, &out.HttpdReadinessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } if in.InitContainers != nil { in, out := &in.InitContainers, &out.InitContainers *out = make([]v1.Container, len(*in)) @@ -313,6 +338,11 @@ func (in *Overrides) DeepCopyInto(out *Overrides) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.AgentImages != nil { + in, out := &in.AgentImages, &out.AgentImages + *out = make([]AgentImages, len(*in)) + copy(*out, *in) + } if in.Labels != nil { in, out := &in.Labels, &out.Labels *out = make(map[string]string, len(*in)) diff --git a/config/crd/bases/ironic.metal3.io_ironics.yaml b/config/crd/bases/ironic.metal3.io_ironics.yaml index b057dd88..a7cec183 100644 --- a/config/crd/bases/ironic.metal3.io_ironics.yaml +++ b/config/crd/bases/ironic.metal3.io_ironics.yaml @@ -313,6 +313,38 @@ spec: Overrides for the generated Deployment or Daemon Set. EXPERIMENTAL: requires feature gate Overrides. properties: + agentImages: + description: |- + AgentImages overrides the default IPA (Ironic Python Agent) images provided by the downloader. + Each image must have a unique architecture (rendered as DEPLOY_KERNEL_BY_ARCH/DEPLOY_RAMDISK_BY_ARCH). + items: + description: AgentImages defines a single IPA (Ironic Python + Agent) image configuration. + properties: + architecture: + description: |- + Architecture is the target CPU architecture for this image. + Each image must have a unique architecture. + enum: + - x86_64 + - aarch64 + type: string + initramfs: + description: |- + Initramfs is the URL of the IPA initramfs/ramdisk image. + Example: "file:///shared/html/images/ironic-python-agent.initramfs" + type: string + kernel: + description: |- + Kernel is the URL of the IPA kernel image. + Example: "file:///shared/html/images/ironic-python-agent.kernel" + type: string + required: + - architecture + - initramfs + - kernel + type: object + type: array annotations: additionalProperties: type: string @@ -1851,6 +1883,314 @@ spec: - name type: object type: array + httpdLivenessProbe: + description: |- + HttpdLivenessProbe configures the httpd container liveness probe. + If not set and AgentImages is not specified, defaults to checking /images/ironic-python-agent.kernel exists. + When AgentImages is specified, no default probe is configured. + properties: + exec: + description: Exec specifies a command to execute in the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header to + be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + httpdReadinessProbe: + description: |- + HttpdReadinessProbe configures the httpd container readiness probe. + If not set and AgentImages is not specified, defaults to checking /images/ironic-python-agent.kernel exists. + When AgentImages is specified, no default probe is configured. + properties: + exec: + description: Exec specifies a command to execute in the container. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies a GRPC HealthCheckRequest. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies an HTTP GET request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header to + be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies a connection to a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object initContainers: description: |- InitContainers to append to the main Ironic pod. diff --git a/docs/api.md b/docs/api.md index bbbf6216..4b44c31c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -659,6 +659,14 @@ EXPERIMENTAL: requires feature gate Overrides. + agentImages + []object + + AgentImages overrides the default IPA (Ironic Python Agent) images provided by the downloader. +Each image must have a unique architecture (rendered as DEPLOY_KERNEL_BY_ARCH/DEPLOY_RAMDISK_BY_ARCH).
+ + false + annotations map[string]string @@ -673,6 +681,24 @@ EXPERIMENTAL: requires feature gate Overrides. If a container name matches an existing container, the existing container is replaced.
false + + httpdLivenessProbe + object + + HttpdLivenessProbe configures the httpd container liveness probe. +If not set and AgentImages is not specified, defaults to checking /images/ironic-python-agent.kernel exists. +When AgentImages is specified, no default probe is configured.
+ + false + + httpdReadinessProbe + object + + HttpdReadinessProbe configures the httpd container readiness probe. +If not set and AgentImages is not specified, defaults to checking /images/ironic-python-agent.kernel exists. +When AgentImages is specified, no default probe is configured.
+ + false initContainers []object @@ -692,6 +718,52 @@ If a container name matches an existing init container, the existing init contai +### Ironic.spec.overrides.agentImages[index] +[↩ Parent](#ironicspecoverrides) + + + +AgentImages defines a single IPA (Ironic Python Agent) image configuration. + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
architectureenum + Architecture is the target CPU architecture for this image. +Each image must have a unique architecture.
+
+ Enum: x86_64, aarch64
+
true
initramfsstring + Initramfs is the URL of the IPA initramfs/ramdisk image. +Example: "file:///shared/html/images/ironic-python-agent.initramfs"
+
true
kernelstring + Kernel is the URL of the IPA kernel image. +Example: "file:///shared/html/images/ironic-python-agent.kernel"
+
true
+ + ### Ironic.spec.overrides.containers[index] [↩ Parent](#ironicspecoverrides) @@ -3755,6 +3827,648 @@ SubPathExpr and SubPath are mutually exclusive.
+### Ironic.spec.overrides.httpdLivenessProbe +[↩ Parent](#ironicspecoverrides) + + + +HttpdLivenessProbe configures the httpd container liveness probe. +If not set and AgentImages is not specified, defaults to checking /images/ironic-python-agent.kernel exists. +When AgentImages is specified, no default probe is configured. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
execobject + Exec specifies a command to execute in the container.
+
false
failureThresholdinteger + Minimum consecutive failures for the probe to be considered failed after having succeeded. +Defaults to 3. Minimum value is 1.
+
+ Format: int32
+
false
grpcobject + GRPC specifies a GRPC HealthCheckRequest.
+
false
httpGetobject + HTTPGet specifies an HTTP GET request to perform.
+
false
initialDelaySecondsinteger + Number of seconds after the container has started before liveness probes are initiated. +More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes
+
+ Format: int32
+
false
periodSecondsinteger + How often (in seconds) to perform the probe. +Default to 10 seconds. Minimum value is 1.
+
+ Format: int32
+
false
successThresholdinteger + Minimum consecutive successes for the probe to be considered successful after having failed. +Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.
+
+ Format: int32
+
false
tcpSocketobject + TCPSocket specifies a connection to a TCP port.
+
false
terminationGracePeriodSecondsinteger + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. +The grace period is the duration in seconds after the processes running in the pod are sent +a termination signal and the time when the processes are forcibly halted with a kill signal. +Set this value longer than the expected cleanup time for your process. +If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this +value overrides the value provided by the pod spec. +Value must be non-negative integer. The value zero indicates stop immediately via +the kill signal (no opportunity to shut down). +This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. +Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.
+
+ Format: int64
+
false
timeoutSecondsinteger + Number of seconds after which the probe times out. +Defaults to 1 second. Minimum value is 1. +More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes
+
+ Format: int32
+
false
+ + +### Ironic.spec.overrides.httpdLivenessProbe.exec +[↩ Parent](#ironicspecoverrideshttpdlivenessprobe) + + + +Exec specifies a command to execute in the container. + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
command[]string + Command is the command line to execute inside the container, the working directory for the +command is root ('/') in the container's filesystem. The command is simply exec'd, it is +not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use +a shell, you need to explicitly call out to that shell. +Exit status of 0 is treated as live/healthy and non-zero is unhealthy.
+
false
+ + +### Ironic.spec.overrides.httpdLivenessProbe.grpc +[↩ Parent](#ironicspecoverrideshttpdlivenessprobe) + + + +GRPC specifies a GRPC HealthCheckRequest. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
portinteger + Port number of the gRPC service. Number must be in the range 1 to 65535.
+
+ Format: int32
+
true
servicestring + Service is the name of the service to place in the gRPC HealthCheckRequest +(see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + +If this is not specified, the default behavior is defined by gRPC.
+
+ Default:
+
false
+ + +### Ironic.spec.overrides.httpdLivenessProbe.httpGet +[↩ Parent](#ironicspecoverrideshttpdlivenessprobe) + + + +HTTPGet specifies an HTTP GET request to perform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
portint or string + Name or number of the port to access on the container. +Number must be in the range 1 to 65535. +Name must be an IANA_SVC_NAME.
+
true
hoststring + Host name to connect to, defaults to the pod IP. You probably want to set +"Host" in httpHeaders instead.
+
false
httpHeaders[]object + Custom headers to set in the request. HTTP allows repeated headers.
+
false
pathstring + Path to access on the HTTP server.
+
false
schemestring + Scheme to use for connecting to the host. +Defaults to HTTP.
+
false
+ + +### Ironic.spec.overrides.httpdLivenessProbe.httpGet.httpHeaders[index] +[↩ Parent](#ironicspecoverrideshttpdlivenessprobehttpget) + + + +HTTPHeader describes a custom header to be used in HTTP probes + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring + The header field name. +This will be canonicalized upon output, so case-variant names will be understood as the same header.
+
true
valuestring + The header field value
+
true
+ + +### Ironic.spec.overrides.httpdLivenessProbe.tcpSocket +[↩ Parent](#ironicspecoverrideshttpdlivenessprobe) + + + +TCPSocket specifies a connection to a TCP port. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
portint or string + Number or name of the port to access on the container. +Number must be in the range 1 to 65535. +Name must be an IANA_SVC_NAME.
+
true
hoststring + Optional: Host name to connect to, defaults to the pod IP.
+
false
+ + +### Ironic.spec.overrides.httpdReadinessProbe +[↩ Parent](#ironicspecoverrides) + + + +HttpdReadinessProbe configures the httpd container readiness probe. +If not set and AgentImages is not specified, defaults to checking /images/ironic-python-agent.kernel exists. +When AgentImages is specified, no default probe is configured. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
execobject + Exec specifies a command to execute in the container.
+
false
failureThresholdinteger + Minimum consecutive failures for the probe to be considered failed after having succeeded. +Defaults to 3. Minimum value is 1.
+
+ Format: int32
+
false
grpcobject + GRPC specifies a GRPC HealthCheckRequest.
+
false
httpGetobject + HTTPGet specifies an HTTP GET request to perform.
+
false
initialDelaySecondsinteger + Number of seconds after the container has started before liveness probes are initiated. +More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes
+
+ Format: int32
+
false
periodSecondsinteger + How often (in seconds) to perform the probe. +Default to 10 seconds. Minimum value is 1.
+
+ Format: int32
+
false
successThresholdinteger + Minimum consecutive successes for the probe to be considered successful after having failed. +Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.
+
+ Format: int32
+
false
tcpSocketobject + TCPSocket specifies a connection to a TCP port.
+
false
terminationGracePeriodSecondsinteger + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. +The grace period is the duration in seconds after the processes running in the pod are sent +a termination signal and the time when the processes are forcibly halted with a kill signal. +Set this value longer than the expected cleanup time for your process. +If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this +value overrides the value provided by the pod spec. +Value must be non-negative integer. The value zero indicates stop immediately via +the kill signal (no opportunity to shut down). +This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. +Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.
+
+ Format: int64
+
false
timeoutSecondsinteger + Number of seconds after which the probe times out. +Defaults to 1 second. Minimum value is 1. +More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes
+
+ Format: int32
+
false
+ + +### Ironic.spec.overrides.httpdReadinessProbe.exec +[↩ Parent](#ironicspecoverrideshttpdreadinessprobe) + + + +Exec specifies a command to execute in the container. + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
command[]string + Command is the command line to execute inside the container, the working directory for the +command is root ('/') in the container's filesystem. The command is simply exec'd, it is +not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use +a shell, you need to explicitly call out to that shell. +Exit status of 0 is treated as live/healthy and non-zero is unhealthy.
+
false
+ + +### Ironic.spec.overrides.httpdReadinessProbe.grpc +[↩ Parent](#ironicspecoverrideshttpdreadinessprobe) + + + +GRPC specifies a GRPC HealthCheckRequest. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
portinteger + Port number of the gRPC service. Number must be in the range 1 to 65535.
+
+ Format: int32
+
true
servicestring + Service is the name of the service to place in the gRPC HealthCheckRequest +(see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + +If this is not specified, the default behavior is defined by gRPC.
+
+ Default:
+
false
+ + +### Ironic.spec.overrides.httpdReadinessProbe.httpGet +[↩ Parent](#ironicspecoverrideshttpdreadinessprobe) + + + +HTTPGet specifies an HTTP GET request to perform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
portint or string + Name or number of the port to access on the container. +Number must be in the range 1 to 65535. +Name must be an IANA_SVC_NAME.
+
true
hoststring + Host name to connect to, defaults to the pod IP. You probably want to set +"Host" in httpHeaders instead.
+
false
httpHeaders[]object + Custom headers to set in the request. HTTP allows repeated headers.
+
false
pathstring + Path to access on the HTTP server.
+
false
schemestring + Scheme to use for connecting to the host. +Defaults to HTTP.
+
false
+ + +### Ironic.spec.overrides.httpdReadinessProbe.httpGet.httpHeaders[index] +[↩ Parent](#ironicspecoverrideshttpdreadinessprobehttpget) + + + +HTTPHeader describes a custom header to be used in HTTP probes + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring + The header field name. +This will be canonicalized upon output, so case-variant names will be understood as the same header.
+
true
valuestring + The header field value
+
true
+ + +### Ironic.spec.overrides.httpdReadinessProbe.tcpSocket +[↩ Parent](#ironicspecoverrideshttpdreadinessprobe) + + + +TCPSocket specifies a connection to a TCP port. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
portint or string + Number or name of the port to access on the container. +Number must be in the range 1 to 65535. +Name must be an IANA_SVC_NAME.
+
true
hoststring + Optional: Host name to connect to, defaults to the pod IP.
+
false
+ + ### Ironic.spec.overrides.initContainers[index] [↩ Parent](#ironicspecoverrides) diff --git a/pkg/ironic/containers.go b/pkg/ironic/containers.go index bd5a5746..47a40cde 100644 --- a/pkg/ironic/containers.go +++ b/pkg/ironic/containers.go @@ -36,7 +36,7 @@ const ( knownExistingPath = "/images/ironic-python-agent.kernel" ) -func buildCommonEnvVars(ironic *metal3api.Ironic) []corev1.EnvVar { +func buildCommonEnvVars(ironic *metal3api.Ironic) ([]corev1.EnvVar, error) { result := []corev1.EnvVar{ { Name: "IRONIC_LISTEN_PORT", @@ -133,7 +133,43 @@ func buildCommonEnvVars(ironic *metal3api.Ironic) []corev1.EnvVar { result = appendListOfStringsEnv(result, "IRONIC_INSPECTOR_VLAN_INTERFACES", ironic.Spec.Inspection.VLANInterfaces, ",") - return result + if ironic.Spec.Overrides != nil { + var err error + result, err = appendAgentImageEnvVars(result, ironic.Spec.Overrides.AgentImages) + if err != nil { + return nil, err + } + } + + return result, nil +} + +// appendAgentImageEnvVars appends agent image environment variables based on the images configuration. +// If images are specified, always uses DEPLOY_KERNEL_BY_ARCH and DEPLOY_RAMDISK_BY_ARCH. +// Returns an error if duplicate architectures are found. +func appendAgentImageEnvVars(envVars []corev1.EnvVar, images []metal3api.AgentImages) ([]corev1.EnvVar, error) { + if len(images) == 0 { + return envVars, nil + } + + seen := make(map[metal3api.CPUArchitecture]bool, len(images)) + kernelByArch := make([]string, 0, len(images)) + ramdiskByArch := make([]string, 0, len(images)) + + for _, img := range images { + if seen[img.Architecture] { + return nil, fmt.Errorf("duplicate architecture %q in agent images", img.Architecture) + } + seen[img.Architecture] = true + + arch := string(img.Architecture) + kernelByArch = append(kernelByArch, arch+":"+strings.Trim(img.Kernel, " \t\n\r")) + ramdiskByArch = append(ramdiskByArch, arch+":"+strings.Trim(img.Initramfs, " \t\n\r")) + } + + envVars = appendStringEnv(envVars, "DEPLOY_KERNEL_BY_ARCH", strings.Join(kernelByArch, ",")) + envVars = appendStringEnv(envVars, "DEPLOY_RAMDISK_BY_ARCH", strings.Join(ramdiskByArch, ",")) + return envVars, nil } func buildExtraConfigVars(ironic *metal3api.Ironic) []corev1.EnvVar { @@ -205,8 +241,11 @@ func buildTrustedCAEnvVars(cctx ControllerContext, configMap *corev1.ConfigMap) } } -func buildIronicEnvVars(cctx ControllerContext, resources Resources) []corev1.EnvVar { - result := buildCommonEnvVars(resources.Ironic) +func buildIronicEnvVars(cctx ControllerContext, resources Resources) ([]corev1.EnvVar, error) { + result, err := buildCommonEnvVars(resources.Ironic) + if err != nil { + return nil, err + } result = append(result, []corev1.EnvVar{ { Name: "IRONIC_USE_MARIADB", @@ -293,11 +332,14 @@ func buildIronicEnvVars(cctx ControllerContext, resources Resources) []corev1.En }) } - return result + return result, nil } -func buildHttpdEnvVars(resources Resources) []corev1.EnvVar { - result := buildCommonEnvVars(resources.Ironic) +func buildHttpdEnvVars(resources Resources) ([]corev1.EnvVar, error) { + result, err := buildCommonEnvVars(resources.Ironic) + if err != nil { + return nil, err + } // When TLS is used, httpd is responsible for authentication if resources.TLSSecret != nil { @@ -316,7 +358,7 @@ func buildHttpdEnvVars(resources Resources) []corev1.EnvVar { ) } - return result + return result, nil } func databaseClientMounts(db *metal3api.Database) (volumes []corev1.Volume, mounts []corev1.VolumeMount) { @@ -552,10 +594,13 @@ func newURLProbeHandler(https bool, port int, path string, requiresOk bool) core } } -func newDnsmasqContainer(versionInfo VersionInfo, ironic *metal3api.Ironic) corev1.Container { +func newDnsmasqContainer(versionInfo VersionInfo, ironic *metal3api.Ironic) (corev1.Container, error) { dhcp := ironic.Spec.Networking.DHCP - envVars := buildCommonEnvVars(ironic) + envVars, err := buildCommonEnvVars(ironic) + if err != nil { + return corev1.Container{}, err + } envVars = append(envVars, corev1.EnvVar{ Name: "DHCP_RANGE", Value: buildDHCPRange(dhcp), @@ -592,7 +637,7 @@ func newDnsmasqContainer(versionInfo VersionInfo, ironic *metal3api.Ironic) core }, LivenessProbe: probe, ReadinessProbe: probe, - } + }, nil } func newKeepalivedContainer(versionInfo VersionInfo, ironic *metal3api.Ironic) corev1.Container { @@ -698,15 +743,42 @@ func newIronicPodTemplate(cctx ControllerContext, resources Resources) (corev1.P ironicPorts, httpdPorts := buildIronicHttpdPorts(resources.Ironic) ironicHandler := newURLProbeHandler(resources.TLSSecret != nil, int(resources.Ironic.Spec.Networking.APIPort), "/v1", true) - httpPathExpected := !resources.Ironic.Spec.DeployRamdisk.DisableDownloader - httpdHandler := newURLProbeHandler(false, int(resources.Ironic.Spec.Networking.ImageServerPort), knownExistingPath, httpPathExpected) + + // Default probes check the standard IPA kernel path. + // When custom agent images are specified, no default probes are set - must be configured via Overrides. + var httpdLivenessProbe, httpdReadinessProbe *corev1.Probe + hasCustomImages := resources.Ironic.Spec.Overrides != nil && len(resources.Ironic.Spec.Overrides.AgentImages) > 0 + if !hasCustomImages { + httpPathExpected := !resources.Ironic.Spec.DeployRamdisk.DisableDownloader + httpdHandler := newURLProbeHandler(false, int(resources.Ironic.Spec.Networking.ImageServerPort), knownExistingPath, httpPathExpected) + httpdLivenessProbe = newProbe(httpdHandler) + httpdReadinessProbe = newProbe(httpdHandler) + } + if resources.Ironic.Spec.Overrides != nil { + if resources.Ironic.Spec.Overrides.HttpdLivenessProbe != nil { + httpdLivenessProbe = resources.Ironic.Spec.Overrides.HttpdLivenessProbe + } + if resources.Ironic.Spec.Overrides.HttpdReadinessProbe != nil { + httpdReadinessProbe = resources.Ironic.Spec.Overrides.HttpdReadinessProbe + } + } + + ironicEnvVars, err := buildIronicEnvVars(cctx, resources) + if err != nil { + return corev1.PodTemplateSpec{}, err + } + + httpdEnvVars, err := buildHttpdEnvVars(resources) + if err != nil { + return corev1.PodTemplateSpec{}, err + } containers := []corev1.Container{ { Name: "ironic", Image: cctx.VersionInfo.IronicImage, Command: []string{"/bin/runironic"}, - Env: buildIronicEnvVars(cctx, resources), + Env: ironicEnvVars, VolumeMounts: mounts, SecurityContext: &corev1.SecurityContext{ RunAsUser: ptr.To(ironicUser), @@ -723,7 +795,7 @@ func newIronicPodTemplate(cctx ControllerContext, resources Resources) (corev1.P Name: "httpd", Image: cctx.VersionInfo.IronicImage, Command: []string{"/bin/runhttpd"}, - Env: buildHttpdEnvVars(resources), + Env: httpdEnvVars, VolumeMounts: mounts, SecurityContext: &corev1.SecurityContext{ RunAsUser: ptr.To(ironicUser), @@ -733,8 +805,8 @@ func newIronicPodTemplate(cctx ControllerContext, resources Resources) (corev1.P }, }, Ports: httpdPorts, - LivenessProbe: newProbe(httpdHandler), - ReadinessProbe: newProbe(httpdHandler), + LivenessProbe: httpdLivenessProbe, + ReadinessProbe: httpdReadinessProbe, }, { Name: "ramdisk-logs", @@ -755,7 +827,11 @@ func newIronicPodTemplate(cctx ControllerContext, resources Resources) (corev1.P if err != nil { return corev1.PodTemplateSpec{}, err } - containers = append(containers, newDnsmasqContainer(cctx.VersionInfo, resources.Ironic)) + dnsmasqContainer, err := newDnsmasqContainer(cctx.VersionInfo, resources.Ironic) + if err != nil { + return corev1.PodTemplateSpec{}, err + } + containers = append(containers, dnsmasqContainer) } if resources.Ironic.Spec.Networking.IPAddressManager == metal3api.IPAddressManagerKeepalived { diff --git a/pkg/ironic/containers_test.go b/pkg/ironic/containers_test.go index 2d9105db..196c70f0 100644 --- a/pkg/ironic/containers_test.go +++ b/pkg/ironic/containers_test.go @@ -524,3 +524,179 @@ func TestPrometheusExporterEnvVars(t *testing.T) { }) } } + +func TestAppendAgentImageEnvVars(t *testing.T) { + testCases := []struct { + name string + images []metal3api.AgentImages + expectedKernelByArch string + expectedRamdiskByArch string + expectNoEnvVars bool + expectError bool + }{ + { + name: "empty images", + images: nil, + expectNoEnvVars: true, + }, + { + name: "single architecture x86_64", + images: []metal3api.AgentImages{ + { + Kernel: "file:///shared/html/images/ipa.x86_64.kernel", + Initramfs: "file:///shared/html/images/ipa.x86_64.initramfs", + Architecture: metal3api.ArchX86_64, + }, + }, + expectedKernelByArch: "x86_64:file:///shared/html/images/ipa.x86_64.kernel", + expectedRamdiskByArch: "x86_64:file:///shared/html/images/ipa.x86_64.initramfs", + }, + { + name: "single architecture aarch64", + images: []metal3api.AgentImages{ + { + Kernel: "file:///shared/html/images/ipa.aarch64.kernel", + Initramfs: "file:///shared/html/images/ipa.aarch64.initramfs", + Architecture: metal3api.ArchAarch64, + }, + }, + expectedKernelByArch: "aarch64:file:///shared/html/images/ipa.aarch64.kernel", + expectedRamdiskByArch: "aarch64:file:///shared/html/images/ipa.aarch64.initramfs", + }, + { + name: "multiple architectures", + images: []metal3api.AgentImages{ + { + Kernel: "file:///shared/html/images/ipa.x86_64.kernel", + Initramfs: "file:///shared/html/images/ipa.x86_64.initramfs", + Architecture: metal3api.ArchX86_64, + }, + { + Kernel: "file:///shared/html/images/ipa.aarch64.kernel", + Initramfs: "file:///shared/html/images/ipa.aarch64.initramfs", + Architecture: metal3api.ArchAarch64, + }, + }, + expectedKernelByArch: "x86_64:file:///shared/html/images/ipa.x86_64.kernel,aarch64:file:///shared/html/images/ipa.aarch64.kernel", + expectedRamdiskByArch: "x86_64:file:///shared/html/images/ipa.x86_64.initramfs,aarch64:file:///shared/html/images/ipa.aarch64.initramfs", + }, + { + name: "whitespace trimming", + images: []metal3api.AgentImages{ + { + Kernel: " file:///path/kernel ", + Initramfs: "\tfile:///path/initramfs\n", + Architecture: metal3api.ArchX86_64, + }, + }, + expectedKernelByArch: "x86_64:file:///path/kernel", + expectedRamdiskByArch: "x86_64:file:///path/initramfs", + }, + { + name: "http protocol single architecture", + images: []metal3api.AgentImages{ + { + Kernel: "http://192.168.1.100:8080/images/ipa.kernel", + Initramfs: "http://192.168.1.100:8080/images/ipa.initramfs", + Architecture: metal3api.ArchX86_64, + }, + }, + expectedKernelByArch: "x86_64:http://192.168.1.100:8080/images/ipa.kernel", + expectedRamdiskByArch: "x86_64:http://192.168.1.100:8080/images/ipa.initramfs", + }, + { + name: "https protocol single architecture", + images: []metal3api.AgentImages{ + { + Kernel: "https://images.example.com/ipa/kernel", + Initramfs: "https://images.example.com/ipa/initramfs", + Architecture: metal3api.ArchAarch64, + }, + }, + expectedKernelByArch: "aarch64:https://images.example.com/ipa/kernel", + expectedRamdiskByArch: "aarch64:https://images.example.com/ipa/initramfs", + }, + { + name: "http protocol multiple architectures", + images: []metal3api.AgentImages{ + { + Kernel: "http://server:8080/x86_64/ipa.kernel", + Initramfs: "http://server:8080/x86_64/ipa.initramfs", + Architecture: metal3api.ArchX86_64, + }, + { + Kernel: "http://server:8080/aarch64/ipa.kernel", + Initramfs: "http://server:8080/aarch64/ipa.initramfs", + Architecture: metal3api.ArchAarch64, + }, + }, + expectedKernelByArch: "x86_64:http://server:8080/x86_64/ipa.kernel,aarch64:http://server:8080/aarch64/ipa.kernel", + expectedRamdiskByArch: "x86_64:http://server:8080/x86_64/ipa.initramfs,aarch64:http://server:8080/aarch64/ipa.initramfs", + }, + { + name: "mixed protocols across architectures", + images: []metal3api.AgentImages{ + { + Kernel: "http://local-server/ipa.x86_64.kernel", + Initramfs: "http://local-server/ipa.x86_64.initramfs", + Architecture: metal3api.ArchX86_64, + }, + { + Kernel: "https://secure-server/ipa.aarch64.kernel", + Initramfs: "https://secure-server/ipa.aarch64.initramfs", + Architecture: metal3api.ArchAarch64, + }, + }, + expectedKernelByArch: "x86_64:http://local-server/ipa.x86_64.kernel,aarch64:https://secure-server/ipa.aarch64.kernel", + expectedRamdiskByArch: "x86_64:http://local-server/ipa.x86_64.initramfs,aarch64:https://secure-server/ipa.aarch64.initramfs", + }, + { + name: "duplicate architectures returns error", + images: []metal3api.AgentImages{ + { + Kernel: "file:///first/kernel", + Initramfs: "file:///first/initramfs", + Architecture: metal3api.ArchX86_64, + }, + { + Kernel: "file:///second/kernel", + Initramfs: "file:///second/initramfs", + Architecture: metal3api.ArchX86_64, + }, + }, + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + envVars, err := appendAgentImageEnvVars(nil, tc.images) + + if tc.expectError { + assert.Error(t, err) + return + } + require.NoError(t, err) + + if tc.expectNoEnvVars { + assert.Empty(t, envVars) + return + } + + require.Len(t, envVars, 2) + + var kernelByArch, ramdiskByArch string + for _, env := range envVars { + switch env.Name { + case "DEPLOY_KERNEL_BY_ARCH": + kernelByArch = env.Value + case "DEPLOY_RAMDISK_BY_ARCH": + ramdiskByArch = env.Value + } + } + + assert.Equal(t, tc.expectedKernelByArch, kernelByArch) + assert.Equal(t, tc.expectedRamdiskByArch, ramdiskByArch) + }) + } +} diff --git a/pkg/ironic/validation.go b/pkg/ironic/validation.go index 918b20dc..887ebcb3 100644 --- a/pkg/ironic/validation.go +++ b/pkg/ironic/validation.go @@ -38,6 +38,33 @@ func validateIPinPrefix(ip string, prefix netip.Prefix) error { return nil } +func validateAgentImages(images []metal3api.AgentImages) error { + if len(images) == 0 { + return nil + } + + seenArchitectures := make(map[metal3api.CPUArchitecture]bool) + + for i, img := range images { + if img.Architecture == "" { + return fmt.Errorf("overrides.agentImages[%d]: architecture is required", i) + } + if img.Kernel == "" { + return fmt.Errorf("overrides.agentImages[%d]: kernel is required", i) + } + if img.Initramfs == "" { + return fmt.Errorf("overrides.agentImages[%d]: initramfs is required", i) + } + + if seenArchitectures[img.Architecture] { + return fmt.Errorf("overrides.agentImages: duplicate architecture %q", img.Architecture) + } + seenArchitectures[img.Architecture] = true + } + + return nil +} + func ValidateDHCP(ironic *metal3api.IronicSpec) error { dhcp := ironic.Networking.DHCP hasNetworking := ironic.Networking.IPAddress != "" || ironic.Networking.Interface != "" || len(ironic.Networking.MACAddresses) > 0 @@ -153,5 +180,11 @@ func ValidateIronic(ironic *metal3api.IronicSpec, old *metal3api.IronicSpec) err } } + if ironic.Overrides != nil { + if err := validateAgentImages(ironic.Overrides.AgentImages); err != nil { + return err + } + } + return nil } diff --git a/pkg/ironic/validation_test.go b/pkg/ironic/validation_test.go index 18d0f8d5..a65048a2 100644 --- a/pkg/ironic/validation_test.go +++ b/pkg/ironic/validation_test.go @@ -343,6 +343,115 @@ func TestValidateIronic(t *testing.T) { }, ExpectedError: "ServiceMonitor support is currently incompatible with the highly available architecture", }, + { + Scenario: "valid agent images single architecture x86_64", + Ironic: metal3api.IronicSpec{ + Overrides: &metal3api.Overrides{ + AgentImages: []metal3api.AgentImages{ + { + Architecture: metal3api.ArchX86_64, + Kernel: "file:///shared/html/images/ipa.x86_64.kernel", + Initramfs: "file:///shared/html/images/ipa.x86_64.initramfs", + }, + }, + }, + }, + }, + { + Scenario: "valid agent images single architecture aarch64", + Ironic: metal3api.IronicSpec{ + Overrides: &metal3api.Overrides{ + AgentImages: []metal3api.AgentImages{ + { + Architecture: metal3api.ArchAarch64, + Kernel: "file:///shared/html/images/ipa.aarch64.kernel", + Initramfs: "file:///shared/html/images/ipa.aarch64.initramfs", + }, + }, + }, + }, + }, + { + Scenario: "valid agent images multiple architectures", + Ironic: metal3api.IronicSpec{ + Overrides: &metal3api.Overrides{ + AgentImages: []metal3api.AgentImages{ + { + Architecture: metal3api.ArchX86_64, + Kernel: "file:///shared/html/images/ipa.x86_64.kernel", + Initramfs: "file:///shared/html/images/ipa.x86_64.initramfs", + }, + { + Architecture: metal3api.ArchAarch64, + Kernel: "file:///shared/html/images/ipa.aarch64.kernel", + Initramfs: "file:///shared/html/images/ipa.aarch64.initramfs", + }, + }, + }, + }, + }, + { + Scenario: "agent images empty architecture", + Ironic: metal3api.IronicSpec{ + Overrides: &metal3api.Overrides{ + AgentImages: []metal3api.AgentImages{ + { + Kernel: "file:///shared/html/images/ipa.kernel", + Initramfs: "file:///shared/html/images/ipa.initramfs", + }, + }, + }, + }, + ExpectedError: "overrides.agentImages[0]: architecture is required", + }, + { + Scenario: "agent images empty kernel", + Ironic: metal3api.IronicSpec{ + Overrides: &metal3api.Overrides{ + AgentImages: []metal3api.AgentImages{ + { + Architecture: metal3api.ArchX86_64, + Initramfs: "file:///shared/html/images/ipa.initramfs", + }, + }, + }, + }, + ExpectedError: "overrides.agentImages[0]: kernel is required", + }, + { + Scenario: "agent images empty initramfs", + Ironic: metal3api.IronicSpec{ + Overrides: &metal3api.Overrides{ + AgentImages: []metal3api.AgentImages{ + { + Architecture: metal3api.ArchX86_64, + Kernel: "file:///shared/html/images/ipa.kernel", + }, + }, + }, + }, + ExpectedError: "overrides.agentImages[0]: initramfs is required", + }, + { + Scenario: "agent images duplicate architecture", + Ironic: metal3api.IronicSpec{ + Overrides: &metal3api.Overrides{ + AgentImages: []metal3api.AgentImages{ + { + Architecture: metal3api.ArchX86_64, + Kernel: "file:///shared/html/images/ipa.x86_64.kernel", + Initramfs: "file:///shared/html/images/ipa.x86_64.initramfs", + }, + { + Architecture: metal3api.ArchX86_64, + Kernel: "file:///shared/html/images/ipa.x86_64.v2.kernel", + Initramfs: "file:///shared/html/images/ipa.x86_64.v2.initramfs", + }, + }, + }, + }, + ExpectedError: "overrides.agentImages: duplicate architecture \"x86_64\"", + }, } for _, tc := range testCases {