Add the cluster-scoped ProviderHealth CRD

One operator-owned object per configured provider will carry the
periodic check result: a Ready condition, per-target verdicts with their
own transition times, and a ready/total count for kubectl get. There is
no spec; only a viewer role is shipped since the operator is the sole
writer. Written by hand (kubebuilder is not installed), registered in
the existing AddKnownTypes call rather than a second init().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-11 18:30:31 +00:00
co-authored by Claude Opus 5
parent 18a3378507
commit 1c773c24ad
9 changed files with 535 additions and 1 deletions
+127
View File
@@ -0,0 +1,127 @@
/*
Copyright 2026.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// ConditionReady summarizes a ProviderHealth's targets: True when every
// target passed its last check.
const ConditionReady = "Ready"
// ProviderTarget is the latest check verdict for one thing a provider
// depends on: a KVM hypervisor, the Kubernetes API server, a GCP project.
type ProviderTarget struct {
// name identifies the target within its provider: the KVM host name,
// "apiserver", or the GCP project.
// +kubebuilder:validation:MinLength=1
// +required
Name string `json:"name"`
// address is where the check connected to.
// +optional
Address string `json:"address,omitempty"`
// ready is true when the last check succeeded.
// +required
Ready bool `json:"ready"`
// reason is a CamelCase verdict such as Reachable, Unreachable,
// Forbidden, SSHHandshakeFailed or PoolNotActive.
// +optional
Reason string `json:"reason,omitempty"`
// message is human-readable detail, usually the error of a failed check.
// +kubebuilder:validation:MaxLength=1024
// +optional
Message string `json:"message,omitempty"`
// latencyMillis is how long the last check took.
// +optional
LatencyMillis int64 `json:"latencyMillis,omitempty"`
// lastTransitionTime is when ready last changed.
// +required
LastTransitionTime metav1.Time `json:"lastTransitionTime"`
// details are provider-specific facts from the last check, e.g. the
// libvirt version and free storage-pool space of a KVM host.
// +optional
Details map[string]string `json:"details,omitempty"`
}
// ProviderHealthStatus is the result of the latest check of one provider.
type ProviderHealthStatus struct {
// type is the provider's type from the providers config: kubernetes,
// gcp or kvm.
// +optional
Type string `json:"type,omitempty"`
// conditions hold Ready, which summarizes the targets.
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
// targets hold one verdict per checked target.
// +listType=map
// +listMapKey=name
// +optional
Targets []ProviderTarget `json:"targets,omitempty"`
// targetsReady counts the ready targets as "ready/total".
// +optional
TargetsReady string `json:"targetsReady,omitempty"`
// lastCheckTime is when the latest check finished.
// +optional
LastCheckTime *metav1.Time `json:"lastCheckTime,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster,shortName=ph
// +kubebuilder:printcolumn:name="Type",type=string,JSONPath=`.status.type`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
// +kubebuilder:printcolumn:name="Targets",type=string,JSONPath=`.status.targetsReady`
// +kubebuilder:printcolumn:name="Last Check",type=date,JSONPath=`.status.lastCheckTime`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
// ProviderHealth reports the periodic health check of one configured
// provider. It is named after the provider, and the operator creates,
// updates and deletes it itself; there is no spec to edit.
type ProviderHealth struct {
metav1.TypeMeta `json:",inline"`
// metadata is a standard object metadata
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
// status is the result of the latest check
// +optional
Status ProviderHealthStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
// ProviderHealthList contains a list of ProviderHealth
type ProviderHealthList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []ProviderHealth `json:"items"`
}
+1 -1
View File
@@ -361,7 +361,7 @@ type ProxyList struct {
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(SchemeGroupVersion, &Proxy{}, &ProxyList{})
s.AddKnownTypes(SchemeGroupVersion, &Proxy{}, &ProxyList{}, &ProviderHealth{}, &ProviderHealthList{})
return nil
})
}
+114
View File
@@ -95,6 +95,120 @@ func (in *PlacementSpec) DeepCopy() *PlacementSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderHealth) DeepCopyInto(out *ProviderHealth) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderHealth.
func (in *ProviderHealth) DeepCopy() *ProviderHealth {
if in == nil {
return nil
}
out := new(ProviderHealth)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ProviderHealth) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderHealthList) DeepCopyInto(out *ProviderHealthList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]ProviderHealth, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderHealthList.
func (in *ProviderHealthList) DeepCopy() *ProviderHealthList {
if in == nil {
return nil
}
out := new(ProviderHealthList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ProviderHealthList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderHealthStatus) DeepCopyInto(out *ProviderHealthStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Targets != nil {
in, out := &in.Targets, &out.Targets
*out = make([]ProviderTarget, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.LastCheckTime != nil {
in, out := &in.LastCheckTime, &out.LastCheckTime
*out = (*in).DeepCopy()
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderHealthStatus.
func (in *ProviderHealthStatus) DeepCopy() *ProviderHealthStatus {
if in == nil {
return nil
}
out := new(ProviderHealthStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderTarget) DeepCopyInto(out *ProviderTarget) {
*out = *in
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
if in.Details != nil {
in, out := &in.Details, &out.Details
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderTarget.
func (in *ProviderTarget) DeepCopy() *ProviderTarget {
if in == nil {
return nil
}
out := new(ProviderTarget)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Proxy) DeepCopyInto(out *Proxy) {
*out = *in