|
| 1 | +/* |
| 2 | +Copyright 2026. |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +package v1alpha1 |
| 18 | + |
| 19 | +import ( |
| 20 | + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 21 | +) |
| 22 | + |
| 23 | +// NOTE: json tags are required. Any new fields you add must have json tags for |
| 24 | +// the fields to be serialized. Run "make generate manifests" after editing. |
| 25 | + |
| 26 | +// SingletonName is the only permitted name for a HyperFleetConfig. The resource |
| 27 | +// is a cluster-scoped singleton: a CEL validation rule pins the name to |
| 28 | +// "cluster", and cluster-scoped name uniqueness then guarantees at most one |
| 29 | +// instance (a second create is rejected by the API server as AlreadyExists, not |
| 30 | +// by CEL, since CEL cannot see other objects). No admission webhooks are used |
| 31 | +// (see architecture ADR-0019). |
| 32 | +const SingletonName = "cluster" |
| 33 | + |
| 34 | +// BundleType selects one of the operator-internal bundle definitions. It is a |
| 35 | +// selector only: the CR carries the choice of a bundle, never its contents. The |
| 36 | +// bundle controller (HYPERFLEET-1407) resolves the selected bundle into a |
| 37 | +// concrete component set. |
| 38 | +// |
| 39 | +// These constants are the single source of truth for the valid bundle values |
| 40 | +// and MUST stay in lockstep with the bundle definitions shipped inside the |
| 41 | +// operator. Adding a bundle means adding a constant here and the matching enum |
| 42 | +// value in the +kubebuilder:validation:Enum marker below. |
| 43 | +// |
| 44 | +// +kubebuilder:validation:Enum=cloud-capi;onprem-agent |
| 45 | +type BundleType string |
| 46 | + |
| 47 | +const ( |
| 48 | + // BundleCloudCAPI is the cloud, CAPI-based provisioning deployment |
| 49 | + // (managed OpenShift on a public cloud). |
| 50 | + BundleCloudCAPI BundleType = "cloud-capi" |
| 51 | + // BundleOnPremAgent is the on-premise, air-gapped (agent-based) deployment. |
| 52 | + BundleOnPremAgent BundleType = "onprem-agent" |
| 53 | +) |
| 54 | + |
| 55 | +// AllBundleTypes is the Go-level source of truth for the valid BundleType |
| 56 | +// values. The +kubebuilder:validation:Enum marker on BundleType must list |
| 57 | +// exactly these values; the lockstep guard test creates a HyperFleetConfig with |
| 58 | +// each entry and fails if any is not accepted by the CRD, catching drift between |
| 59 | +// the constants and the enum marker. |
| 60 | +var AllBundleTypes = []BundleType{BundleCloudCAPI, BundleOnPremAgent} |
| 61 | + |
| 62 | +// SizingProfile expresses sizing intent, not replica engineering. The operator |
| 63 | +// maps each profile to concrete replicas, resource requests/limits and HPA/PDB |
| 64 | +// defaults for the operand. |
| 65 | +// |
| 66 | +// +kubebuilder:validation:Enum=small;medium;large |
| 67 | +type SizingProfile string |
| 68 | + |
| 69 | +const ( |
| 70 | + // SizingProfileSmall is the default, lowest-footprint sizing profile. |
| 71 | + SizingProfileSmall SizingProfile = "small" |
| 72 | + // SizingProfileMedium is a mid-range sizing profile. |
| 73 | + SizingProfileMedium SizingProfile = "medium" |
| 74 | + // SizingProfileLarge is the highest-footprint sizing profile. |
| 75 | + SizingProfileLarge SizingProfile = "large" |
| 76 | +) |
| 77 | + |
| 78 | +// AllSizingProfiles is the Go-level source of truth for the valid SizingProfile |
| 79 | +// values; it must match the +kubebuilder:validation:Enum marker on |
| 80 | +// SizingProfile (asserted by the lockstep guard test). |
| 81 | +var AllSizingProfiles = []SizingProfile{SizingProfileSmall, SizingProfileMedium, SizingProfileLarge} |
| 82 | + |
| 83 | +// Condition types reported on HyperFleetConfig status. This is deliberate |
| 84 | +// operator-layer vocabulary describing installation health, and is distinct from |
| 85 | +// the HyperFleet API's own resource-condition vocabulary (Available/Ready/ |
| 86 | +// Reconciled/LastKnownReconciled/per-adapter; see architecture ADR-0007 and |
| 87 | +// ADR-0008). The bundle controller (HYPERFLEET-1409) populates these; this story |
| 88 | +// defines the schema only. |
| 89 | +const ( |
| 90 | + // ConditionAvailable is True when the installed operand (the API) is |
| 91 | + // deployed and healthy. |
| 92 | + ConditionAvailable = "Available" |
| 93 | + // ConditionProgressing is True while the operator is actively rolling out a |
| 94 | + // change to the operand. |
| 95 | + ConditionProgressing = "Progressing" |
| 96 | + // ConditionDegraded is True when the operator cannot reach or maintain the |
| 97 | + // desired state. |
| 98 | + ConditionDegraded = "Degraded" |
| 99 | +) |
| 100 | + |
| 101 | +// SecretReference references a Secret by name. Referenced Secrets must live in |
| 102 | +// the operator's own namespace: because HyperFleetConfig is cluster-scoped, no |
| 103 | +// namespace field is exposed (name-only + operator-namespace convention, decided |
| 104 | +// in the HYPERFLEET-1406 API review). |
| 105 | +// |
| 106 | +// TODO(HYPERFLEET-1512): the operator-namespace constraint is convention-only |
| 107 | +// today — the schema cannot enforce it (CEL sees no cross-object/namespace |
| 108 | +// state; ADR-0019 rules out webhooks). The reconciler must enforce it (resolve |
| 109 | +// the Secret in the operator's own namespace and surface a Degraded condition |
| 110 | +// when it is missing) once it lands. |
| 111 | +type SecretReference struct { |
| 112 | + // name is the name of the Secret in the operator's namespace. It must be a |
| 113 | + // valid DNS-1123 subdomain, matching what k8s.io/apimachinery/pkg/util/validation |
| 114 | + // enforces for Secret names (IsDNS1123Subdomain, max length 253), so an |
| 115 | + // unresolvable reference is rejected at admission rather than failing opaquely |
| 116 | + // when the reference is later resolved. |
| 117 | + // |
| 118 | + // +kubebuilder:validation:Required |
| 119 | + // +kubebuilder:validation:MinLength=1 |
| 120 | + // +kubebuilder:validation:MaxLength=253 |
| 121 | + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` |
| 122 | + Name string `json:"name"` |
| 123 | + |
| 124 | + // Maintainer note (free-standing so it stays out of the generated CRD |
| 125 | + // description): the MaxLength and Pattern markers on name are literal copies of |
| 126 | + // apimachinery's DNS1123SubdomainMaxLength (253) and dns1123SubdomainFmt. They |
| 127 | + // can't reference those symbols — controller-gen markers accept only literal |
| 128 | + // values, and the format constant is unexported — so keep them in sync by hand |
| 129 | + // if k8s.io/apimachinery/pkg/util/validation ever changes. |
| 130 | +} |
| 131 | + |
| 132 | +// DatabaseSpec configures the HyperFleet API's connection to its external |
| 133 | +// PostgreSQL database. The database is partner-provided; the operator never |
| 134 | +// provisions it. |
| 135 | +type DatabaseSpec struct { |
| 136 | + // secretRef references a Secret holding the database connection credentials. |
| 137 | + // The Secret must provide the keys db.host, db.port, db.name, db.user and |
| 138 | + // db.password. |
| 139 | + // |
| 140 | + // +kubebuilder:validation:Required |
| 141 | + SecretRef SecretReference `json:"secretRef"` |
| 142 | +} |
| 143 | + |
| 144 | +// AuthSpec configures partner-facing JWT authentication intent for the API. |
| 145 | +// Machinery details (JWKS rotation, public-path allowlist) remain |
| 146 | +// operator-internal defaults and are not exposed here. |
| 147 | +// |
| 148 | +// +kubebuilder:validation:XValidation:rule="!self.enabled || (has(self.issuer) && has(self.audience))",message="issuer and audience are required when auth is enabled" |
| 149 | +type AuthSpec struct { |
| 150 | + // enabled turns JWT authentication on for the API endpoint. It defaults to |
| 151 | + // true, so a config that omits it gets authentication ON. It is a pointer to |
| 152 | + // distinguish "unset" (apply the default, true) from an explicit false |
| 153 | + // (disable auth), which a non-pointer bool cannot express: with omitempty a |
| 154 | + // plain false is dropped and re-defaulted to true, so auth could never be |
| 155 | + // turned off via the typed client; without omitempty an unset field serializes |
| 156 | + // as false and suppresses the default. Only *bool avoids both traps. |
| 157 | + // |
| 158 | + // +kubebuilder:default=true |
| 159 | + // +optional |
| 160 | + Enabled *bool `json:"enabled,omitempty"` |
| 161 | + |
| 162 | + // issuer is the OIDC issuer URL that mints accepted tokens. Required when |
| 163 | + // enabled is true. Whenever it is set (regardless of enabled) it must be a |
| 164 | + // valid https URL with a host, so a malformed issuer is rejected at admission |
| 165 | + // rather than surfacing later at token-validation time. |
| 166 | + // |
| 167 | + // +kubebuilder:validation:MinLength=1 |
| 168 | + // +kubebuilder:validation:MaxLength=2048 |
| 169 | + // +kubebuilder:validation:XValidation:rule="isURL(self) && url(self).getScheme() == 'https' && url(self).getHostname() != ''",message="issuer must be a valid https URL" |
| 170 | + // +optional |
| 171 | + Issuer string `json:"issuer,omitempty"` |
| 172 | + |
| 173 | + // audience is the token audience the API requires. Required and non-empty |
| 174 | + // when enabled is true. |
| 175 | + // |
| 176 | + // +kubebuilder:validation:MinLength=1 |
| 177 | + // +kubebuilder:validation:MaxLength=253 |
| 178 | + // +optional |
| 179 | + Audience string `json:"audience,omitempty"` |
| 180 | +} |
| 181 | + |
| 182 | +// TLSSpec configures TLS for the API endpoint. The certificate material is |
| 183 | +// referenced, not described. |
| 184 | +type TLSSpec struct { |
| 185 | + // secretRef references a kubernetes.io/tls Secret (providing tls.crt and |
| 186 | + // tls.key) used to serve the API endpoint. |
| 187 | + // |
| 188 | + // +kubebuilder:validation:Required |
| 189 | + SecretRef SecretReference `json:"secretRef"` |
| 190 | +} |
| 191 | + |
| 192 | +// APISpec is the partner-facing configuration for the HyperFleet API component, |
| 193 | +// which lives in the shared tier of every bundle. |
| 194 | +type APISpec struct { |
| 195 | + // database configures the external PostgreSQL connection. |
| 196 | + // |
| 197 | + // +kubebuilder:validation:Required |
| 198 | + Database DatabaseSpec `json:"database"` |
| 199 | + |
| 200 | + // auth configures partner-facing JWT authentication intent. |
| 201 | + // |
| 202 | + // +kubebuilder:validation:Required |
| 203 | + Auth AuthSpec `json:"auth"` |
| 204 | + |
| 205 | + // tls optionally configures TLS for the API endpoint. When omitted, the |
| 206 | + // operator applies its default serving configuration. |
| 207 | + // |
| 208 | + // +optional |
| 209 | + TLS *TLSSpec `json:"tls,omitempty"` |
| 210 | + |
| 211 | + // profile selects a sizing profile for the API. Defaults to "small". |
| 212 | + // |
| 213 | + // +kubebuilder:default=small |
| 214 | + // +optional |
| 215 | + Profile SizingProfile `json:"profile,omitempty"` |
| 216 | +} |
| 217 | + |
| 218 | +// HyperFleetConfigSpec defines the desired state of HyperFleetConfig. It captures |
| 219 | +// partner intent only; internal machinery (broker, adapters, sentinel) is never |
| 220 | +// expressed here. |
| 221 | +type HyperFleetConfigSpec struct { |
| 222 | + // bundle selects one of the operator-internal bundle definitions. It is |
| 223 | + // immutable after creation: switching deployments requires recreating the |
| 224 | + // resource. |
| 225 | + // |
| 226 | + // +kubebuilder:validation:Required |
| 227 | + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="bundle is immutable" |
| 228 | + Bundle BundleType `json:"bundle"` |
| 229 | + |
| 230 | + // api is the partner-facing configuration for the HyperFleet API component. |
| 231 | + // |
| 232 | + // +kubebuilder:validation:Required |
| 233 | + API APISpec `json:"api"` |
| 234 | +} |
| 235 | + |
| 236 | +// HyperFleetConfigStatus defines the observed state of HyperFleetConfig. It is |
| 237 | +// populated by the bundle controller in later stories; this story defines the |
| 238 | +// schema only. |
| 239 | +type HyperFleetConfigStatus struct { |
| 240 | + // observedGeneration is the .metadata.generation the operator last acted on. |
| 241 | + // |
| 242 | + // +kubebuilder:validation:Minimum=0 |
| 243 | + // +optional |
| 244 | + ObservedGeneration int64 `json:"observedGeneration,omitempty"` |
| 245 | + |
| 246 | + // conditions represent the current installation health of the operand. |
| 247 | + // Recognized types are Available, Progressing and Degraded. |
| 248 | + // |
| 249 | + // +listType=map |
| 250 | + // +listMapKey=type |
| 251 | + // +optional |
| 252 | + Conditions []metav1.Condition `json:"conditions,omitempty"` |
| 253 | +} |
| 254 | + |
| 255 | +// +kubebuilder:object:root=true |
| 256 | +// +kubebuilder:subresource:status |
| 257 | +// +kubebuilder:resource:scope=Cluster,shortName=hfc |
| 258 | +// +kubebuilder:validation:XValidation:rule="self.metadata.name == 'cluster'",message="the only permitted name is 'cluster'; HyperFleetConfig is a cluster-scoped singleton" |
| 259 | +// +kubebuilder:printcolumn:name="Bundle",type=string,JSONPath=`.spec.bundle` |
| 260 | +// +kubebuilder:printcolumn:name="Profile",type=string,JSONPath=`.spec.api.profile` |
| 261 | +// +kubebuilder:printcolumn:name="Available",type=string,JSONPath=`.status.conditions[?(@.type=="Available")].status` |
| 262 | +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` |
| 263 | + |
| 264 | +// HyperFleetConfig is the Schema for the hyperfleetconfigs API. It is a |
| 265 | +// cluster-scoped singleton: exactly one instance, named "cluster", is permitted. |
| 266 | +type HyperFleetConfig struct { |
| 267 | + metav1.TypeMeta `json:",inline"` |
| 268 | + metav1.ObjectMeta `json:"metadata,omitempty"` |
| 269 | + |
| 270 | + // +kubebuilder:validation:Required |
| 271 | + Spec HyperFleetConfigSpec `json:"spec"` |
| 272 | + Status HyperFleetConfigStatus `json:"status,omitempty"` |
| 273 | +} |
| 274 | + |
| 275 | +// +kubebuilder:object:root=true |
| 276 | + |
| 277 | +// HyperFleetConfigList contains a list of HyperFleetConfig. |
| 278 | +type HyperFleetConfigList struct { |
| 279 | + metav1.TypeMeta `json:",inline"` |
| 280 | + metav1.ListMeta `json:"metadata,omitempty"` |
| 281 | + Items []HyperFleetConfig `json:"items"` |
| 282 | +} |
| 283 | + |
| 284 | +func init() { |
| 285 | + SchemeBuilder.Register(&HyperFleetConfig{}, &HyperFleetConfigList{}) |
| 286 | +} |
0 commit comments