From 9bdcfac07600c12d15be5f5fc995cab8d642a78d Mon Sep 17 00:00:00 2001 From: Chris Doan Date: Fri, 31 Jul 2026 16:02:54 -0500 Subject: [PATCH 1/7] ROSAENG-61801: add v2alpha1 public API module with passthrough types and codegen pipeline New standalone module at api/public/v2alpha1/ with generated passthrough types (HostedClusterSpecPassthrough, NodePoolSpecPassthrough), envelope types (Cluster, NodePool), configuration mirror types, and per-field markers for write-mode, visibility, and feature gates. Adds platform-api codegen packages: field metadata registry (120 fields), feature gate registry (6 gates), and conversion helpers. Adds Makefile codegen pipeline (make codegen) and updates verify/deps targets. v1alpha1 internal CRD types are unchanged. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 3 + Makefile | 40 +- api/public/v2alpha1/cluster_types.go | 156 +++ api/public/v2alpha1/configuration.go | 219 +++++ api/public/v2alpha1/go.mod | 29 + api/public/v2alpha1/go.sum | 62 ++ api/public/v2alpha1/groupversion_info.go | 35 + .../v2alpha1/hostedclusterspec.passthrough.go | 210 ++++ api/public/v2alpha1/nodepool_types.go | 119 +++ api/public/v2alpha1/zz_generated.deepcopy.go | 908 ++++++++++++++++++ hack/api-codegen/go.mod | 4 +- hack/api-codegen/go.sum | 8 +- hack/api-codegen/pkg/markers/scanner.go | 8 +- platform-api/Containerfile | 2 + platform-api/go.mod | 6 +- platform-api/go.sum | 8 +- .../internal/codegen/conversion/cluster.go | 40 + .../internal/codegen/featuregate/registry.go | 53 + .../internal/codegen/featuregate/types.go | 53 + .../codegen/registry/field_metadata.go | 588 ++++++++++++ .../codegen/registry/field_metadata.json | 567 +++++++++++ 21 files changed, 3099 insertions(+), 19 deletions(-) create mode 100644 api/public/v2alpha1/cluster_types.go create mode 100644 api/public/v2alpha1/configuration.go create mode 100644 api/public/v2alpha1/go.mod create mode 100644 api/public/v2alpha1/go.sum create mode 100644 api/public/v2alpha1/groupversion_info.go create mode 100644 api/public/v2alpha1/hostedclusterspec.passthrough.go create mode 100644 api/public/v2alpha1/nodepool_types.go create mode 100644 api/public/v2alpha1/zz_generated.deepcopy.go create mode 100644 platform-api/internal/codegen/conversion/cluster.go create mode 100644 platform-api/internal/codegen/featuregate/registry.go create mode 100644 platform-api/internal/codegen/featuregate/types.go create mode 100644 platform-api/internal/codegen/registry/field_metadata.go create mode 100644 platform-api/internal/codegen/registry/field_metadata.json diff --git a/.gitignore b/.gitignore index 802359dd..ac28e79a 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ hyperfleet-operator/bin/ # Compiled operator binaries (built outside bin/) hyperfleet-operator/manager hyperfleet-operator/compactor + +# Codegen intermediate files +*.passthrough.go.raw diff --git a/Makefile b/Makefile index 2d1b379b..11ea26dc 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,9 @@ test-e2e test-e2e-api test-e2e-cli test-e2e-platform-monitoring test-e2e-zoa test-e2e-authz test-e2e-sdk \ e2e-authz-infra-up e2e-authz-infra-down e2e-init-db \ fmt vet verify deps \ - manifests generate generate-clientset verify-clientset setup-envtest \ + manifests generate generate-clientset verify-clientset \ + generate-public-deepcopy setup-envtest \ + codegen-passthrough codegen-registry codegen-verify codegen \ image-api image-operator image-push-api image-push-operator # ── Configuration ──────────────────────────────────────────────────────── @@ -106,9 +108,14 @@ help: @echo "" @echo "Code Generation:" @echo " manifests Generate CRD manifests" - @echo " generate Generate deepcopy methods" - @echo " generate-clientset Generate typed client SDK from CRD types" - @echo " verify-clientset Fail if generated clientset is out of date" + @echo " generate Generate deepcopy methods (v1alpha1)" + @echo " generate-clientset Generate typed client SDK from CRD types" + @echo " verify-clientset Fail if generated clientset is out of date" + @echo " generate-public-deepcopy Generate deepcopy methods (v2alpha1 public API)" + @echo " codegen Full codegen pipeline (passthrough → deepcopy → registry)" + @echo " codegen-passthrough Generate passthrough types from HyperShift" + @echo " codegen-registry Generate field metadata registry from markers" + @echo " codegen-verify Verify codegen outputs compile" @echo " setup-envtest Install envtest binaries (etcd, kube-apiserver)" @echo " deps Download and tidy all modules" @echo "" @@ -260,6 +267,7 @@ verify: cd hyperfleet-db && go mod tidy cd hyperfleet-operator/api && go mod tidy cd hyperfleet-operator && go mod tidy + cd api/public/v2alpha1 && go mod tidy cd platform-api && go mod tidy cd test && go mod tidy cd hack/tools && go mod tidy @@ -268,6 +276,7 @@ verify: hyperfleet-db/go.mod hyperfleet-db/go.sum \ hyperfleet-operator/api/go.mod hyperfleet-operator/api/go.sum \ hyperfleet-operator/go.mod hyperfleet-operator/go.sum \ + api/public/v2alpha1/go.mod api/public/v2alpha1/go.sum \ platform-api/go.mod platform-api/go.sum \ test/go.mod test/go.sum \ hack/tools/go.mod hack/tools/go.sum \ @@ -277,6 +286,7 @@ deps: cd hyperfleet-db && go mod download && go mod tidy cd hyperfleet-operator/api && go mod download && go mod tidy cd hyperfleet-operator && go mod download && go mod tidy + cd api/public/v2alpha1 && go mod download && go mod tidy cd platform-api && go mod download && go mod tidy cd test && go mod download && go mod tidy cd hack/api-codegen && go mod download && go mod tidy @@ -315,6 +325,28 @@ generate-clientset: $(CLIENT_GEN) $(WIRE_GEN) verify-clientset: generate-clientset git diff --exit-code clientset/ +codegen-passthrough: build-api-codegen + cd api/public/v2alpha1 && ../../../bin/passthrough-gen \ + -import-path github.com/openshift/hypershift/api/hypershift/v1beta1 \ + -types HostedClusterSpec,NodePoolSpec \ + -output-dir . \ + -package v2alpha1 + mv api/public/v2alpha1/zz_generated.passthrough.go api/public/v2alpha1/zz_generated.passthrough.go.raw + +generate-public-deepcopy: codegen-passthrough $(CONTROLLER_GEN) + $(CONTROLLER_GEN) object paths="./api/public/v2alpha1/..." + +codegen-registry: generate-public-deepcopy build-api-codegen + ./bin/marker-scanner \ + -input-dirs api/public/v2alpha1 \ + -output-file platform-api/internal/codegen/registry/field_metadata.go + +codegen-verify: codegen-registry + cd api/public/v2alpha1 && go build ./... + cd platform-api && go build ./internal/codegen/... + +codegen: codegen-verify + ENVTEST_BIN_DIR ?= $(shell pwd)/.envtest setup-envtest: $(SETUP_ENVTEST) diff --git a/api/public/v2alpha1/cluster_types.go b/api/public/v2alpha1/cluster_types.go new file mode 100644 index 00000000..20ae3855 --- /dev/null +++ b/api/public/v2alpha1/cluster_types.go @@ -0,0 +1,156 @@ +/* +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 v2alpha1 + +import ( + hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// ClusterPhase represents the lifecycle phase of a Cluster. +// +kubebuilder:validation:Enum=WaitingForPlacement;Provisioning;Ready;Deleting +type ClusterPhase string + +const ( + ClusterPhaseWaitingForPlacement ClusterPhase = "WaitingForPlacement" + ClusterPhaseProvisioning ClusterPhase = "Provisioning" + ClusterPhaseReady ClusterPhase = "Ready" + ClusterPhaseDeleting ClusterPhase = "Deleting" +) + +// ClusterSpec defines the desired state of a ROSA HCP cluster. +type ClusterSpec struct { + // DisplayName is a human-readable name for the cluster. + // +hyperfleet:write-mode=mutable + // +kubebuilder:validation:MaxLength=256 + // +optional + DisplayName string `json:"displayName,omitempty"` + + // DeleteProtection prevents accidental deletion when enabled. + // +hyperfleet:write-mode=mutable + // +optional + DeleteProtection *bool `json:"deleteProtection,omitempty"` + + // ExpirationTimestamp marks when this cluster should be automatically deleted. + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=mutable + // +optional + ExpirationTimestamp *metav1.Time `json:"expirationTimestamp,omitempty"` + + // Properties are arbitrary key-value pairs for customer metadata. + // +hyperfleet:write-mode=mutable + // +optional + Properties map[string]string `json:"properties,omitempty"` + + // Tags are customer-defined labels for organizational purposes. + // +hyperfleet:write-mode=mutable + // +openshift:enable:FeatureGate=HyperFleetAutoScaling + // +optional + Tags map[string]string `json:"tags,omitempty"` + + // AccountID identifies the customer account (platform-managed, hidden from API). + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +optional + AccountID string `json:"accountId,omitempty"` + + // CreatorARN is the IAM ARN of the user who created this cluster. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +optional + // +kubebuilder:validation:Pattern=`^arn:aws:` + CreatorARN string `json:"creatorARN,omitempty"` + + // InternalID is an internal platform identifier (platform-managed, hidden). + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +optional + InternalID string `json:"internalId,omitempty"` + + // HostedCluster is the full HyperShift HostedClusterSpec. + // +kubebuilder:validation:Required + HostedCluster hypershiftv1beta1.HostedClusterSpec `json:"hostedCluster"` +} + +// ClusterStatus defines the observed state of a Cluster. +type ClusterStatus struct { + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // +optional + Phase ClusterPhase `json:"phase,omitempty"` + + // +optional + ControlPlaneEndpoint hypershiftv1beta1.APIEndpoint `json:"controlPlaneEndpoint,omitempty"` + + // +optional + Version string `json:"version,omitempty"` + + // +optional + PlacementRef *PlacementReference `json:"placementRef,omitempty"` + + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +// PlacementReference identifies the management cluster assignment. +type PlacementReference struct { + Name string `json:"name"` + ManagementCluster string `json:"managementCluster"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=hfc +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=".status.phase" +// +kubebuilder:printcolumn:name="MC",type=string,JSONPath=".status.placementRef.managementCluster" +// +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=".status.controlPlaneEndpoint.host",priority=1 +// +kubebuilder:printcolumn:name="Expires",type=date,JSONPath=".spec.expirationTimestamp",priority=1 +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" + +// Cluster is the Schema for the clusters API. +type Cluster struct { + metav1.TypeMeta `json:",inline"` + + // +optional + metav1.ObjectMeta `json:"metadata,omitzero"` + + // +required + Spec ClusterSpec `json:"spec"` + + // +optional + Status ClusterStatus `json:"status,omitzero"` +} + +// +kubebuilder:object:root=true + +// ClusterList contains a list of Cluster. +type ClusterList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitzero"` + Items []Cluster `json:"items"` +} + +func init() { + SchemeBuilder.Register(func(s *runtime.Scheme) error { + s.AddKnownTypes(SchemeGroupVersion, &Cluster{}, &ClusterList{}) + return nil + }) +} diff --git a/api/public/v2alpha1/configuration.go b/api/public/v2alpha1/configuration.go new file mode 100644 index 00000000..c19d15de --- /dev/null +++ b/api/public/v2alpha1/configuration.go @@ -0,0 +1,219 @@ +package v2alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ClusterConfiguration specifies configuration for individual OCP components in the cluster. +// This is a HyperFleet-owned mirror of hypershiftv1beta1.ClusterConfiguration that allows +// us to add granular markers to nested fields like kubelet config. +type ClusterConfiguration struct { + // apiServer contains advanced network settings for the API server. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + APIServer *APIServerNetworkConfiguration `json:"apiServer,omitempty"` + + // authentication contains configuration for the cluster authentication. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Authentication *ClusterAuthentication `json:"authentication,omitempty"` + + // featureGate contains the desired configuration for feature gates. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + FeatureGate *FeatureGateConfiguration `json:"featureGate,omitempty"` + + // image contains the configuration for internal registry. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Image *ImageConfiguration `json:"image,omitempty"` + + // ingress contains the configuration for ingress. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Ingress *IngressConfiguration `json:"ingress,omitempty"` + + // network contains the configuration for cluster networking. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Network *NetworkConfiguration `json:"network,omitempty"` + + // oauth contains the configuration for OAuth. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + OAuth *OAuthConfiguration `json:"oauth,omitempty"` + + // scheduler contains the configuration for scheduler. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Scheduler *SchedulerConfiguration `json:"scheduler,omitempty"` + + // proxy contains the configuration for the cluster-wide proxy. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Proxy *ProxyConfiguration `json:"proxy,omitempty"` + + // kubelet contains the configuration for kubelet on nodes. + // +hyperfleet:write-mode=service-set + Kubelet *KubeletConfig `json:"kubelet,omitempty"` + + // machineConfig contains the configuration for machine-level settings. + // +hyperfleet:write-mode=service-set + MachineConfig *MachineConfigSpec `json:"machineConfig,omitempty"` +} + +// KubeletConfig specifies kubelet configuration with granular markers for customer control. +type KubeletConfig struct { + // +hyperfleet:write-mode=mutable + MaxPods *int32 `json:"maxPods,omitempty"` + + // +hyperfleet:write-mode=mutable + PodPidsLimit *int64 `json:"podPidsLimit,omitempty"` + + // +hyperfleet:write-mode=immutable + SystemReserved map[string]string `json:"systemReserved,omitempty"` + + // +hyperfleet:write-mode=immutable + KubeReserved map[string]string `json:"kubeReserved,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + EvictionHard map[string]string `json:"evictionHard,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + EvictionSoft map[string]string `json:"evictionSoft,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + EvictionSoftGracePeriod map[string]string `json:"evictionSoftGracePeriod,omitempty"` + + // +hyperfleet:write-mode=mutable + ImageGCHighThresholdPercent *int32 `json:"imageGCHighThresholdPercent,omitempty"` + + // +hyperfleet:write-mode=mutable + ImageGCLowThresholdPercent *int32 `json:"imageGCLowThresholdPercent,omitempty"` + + // +hyperfleet:write-mode=mutable + ImageMinimumGCAge *metav1.Duration `json:"imageMinimumGCAge,omitempty"` + + // +openshift:enable:FeatureGate=HyperFleetKubeletAdvanced + // +hyperfleet:write-mode=mutable + SerializeImagePulls *bool `json:"serializeImagePulls,omitempty"` + + // +openshift:enable:FeatureGate=HyperFleetKubeletAdvanced + // +hyperfleet:write-mode=mutable + RegistryPullQPS *int32 `json:"registryPullQPS,omitempty"` + + // +openshift:enable:FeatureGate=HyperFleetKubeletAdvanced + // +hyperfleet:write-mode=mutable + RegistryBurst *int32 `json:"registryBurst,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + CPUManagerPolicy *string `json:"cpuManagerPolicy,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + CPUManagerPolicyOptions map[string]string `json:"cpuManagerPolicyOptions,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + CPUManagerReconcilePeriod *metav1.Duration `json:"cpuManagerReconcilePeriod,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + TopologyManagerPolicy *string `json:"topologyManagerPolicy,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + TopologyManagerScope *string `json:"topologyManagerScope,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + AllowedUnsafeSysctls []string `json:"allowedUnsafeSysctls,omitempty"` + + // +hyperfleet:write-mode=mutable + StreamingConnectionIdleTimeout *metav1.Duration `json:"streamingConnectionIdleTimeout,omitempty"` + + // +hyperfleet:write-mode=mutable + ContainerLogMaxSize *string `json:"containerLogMaxSize,omitempty"` + + // +hyperfleet:write-mode=mutable + ContainerLogMaxFiles *int32 `json:"containerLogMaxFiles,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + MemoryThrottlingFactor *float64 `json:"memoryThrottlingFactor,omitempty"` +} + +// Placeholder types for configuration areas not yet exposed. + +type APIServerNetworkConfiguration struct{} + +type ClusterAuthentication struct{} + +type FeatureGateConfiguration struct{} + +type ImageConfiguration struct{} + +type IngressConfiguration struct{} + +type NetworkConfiguration struct{} + +type OAuthConfiguration struct{} + +type SchedulerConfiguration struct{} + +type ProxyConfiguration struct{} + +// MachineConfigSpec specifies machine-level configuration. +type MachineConfigSpec struct { + // +openshift:enable:FeatureGate=HyperFleetMachineConfig + // +hyperfleet:write-mode=immutable + AllowedKernelArguments []string `json:"allowedKernelArguments,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + KernelArguments []string `json:"kernelArguments,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + SystemdUnits []SystemdUnit `json:"systemdUnits,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Files []FileSpec `json:"files,omitempty"` + + // +hyperfleet:write-mode=immutable + FIPS *bool `json:"fips,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + KernelType *string `json:"kernelType,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Extensions []string `json:"extensions,omitempty"` +} + +type SystemdUnit struct { + Name string `json:"name"` + Enabled *bool `json:"enabled,omitempty"` + Contents string `json:"contents,omitempty"` + Dropins []SystemdDropin `json:"dropins,omitempty"` +} + +type SystemdDropin struct { + Name string `json:"name"` + Contents string `json:"contents,omitempty"` +} + +type FileSpec struct { + Path string `json:"path"` + Contents string `json:"contents,omitempty"` + Mode *int32 `json:"mode,omitempty"` + User *string `json:"user,omitempty"` + Group *string `json:"group,omitempty"` + Overwrite *bool `json:"overwrite,omitempty"` +} diff --git a/api/public/v2alpha1/go.mod b/api/public/v2alpha1/go.mod new file mode 100644 index 00000000..f5524ec1 --- /dev/null +++ b/api/public/v2alpha1/go.mod @@ -0,0 +1,29 @@ +module github.com/openshift-online/rosa-hyperfleet-api/api/public/v2alpha1 + +go 1.26.3 + +require ( + github.com/openshift/api v0.0.0-20260416105050-3c6b218b8a80 + github.com/openshift/hypershift/api v0.0.0-20260625052409-9acec4759a16 + k8s.io/api v0.36.0 + k8s.io/apimachinery v0.36.0 +) + +require ( + github.com/fxamacker/cbor/v2 v2.9.2 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/text v0.38.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect + k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect +) diff --git a/api/public/v2alpha1/go.sum b/api/public/v2alpha1/go.sum new file mode 100644 index 00000000..7fd506bd --- /dev/null +++ b/api/public/v2alpha1/go.sum @@ -0,0 +1,62 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= +github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/openshift/api v0.0.0-20260416105050-3c6b218b8a80 h1:r0S/yoZAI0iWo1JvoIijaIgWGWf/izg4WiV7Wrtz16k= +github.com/openshift/api v0.0.0-20260416105050-3c6b218b8a80/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo= +github.com/openshift/hypershift/api v0.0.0-20260625052409-9acec4759a16 h1:QDSh3vkKYq7Fn9utYGlAJadkTdyaRl9IY7Cr6cNDAow= +github.com/openshift/hypershift/api v0.0.0-20260625052409-9acec4759a16/go.mod h1:Z3lkj5pFqY+KTl3Do9gXdEZdKWLnkUTSDShLD1HE0CM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80= +k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34= +k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ= +k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 h1:jVkFFVfXdXP74B/zbO3hM3hpSFD0xvhQ5U686DPurkE= +k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3/go.mod h1:M2s5JB1lIYP3jzZdorPLHXIPJzt9vv2muW5a6L9DtNM= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/api/public/v2alpha1/groupversion_info.go b/api/public/v2alpha1/groupversion_info.go new file mode 100644 index 00000000..2ca03a9a --- /dev/null +++ b/api/public/v2alpha1/groupversion_info.go @@ -0,0 +1,35 @@ +/* +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. +*/ + +// +kubebuilder:object:generate=true +// +groupName=hyperfleet.io +package v2alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + SchemeGroupVersion = schema.GroupVersion{Group: "hyperfleet.io", Version: "v2alpha1"} + GroupVersion = SchemeGroupVersion + SchemeBuilder = runtime.NewSchemeBuilder(func(s *runtime.Scheme) error { + metav1.AddToGroupVersion(s, SchemeGroupVersion) + return nil + }) + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/public/v2alpha1/hostedclusterspec.passthrough.go b/api/public/v2alpha1/hostedclusterspec.passthrough.go new file mode 100644 index 00000000..e1d5dc6d --- /dev/null +++ b/api/public/v2alpha1/hostedclusterspec.passthrough.go @@ -0,0 +1,210 @@ +// Code generated by passthrough-gen. DO NOT EDIT. + +package v2alpha1 + +import ( + configv1 "github.com/openshift/api/config/v1" + hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// HostedClusterSpecPassthrough mirrors HostedClusterSpec from upstream HyperShift +type HostedClusterSpecPassthrough struct { + // release specifies the desired OCP release payload for all the hosted cluster components. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Release hypershiftv1beta1.Release `json:"release"` + // controlPlaneRelease is like spec.release but only for the components running on the management cluster. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + ControlPlaneRelease *hypershiftv1beta1.Release `json:"controlPlaneRelease,omitempty"` + // clusterID uniquely identifies this cluster. This is expected to be an RFC4122 UUID value (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx in hexadecimal digits). + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + ClusterID string `json:"clusterID,omitempty"` + // infraID is a globally unique identifier for the cluster. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + InfraID string `json:"infraID,omitempty"` + // updateService may be used to specify the preferred upstream update service. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + UpdateService configv1.URL `json:"updateService,omitempty"` + // channel is an identifier for explicitly requesting that a non-default set of updates be applied to this cluster. + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=service-set + Channel string `json:"channel,omitempty"` + // platform specifies the underlying infrastructure provider for the cluster + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Platform hypershiftv1beta1.PlatformSpec `json:"platform"` + // kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + KubeAPIServerDNSName string `json:"kubeAPIServerDNSName,omitempty"` + // controllerAvailabilityPolicy specifies the availability policy applied to critical control plane components like the Kube API Server. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + ControllerAvailabilityPolicy hypershiftv1beta1.AvailabilityPolicy `json:"controllerAvailabilityPolicy,omitempty"` + // infrastructureAvailabilityPolicy specifies the availability policy applied to infrastructure services which run on the hosted cluster data plane like the ingress controller and image registry controller. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + InfrastructureAvailabilityPolicy hypershiftv1beta1.AvailabilityPolicy `json:"infrastructureAvailabilityPolicy,omitempty"` + // dns specifies the DNS configuration for the hosted cluster ingress. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + DNS hypershiftv1beta1.DNSSpec `json:"dns,omitempty"` + // networking specifies network configuration for the hosted cluster. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Networking hypershiftv1beta1.ClusterNetworking `json:"networking"` + // autoscaling specifies auto-scaling behavior that applies to all NodePools + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Autoscaling hypershiftv1beta1.ClusterAutoscaling `json:"autoscaling,omitempty"` + // autoNode specifies the configuration for automatic node provisioning and lifecycle management. + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=service-set + AutoNode hypershiftv1beta1.AutoNode `json:"autoNode,omitzero"` + // etcd specifies configuration for the control plane etcd cluster. The + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Etcd hypershiftv1beta1.EtcdSpec `json:"etcd"` + // services specifies how individual control plane services endpoints are published for consumption. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Services []hypershiftv1beta1.ServicePublishingStrategyMapping `json:"services"` + // pullSecret is a local reference to a Secret that must have a ".dockerconfigjson" key whose content must be a valid Openshift pull secret JSON. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + PullSecret corev1.LocalObjectReference `json:"pullSecret"` + // sshKey is a local reference to a Secret that must have a "id_rsa.pub" key whose content must be the public part of 1..N SSH keys. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + SSHKey corev1.LocalObjectReference `json:"sshKey"` + // issuerURL is an OIDC issuer URL which will be used as the issuer in all + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + IssuerURL string `json:"issuerURL,omitempty"` + // serviceAccountSigningKey is a local reference to a secret that must have a "key" key whose content must be the private key + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + ServiceAccountSigningKey *corev1.LocalObjectReference `json:"serviceAccountSigningKey,omitempty"` + // configuration specifies configuration for individual OCP components in the + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=service-set + Configuration *hypershiftv1beta1.ClusterConfiguration `json:"configuration,omitempty"` + // operatorConfiguration specifies configuration for individual OCP operators in the cluster. + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=service-set + OperatorConfiguration *hypershiftv1beta1.OperatorConfiguration `json:"operatorConfiguration,omitempty"` + // auditWebhook contains metadata for configuring an audit webhook endpoint + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + AuditWebhook *corev1.LocalObjectReference `json:"auditWebhook,omitempty"` + // imageContentSources specifies image mirrors that can be used by cluster + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + ImageContentSources []hypershiftv1beta1.ImageContentSource `json:"imageContentSources,omitempty"` + // additionalTrustBundle is a local reference to a ConfigMap that must have a "ca-bundle.crt" key + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + AdditionalTrustBundle *corev1.LocalObjectReference `json:"additionalTrustBundle,omitempty"` + // secretEncryption specifies a Kubernetes secret encryption strategy for the + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + SecretEncryption *hypershiftv1beta1.SecretEncryptionSpec `json:"secretEncryption,omitempty"` + // fips indicates whether this cluster's nodes will be running in FIPS mode. + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=service-set + FIPS bool `json:"fips"` + // pausedUntil is a field that can be used to pause reconciliation on the HostedCluster controller, resulting in any change to the HostedCluster being ignored. + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=service-set + PausedUntil *string `json:"pausedUntil,omitempty"` + // olmCatalogPlacement specifies the placement of OLM catalog components. By default, + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + OLMCatalogPlacement hypershiftv1beta1.OLMCatalogPlacement `json:"olmCatalogPlacement,omitempty"` + // nodeSelector when specified, is propagated to all control plane Deployments and Stateful sets running management side. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + // tolerations when specified, define what custom tolerations are added to the hcp pods. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + // labels when specified, define what custom labels are added to the hcp pods. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Labels map[string]string `json:"labels,omitempty"` + // capabilities allows for disabling optional components at cluster install time. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Capabilities *hypershiftv1beta1.Capabilities `json:"capabilities,omitempty"` +} + +// NodePoolSpecPassthrough mirrors NodePoolSpec from upstream HyperShift +type NodePoolSpecPassthrough struct { + // clusterName is the name of the HostedCluster this NodePool belongs to. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + ClusterName string `json:"clusterName"` + // release specifies the OCP release used for this NodePool. It drives the machine ignition configuration (including + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Release hypershiftv1beta1.Release `json:"release"` + // platform specifies the underlying infrastructure provider for the NodePool + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Platform hypershiftv1beta1.NodePoolPlatform `json:"platform"` + // replicas is the desired number of nodes the pool should maintain. If unset, the controller default value is 0. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Replicas *int32 `json:"replicas,omitempty"` + // management specifies behavior for managing nodes in the pool, such as + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Management hypershiftv1beta1.NodePoolManagement `json:"management"` + // autoScaling specifies auto-scaling behavior for the NodePool. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + AutoScaling *hypershiftv1beta1.NodePoolAutoScaling `json:"autoScaling,omitempty"` + // config is a list of references to ConfigMaps containing serialized + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Config []corev1.LocalObjectReference `json:"config,omitempty"` + // nodeDrainTimeout is the maximum amount of time that the controller will spend on retrying to drain a node until it succeeds. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + NodeDrainTimeout *metav1.Duration `json:"nodeDrainTimeout,omitempty"` + // nodeVolumeDetachTimeout is the maximum amount of time that the controller will spend on detaching volumes from a node. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + NodeVolumeDetachTimeout *metav1.Duration `json:"nodeVolumeDetachTimeout,omitempty"` + // nodeLabels propagates a list of labels to Nodes, only once on creation. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + NodeLabels map[string]string `json:"nodeLabels,omitempty"` + // taints if specified, propagates a list of taints to Nodes, only once on creation. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Taints []hypershiftv1beta1.Taint `json:"taints,omitempty"` + // pausedUntil is a field that can be used to pause reconciliation on the NodePool controller. Resulting in any change to the NodePool being ignored. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + PausedUntil *string `json:"pausedUntil,omitempty"` + // tuningConfig is a list of references to ConfigMaps containing serialized + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + TuningConfig []corev1.LocalObjectReference `json:"tuningConfig,omitempty"` + // arch is the preferred processor architecture for the NodePool. Different platforms might have different supported architectures. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Arch string `json:"arch,omitempty"` + // osImageStream specifies an OS stream to be used for nodes in this pool. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + OSImageStream hypershiftv1beta1.OSImageStreamReference `json:"osImageStream,omitzero"` +} diff --git a/api/public/v2alpha1/nodepool_types.go b/api/public/v2alpha1/nodepool_types.go new file mode 100644 index 00000000..6b7a17f4 --- /dev/null +++ b/api/public/v2alpha1/nodepool_types.go @@ -0,0 +1,119 @@ +/* +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 v2alpha1 + +import ( + hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// NodePoolPhase represents the lifecycle phase of a NodePool. +// +kubebuilder:validation:Enum=WaitingForCluster;Provisioning;Ready;Deleting +type NodePoolPhase string + +const ( + NodePoolPhaseWaitingForCluster NodePoolPhase = "WaitingForCluster" + NodePoolPhaseProvisioning NodePoolPhase = "Provisioning" + NodePoolPhaseReady NodePoolPhase = "Ready" + NodePoolPhaseDeleting NodePoolPhase = "Deleting" +) + +// NodePoolSpec defines the desired state of a NodePool. +type NodePoolSpec struct { + // DisplayName is a human-readable name for the node pool. + // +hyperfleet:write-mode=mutable + // +kubebuilder:validation:MaxLength=256 + // +optional + DisplayName string `json:"displayName,omitempty"` + + // AutoRepair enables automatic repair of unhealthy nodes. + // +hyperfleet:write-mode=mutable + // +optional + AutoRepair *bool `json:"autoRepair,omitempty"` + + // Labels are customer-defined labels applied to nodes. + // +hyperfleet:write-mode=mutable + // +optional + Labels map[string]string `json:"labels,omitempty"` + + // AccountID identifies the customer account (platform-managed, hidden from API). + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +optional + AccountID string `json:"accountId,omitempty"` + + // InternalPoolID is an internal platform identifier (platform-managed, hidden). + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +optional + InternalPoolID string `json:"internalPoolId,omitempty"` + + // NodePool is the full HyperShift NodePoolSpec. + // +kubebuilder:validation:Required + NodePool hypershiftv1beta1.NodePoolSpec `json:"nodePool"` +} + +// NodePoolStatus defines the observed state of a NodePool. +type NodePoolStatus struct { + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // +optional + Phase NodePoolPhase `json:"phase,omitempty"` + + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=hfnp +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=".status.phase" +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" + +// NodePool is the Schema for the nodepools API. +type NodePool struct { + metav1.TypeMeta `json:",inline"` + + // +optional + metav1.ObjectMeta `json:"metadata,omitzero"` + + // +required + Spec NodePoolSpec `json:"spec"` + + // +optional + Status NodePoolStatus `json:"status,omitzero"` +} + +// +kubebuilder:object:root=true + +// NodePoolList contains a list of NodePool. +type NodePoolList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitzero"` + Items []NodePool `json:"items"` +} + +func init() { + SchemeBuilder.Register(func(s *runtime.Scheme) error { + s.AddKnownTypes(SchemeGroupVersion, &NodePool{}, &NodePoolList{}) + return nil + }) +} diff --git a/api/public/v2alpha1/zz_generated.deepcopy.go b/api/public/v2alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..41f8e35d --- /dev/null +++ b/api/public/v2alpha1/zz_generated.deepcopy.go @@ -0,0 +1,908 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v2alpha1 + +import ( + "github.com/openshift/hypershift/api/hypershift/v1beta1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServerNetworkConfiguration) DeepCopyInto(out *APIServerNetworkConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServerNetworkConfiguration. +func (in *APIServerNetworkConfiguration) DeepCopy() *APIServerNetworkConfiguration { + if in == nil { + return nil + } + out := new(APIServerNetworkConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Cluster) DeepCopyInto(out *Cluster) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Cluster. +func (in *Cluster) DeepCopy() *Cluster { + if in == nil { + return nil + } + out := new(Cluster) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Cluster) 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 *ClusterAuthentication) DeepCopyInto(out *ClusterAuthentication) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterAuthentication. +func (in *ClusterAuthentication) DeepCopy() *ClusterAuthentication { + if in == nil { + return nil + } + out := new(ClusterAuthentication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterConfiguration) DeepCopyInto(out *ClusterConfiguration) { + *out = *in + if in.APIServer != nil { + in, out := &in.APIServer, &out.APIServer + *out = new(APIServerNetworkConfiguration) + **out = **in + } + if in.Authentication != nil { + in, out := &in.Authentication, &out.Authentication + *out = new(ClusterAuthentication) + **out = **in + } + if in.FeatureGate != nil { + in, out := &in.FeatureGate, &out.FeatureGate + *out = new(FeatureGateConfiguration) + **out = **in + } + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(ImageConfiguration) + **out = **in + } + if in.Ingress != nil { + in, out := &in.Ingress, &out.Ingress + *out = new(IngressConfiguration) + **out = **in + } + if in.Network != nil { + in, out := &in.Network, &out.Network + *out = new(NetworkConfiguration) + **out = **in + } + if in.OAuth != nil { + in, out := &in.OAuth, &out.OAuth + *out = new(OAuthConfiguration) + **out = **in + } + if in.Scheduler != nil { + in, out := &in.Scheduler, &out.Scheduler + *out = new(SchedulerConfiguration) + **out = **in + } + if in.Proxy != nil { + in, out := &in.Proxy, &out.Proxy + *out = new(ProxyConfiguration) + **out = **in + } + if in.Kubelet != nil { + in, out := &in.Kubelet, &out.Kubelet + *out = new(KubeletConfig) + (*in).DeepCopyInto(*out) + } + if in.MachineConfig != nil { + in, out := &in.MachineConfig, &out.MachineConfig + *out = new(MachineConfigSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterConfiguration. +func (in *ClusterConfiguration) DeepCopy() *ClusterConfiguration { + if in == nil { + return nil + } + out := new(ClusterConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterList) DeepCopyInto(out *ClusterList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Cluster, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterList. +func (in *ClusterList) DeepCopy() *ClusterList { + if in == nil { + return nil + } + out := new(ClusterList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterList) 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 *ClusterSpec) DeepCopyInto(out *ClusterSpec) { + *out = *in + if in.DeleteProtection != nil { + in, out := &in.DeleteProtection, &out.DeleteProtection + *out = new(bool) + **out = **in + } + if in.ExpirationTimestamp != nil { + in, out := &in.ExpirationTimestamp, &out.ExpirationTimestamp + *out = (*in).DeepCopy() + } + if in.Properties != nil { + in, out := &in.Properties, &out.Properties + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.HostedCluster.DeepCopyInto(&out.HostedCluster) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSpec. +func (in *ClusterSpec) DeepCopy() *ClusterSpec { + if in == nil { + return nil + } + out := new(ClusterSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterStatus) DeepCopyInto(out *ClusterStatus) { + *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]) + } + } + out.ControlPlaneEndpoint = in.ControlPlaneEndpoint + if in.PlacementRef != nil { + in, out := &in.PlacementRef, &out.PlacementRef + *out = new(PlacementReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterStatus. +func (in *ClusterStatus) DeepCopy() *ClusterStatus { + if in == nil { + return nil + } + out := new(ClusterStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureGateConfiguration) DeepCopyInto(out *FeatureGateConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureGateConfiguration. +func (in *FeatureGateConfiguration) DeepCopy() *FeatureGateConfiguration { + if in == nil { + return nil + } + out := new(FeatureGateConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSpec) DeepCopyInto(out *FileSpec) { + *out = *in + if in.Mode != nil { + in, out := &in.Mode, &out.Mode + *out = new(int32) + **out = **in + } + if in.User != nil { + in, out := &in.User, &out.User + *out = new(string) + **out = **in + } + if in.Group != nil { + in, out := &in.Group, &out.Group + *out = new(string) + **out = **in + } + if in.Overwrite != nil { + in, out := &in.Overwrite, &out.Overwrite + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSpec. +func (in *FileSpec) DeepCopy() *FileSpec { + if in == nil { + return nil + } + out := new(FileSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostedClusterSpecPassthrough) DeepCopyInto(out *HostedClusterSpecPassthrough) { + *out = *in + out.Release = in.Release + if in.ControlPlaneRelease != nil { + in, out := &in.ControlPlaneRelease, &out.ControlPlaneRelease + *out = new(v1beta1.Release) + **out = **in + } + in.Platform.DeepCopyInto(&out.Platform) + in.DNS.DeepCopyInto(&out.DNS) + in.Networking.DeepCopyInto(&out.Networking) + in.Autoscaling.DeepCopyInto(&out.Autoscaling) + out.AutoNode = in.AutoNode + in.Etcd.DeepCopyInto(&out.Etcd) + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = make([]v1beta1.ServicePublishingStrategyMapping, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.PullSecret = in.PullSecret + out.SSHKey = in.SSHKey + if in.ServiceAccountSigningKey != nil { + in, out := &in.ServiceAccountSigningKey, &out.ServiceAccountSigningKey + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.Configuration != nil { + in, out := &in.Configuration, &out.Configuration + *out = new(v1beta1.ClusterConfiguration) + (*in).DeepCopyInto(*out) + } + if in.OperatorConfiguration != nil { + in, out := &in.OperatorConfiguration, &out.OperatorConfiguration + *out = new(v1beta1.OperatorConfiguration) + (*in).DeepCopyInto(*out) + } + if in.AuditWebhook != nil { + in, out := &in.AuditWebhook, &out.AuditWebhook + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.ImageContentSources != nil { + in, out := &in.ImageContentSources, &out.ImageContentSources + *out = make([]v1beta1.ImageContentSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.AdditionalTrustBundle != nil { + in, out := &in.AdditionalTrustBundle, &out.AdditionalTrustBundle + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.SecretEncryption != nil { + in, out := &in.SecretEncryption, &out.SecretEncryption + *out = new(v1beta1.SecretEncryptionSpec) + (*in).DeepCopyInto(*out) + } + if in.PausedUntil != nil { + in, out := &in.PausedUntil, &out.PausedUntil + *out = new(string) + **out = **in + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]corev1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Capabilities != nil { + in, out := &in.Capabilities, &out.Capabilities + *out = new(v1beta1.Capabilities) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostedClusterSpecPassthrough. +func (in *HostedClusterSpecPassthrough) DeepCopy() *HostedClusterSpecPassthrough { + if in == nil { + return nil + } + out := new(HostedClusterSpecPassthrough) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImageConfiguration) DeepCopyInto(out *ImageConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageConfiguration. +func (in *ImageConfiguration) DeepCopy() *ImageConfiguration { + if in == nil { + return nil + } + out := new(ImageConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressConfiguration) DeepCopyInto(out *IngressConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressConfiguration. +func (in *IngressConfiguration) DeepCopy() *IngressConfiguration { + if in == nil { + return nil + } + out := new(IngressConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeletConfig) DeepCopyInto(out *KubeletConfig) { + *out = *in + if in.MaxPods != nil { + in, out := &in.MaxPods, &out.MaxPods + *out = new(int32) + **out = **in + } + if in.PodPidsLimit != nil { + in, out := &in.PodPidsLimit, &out.PodPidsLimit + *out = new(int64) + **out = **in + } + if in.SystemReserved != nil { + in, out := &in.SystemReserved, &out.SystemReserved + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.KubeReserved != nil { + in, out := &in.KubeReserved, &out.KubeReserved + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.EvictionHard != nil { + in, out := &in.EvictionHard, &out.EvictionHard + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.EvictionSoft != nil { + in, out := &in.EvictionSoft, &out.EvictionSoft + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.EvictionSoftGracePeriod != nil { + in, out := &in.EvictionSoftGracePeriod, &out.EvictionSoftGracePeriod + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ImageGCHighThresholdPercent != nil { + in, out := &in.ImageGCHighThresholdPercent, &out.ImageGCHighThresholdPercent + *out = new(int32) + **out = **in + } + if in.ImageGCLowThresholdPercent != nil { + in, out := &in.ImageGCLowThresholdPercent, &out.ImageGCLowThresholdPercent + *out = new(int32) + **out = **in + } + if in.ImageMinimumGCAge != nil { + in, out := &in.ImageMinimumGCAge, &out.ImageMinimumGCAge + *out = new(v1.Duration) + **out = **in + } + if in.SerializeImagePulls != nil { + in, out := &in.SerializeImagePulls, &out.SerializeImagePulls + *out = new(bool) + **out = **in + } + if in.RegistryPullQPS != nil { + in, out := &in.RegistryPullQPS, &out.RegistryPullQPS + *out = new(int32) + **out = **in + } + if in.RegistryBurst != nil { + in, out := &in.RegistryBurst, &out.RegistryBurst + *out = new(int32) + **out = **in + } + if in.CPUManagerPolicy != nil { + in, out := &in.CPUManagerPolicy, &out.CPUManagerPolicy + *out = new(string) + **out = **in + } + if in.CPUManagerPolicyOptions != nil { + in, out := &in.CPUManagerPolicyOptions, &out.CPUManagerPolicyOptions + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.CPUManagerReconcilePeriod != nil { + in, out := &in.CPUManagerReconcilePeriod, &out.CPUManagerReconcilePeriod + *out = new(v1.Duration) + **out = **in + } + if in.TopologyManagerPolicy != nil { + in, out := &in.TopologyManagerPolicy, &out.TopologyManagerPolicy + *out = new(string) + **out = **in + } + if in.TopologyManagerScope != nil { + in, out := &in.TopologyManagerScope, &out.TopologyManagerScope + *out = new(string) + **out = **in + } + if in.AllowedUnsafeSysctls != nil { + in, out := &in.AllowedUnsafeSysctls, &out.AllowedUnsafeSysctls + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.StreamingConnectionIdleTimeout != nil { + in, out := &in.StreamingConnectionIdleTimeout, &out.StreamingConnectionIdleTimeout + *out = new(v1.Duration) + **out = **in + } + if in.ContainerLogMaxSize != nil { + in, out := &in.ContainerLogMaxSize, &out.ContainerLogMaxSize + *out = new(string) + **out = **in + } + if in.ContainerLogMaxFiles != nil { + in, out := &in.ContainerLogMaxFiles, &out.ContainerLogMaxFiles + *out = new(int32) + **out = **in + } + if in.MemoryThrottlingFactor != nil { + in, out := &in.MemoryThrottlingFactor, &out.MemoryThrottlingFactor + *out = new(float64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeletConfig. +func (in *KubeletConfig) DeepCopy() *KubeletConfig { + if in == nil { + return nil + } + out := new(KubeletConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineConfigSpec) DeepCopyInto(out *MachineConfigSpec) { + *out = *in + if in.AllowedKernelArguments != nil { + in, out := &in.AllowedKernelArguments, &out.AllowedKernelArguments + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.KernelArguments != nil { + in, out := &in.KernelArguments, &out.KernelArguments + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.SystemdUnits != nil { + in, out := &in.SystemdUnits, &out.SystemdUnits + *out = make([]SystemdUnit, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = make([]FileSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.FIPS != nil { + in, out := &in.FIPS, &out.FIPS + *out = new(bool) + **out = **in + } + if in.KernelType != nil { + in, out := &in.KernelType, &out.KernelType + *out = new(string) + **out = **in + } + if in.Extensions != nil { + in, out := &in.Extensions, &out.Extensions + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineConfigSpec. +func (in *MachineConfigSpec) DeepCopy() *MachineConfigSpec { + if in == nil { + return nil + } + out := new(MachineConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkConfiguration) DeepCopyInto(out *NetworkConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkConfiguration. +func (in *NetworkConfiguration) DeepCopy() *NetworkConfiguration { + if in == nil { + return nil + } + out := new(NetworkConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodePool) DeepCopyInto(out *NodePool) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodePool. +func (in *NodePool) DeepCopy() *NodePool { + if in == nil { + return nil + } + out := new(NodePool) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NodePool) 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 *NodePoolList) DeepCopyInto(out *NodePoolList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NodePool, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodePoolList. +func (in *NodePoolList) DeepCopy() *NodePoolList { + if in == nil { + return nil + } + out := new(NodePoolList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NodePoolList) 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 *NodePoolSpec) DeepCopyInto(out *NodePoolSpec) { + *out = *in + if in.AutoRepair != nil { + in, out := &in.AutoRepair, &out.AutoRepair + *out = new(bool) + **out = **in + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.NodePool.DeepCopyInto(&out.NodePool) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodePoolSpec. +func (in *NodePoolSpec) DeepCopy() *NodePoolSpec { + if in == nil { + return nil + } + out := new(NodePoolSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodePoolSpecPassthrough) DeepCopyInto(out *NodePoolSpecPassthrough) { + *out = *in + out.Release = in.Release + in.Platform.DeepCopyInto(&out.Platform) + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + in.Management.DeepCopyInto(&out.Management) + if in.AutoScaling != nil { + in, out := &in.AutoScaling, &out.AutoScaling + *out = new(v1beta1.NodePoolAutoScaling) + (*in).DeepCopyInto(*out) + } + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = make([]corev1.LocalObjectReference, len(*in)) + copy(*out, *in) + } + if in.NodeDrainTimeout != nil { + in, out := &in.NodeDrainTimeout, &out.NodeDrainTimeout + *out = new(v1.Duration) + **out = **in + } + if in.NodeVolumeDetachTimeout != nil { + in, out := &in.NodeVolumeDetachTimeout, &out.NodeVolumeDetachTimeout + *out = new(v1.Duration) + **out = **in + } + if in.NodeLabels != nil { + in, out := &in.NodeLabels, &out.NodeLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Taints != nil { + in, out := &in.Taints, &out.Taints + *out = make([]v1beta1.Taint, len(*in)) + copy(*out, *in) + } + if in.PausedUntil != nil { + in, out := &in.PausedUntil, &out.PausedUntil + *out = new(string) + **out = **in + } + if in.TuningConfig != nil { + in, out := &in.TuningConfig, &out.TuningConfig + *out = make([]corev1.LocalObjectReference, len(*in)) + copy(*out, *in) + } + out.OSImageStream = in.OSImageStream +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodePoolSpecPassthrough. +func (in *NodePoolSpecPassthrough) DeepCopy() *NodePoolSpecPassthrough { + if in == nil { + return nil + } + out := new(NodePoolSpecPassthrough) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodePoolStatus) DeepCopyInto(out *NodePoolStatus) { + *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]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodePoolStatus. +func (in *NodePoolStatus) DeepCopy() *NodePoolStatus { + if in == nil { + return nil + } + out := new(NodePoolStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OAuthConfiguration) DeepCopyInto(out *OAuthConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OAuthConfiguration. +func (in *OAuthConfiguration) DeepCopy() *OAuthConfiguration { + if in == nil { + return nil + } + out := new(OAuthConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlacementReference) DeepCopyInto(out *PlacementReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlacementReference. +func (in *PlacementReference) DeepCopy() *PlacementReference { + if in == nil { + return nil + } + out := new(PlacementReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProxyConfiguration) DeepCopyInto(out *ProxyConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyConfiguration. +func (in *ProxyConfiguration) DeepCopy() *ProxyConfiguration { + if in == nil { + return nil + } + out := new(ProxyConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SchedulerConfiguration) DeepCopyInto(out *SchedulerConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SchedulerConfiguration. +func (in *SchedulerConfiguration) DeepCopy() *SchedulerConfiguration { + if in == nil { + return nil + } + out := new(SchedulerConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SystemdDropin) DeepCopyInto(out *SystemdDropin) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SystemdDropin. +func (in *SystemdDropin) DeepCopy() *SystemdDropin { + if in == nil { + return nil + } + out := new(SystemdDropin) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SystemdUnit) DeepCopyInto(out *SystemdUnit) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.Dropins != nil { + in, out := &in.Dropins, &out.Dropins + *out = make([]SystemdDropin, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SystemdUnit. +func (in *SystemdUnit) DeepCopy() *SystemdUnit { + if in == nil { + return nil + } + out := new(SystemdUnit) + in.DeepCopyInto(out) + return out +} diff --git a/hack/api-codegen/go.mod b/hack/api-codegen/go.mod index aab7dd67..9a0c623b 100644 --- a/hack/api-codegen/go.mod +++ b/hack/api-codegen/go.mod @@ -3,7 +3,7 @@ module github.com/openshift-online/rosa-hyperfleet-api/hack/api-codegen go 1.26.3 require ( - github.com/openshift/hypershift/api v0.0.0-20251113065312-f919037748bf + github.com/openshift/hypershift/api v0.0.0-20260625052409-9acec4759a16 gopkg.in/yaml.v3 v3.0.1 k8s.io/apiextensions-apiserver v0.36.0 sigs.k8s.io/controller-tools v0.21.0 @@ -17,7 +17,7 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect - github.com/openshift/api v0.0.0-20250609083529-2b129d95495e // indirect + github.com/openshift/api v0.0.0-20260416105050-3c6b218b8a80 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect golang.org/x/mod v0.38.0 // indirect diff --git a/hack/api-codegen/go.sum b/hack/api-codegen/go.sum index 55a0e604..119aa6f8 100644 --- a/hack/api-codegen/go.sum +++ b/hack/api-codegen/go.sum @@ -88,10 +88,10 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= -github.com/openshift/api v0.0.0-20250609083529-2b129d95495e h1:QjdoupNBBgSMDypMWsbhb+/yfyv27b3mqT9eVj8g0h4= -github.com/openshift/api v0.0.0-20250609083529-2b129d95495e/go.mod h1:yk60tHAmHhtVpJQo3TwVYq2zpuP70iJIFDCmeKMIzPw= -github.com/openshift/hypershift/api v0.0.0-20251113065312-f919037748bf h1:TibqiqqSGwofAm8p5vdZ4Q3nYMwVofdtCcSoF0geF1o= -github.com/openshift/hypershift/api v0.0.0-20251113065312-f919037748bf/go.mod h1:JiaoBwTsYtBVKKPgHcajChZCu20KdM97W2xc0MeBCBA= +github.com/openshift/api v0.0.0-20260416105050-3c6b218b8a80 h1:r0S/yoZAI0iWo1JvoIijaIgWGWf/izg4WiV7Wrtz16k= +github.com/openshift/api v0.0.0-20260416105050-3c6b218b8a80/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo= +github.com/openshift/hypershift/api v0.0.0-20260625052409-9acec4759a16 h1:QDSh3vkKYq7Fn9utYGlAJadkTdyaRl9IY7Cr6cNDAow= +github.com/openshift/hypershift/api v0.0.0-20260625052409-9acec4759a16/go.mod h1:Z3lkj5pFqY+KTl3Do9gXdEZdKWLnkUTSDShLD1HE0CM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/hack/api-codegen/pkg/markers/scanner.go b/hack/api-codegen/pkg/markers/scanner.go index 023f6fb4..d3db1462 100644 --- a/hack/api-codegen/pkg/markers/scanner.go +++ b/hack/api-codegen/pkg/markers/scanner.go @@ -90,13 +90,15 @@ func (s *MarkerScanner) scanDir(dir string) error { return nil } -// isRootType returns true for top-level CRD types (not Spec/Status/Passthrough types) +// isRootType returns true for types that are entry points for marker scanning. +// Passthrough types ARE root types since they carry curated markers. func isRootType(typeName string) bool { - // Root types don't have suffixes + if strings.HasSuffix(typeName, "Passthrough") { + return true + } return !strings.HasSuffix(typeName, "Spec") && !strings.HasSuffix(typeName, "Status") && !strings.HasSuffix(typeName, "List") && - !strings.HasSuffix(typeName, "Passthrough") && typeName != "ClusterReference" } diff --git a/platform-api/Containerfile b/platform-api/Containerfile index ffb84f0c..44b16984 100644 --- a/platform-api/Containerfile +++ b/platform-api/Containerfile @@ -8,11 +8,13 @@ WORKDIR /app COPY hyperfleet-db/go.mod hyperfleet-db/go.sum ./hyperfleet-db/ COPY hyperfleet-operator/api/go.mod hyperfleet-operator/api/go.sum ./hyperfleet-operator/api/ +COPY hack/api-codegen/go.mod hack/api-codegen/go.sum ./hack/api-codegen/ COPY platform-api/go.mod platform-api/go.sum ./platform-api/ RUN cd platform-api && go mod download COPY hyperfleet-db/ ./hyperfleet-db/ COPY hyperfleet-operator/api/ ./hyperfleet-operator/api/ +COPY hack/api-codegen/ ./hack/api-codegen/ COPY platform-api/ ./platform-api/ RUN cd platform-api && \ diff --git a/platform-api/go.mod b/platform-api/go.mod index b422bdf5..148c0c55 100644 --- a/platform-api/go.mod +++ b/platform-api/go.mod @@ -3,6 +3,7 @@ module github.com/openshift-online/rosa-hyperfleet-api/platform-api go 1.26.3 replace ( + github.com/openshift-online/rosa-hyperfleet-api/hack/api-codegen => ../hack/api-codegen github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-db => ../hyperfleet-db github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api => ../hyperfleet-operator/api ) @@ -20,6 +21,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/onsi/ginkgo/v2 v2.28.1 + github.com/openshift-online/rosa-hyperfleet-api/hack/api-codegen v0.0.0 github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-db v0.0.0 github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api v0.0.0 github.com/openshift/hypershift/api v0.0.0-20260625052409-9acec4759a16 @@ -114,10 +116,10 @@ require ( k8s.io/apiextensions-apiserver v0.36.0 // indirect k8s.io/client-go v0.36.0 // indirect k8s.io/klog/v2 v2.140.0 // indirect - k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect + k8s.io/kube-openapi v0.0.0-20260706235625-cdb1db5517a0 // indirect k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.4.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/platform-api/go.sum b/platform-api/go.sum index c2ef6c1e..b6f73913 100644 --- a/platform-api/go.sum +++ b/platform-api/go.sum @@ -279,8 +279,8 @@ k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c= k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= -k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= -k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/kube-openapi v0.0.0-20260706235625-cdb1db5517a0 h1:CVjOUCTXINUThEmDs25FNSna0+vnGSoTleN+wiJu6hE= +k8s.io/kube-openapi v0.0.0-20260706235625-cdb1db5517a0/go.mod h1:rcZ+P5cEvHQB+m154WBOatIGBgOEPjzmLkXjkHfg3ms= k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 h1:jVkFFVfXdXP74B/zbO3hM3hpSFD0xvhQ5U686DPurkE= k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3/go.mod h1:M2s5JB1lIYP3jzZdorPLHXIPJzt9vv2muW5a6L9DtNM= sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4= @@ -289,7 +289,7 @@ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5E sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.4.1 h1:AkER7js0XVWi/F/V2Iwl5N7O/B9VP2JyrOMmHPdco+g= +sigs.k8s.io/structured-merge-diff/v6 v6.4.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/platform-api/internal/codegen/conversion/cluster.go b/platform-api/internal/codegen/conversion/cluster.go new file mode 100644 index 00000000..a5fe086f --- /dev/null +++ b/platform-api/internal/codegen/conversion/cluster.go @@ -0,0 +1,40 @@ +package conversion + +// ClusterServiceSetFields holds platform-injected values for cluster creation. +type ClusterServiceSetFields struct { + CloudURL string + Placement string + CreatorARN string +} + +// InjectClusterServiceSet strips client-supplied service-set fields and +// replaces them with platform-injected values. +func InjectClusterServiceSet(spec map[string]interface{}, ssf ClusterServiceSetFields) { + if spec == nil { + return + } + delete(spec, "cloudUrl") + if ssf.CloudURL != "" { + spec["cloudUrl"] = ssf.CloudURL + } + + delete(spec, "creatorARN") + if ssf.CreatorARN != "" { + spec["creatorARN"] = ssf.CreatorARN + } + + if ssf.Placement != "" { + existing, _ := spec["placement"].(string) + if existing == "" { + spec["placement"] = ssf.Placement + } + } +} + +// RewriteCloudURLWithID sets cloudUrl to baseURL/clusterID in a response spec. +func RewriteCloudURLWithID(spec map[string]interface{}, baseURL, clusterID string) { + if spec == nil { + return + } + spec["cloudUrl"] = baseURL + "/" + clusterID +} diff --git a/platform-api/internal/codegen/featuregate/registry.go b/platform-api/internal/codegen/featuregate/registry.go new file mode 100644 index 00000000..6add7396 --- /dev/null +++ b/platform-api/internal/codegen/featuregate/registry.go @@ -0,0 +1,53 @@ +package featuregate + +import "sort" + +// HyperFleetFeatureGates is the registry of all feature gates. +var HyperFleetFeatureGates = map[string]FeatureGateInfo{ + "HyperFleetEtcdConfig": { + Stage: GA, + Description: "Allows customers to configure etcd settings", + }, + "HyperFleetAutoScaling": { + Stage: TechPreview, + Description: "Enables cluster autoscaling configuration", + }, + "HyperFleetSecretEncryption": { + Stage: TechPreview, + Description: "Allows customers to configure secret encryption", + }, + "HyperFleetCustomDNS": { + Stage: DevPreview, + Description: "Enables custom DNS configuration for development/testing", + }, + "HyperFleetKubeletAdvanced": { + Stage: TechPreview, + Description: "Enables advanced kubelet configuration (serializeImagePulls, registryPullQPS, etc.)", + }, + "HyperFleetMachineConfig": { + Stage: TechPreview, + Description: "Allows customers to request approved kernel parameters via allowlist", + }, +} + +// IsGateEnabled returns true if the given gate is enabled for the feature set. +func IsGateEnabled(gate string, featureSet FeatureSet) bool { + info, exists := HyperFleetFeatureGates[gate] + if !exists { + return false + } + return featureSet.Includes(info.Stage) +} + +// GatesForFeatureSet returns all gates enabled for the given feature set. +func GatesForFeatureSet(featureSet FeatureSet) []string { + var gates []string + maxStage := featureSet.MaxStage() + for gate, info := range HyperFleetFeatureGates { + if info.Stage <= maxStage { + gates = append(gates, gate) + } + } + sort.Strings(gates) + return gates +} diff --git a/platform-api/internal/codegen/featuregate/types.go b/platform-api/internal/codegen/featuregate/types.go new file mode 100644 index 00000000..4c4676e8 --- /dev/null +++ b/platform-api/internal/codegen/featuregate/types.go @@ -0,0 +1,53 @@ +package featuregate + +// FeatureStage represents the maturity stage of a feature gate +type FeatureStage int + +const ( + GA FeatureStage = iota + TechPreview + DevPreview +) + +func (s FeatureStage) String() string { + switch s { + case GA: + return "GA" + case TechPreview: + return "TechPreview" + case DevPreview: + return "DevPreview" + default: + return "Unknown" + } +} + +// FeatureGateInfo describes a single feature gate +type FeatureGateInfo struct { + Stage FeatureStage + Description string +} + +// FeatureSet represents a collection of feature gates +type FeatureSet string + +const ( + Default FeatureSet = "Default" + TechPreviewNoUpgrade FeatureSet = "TechPreviewNoUpgrade" + DevPreviewNoUpgrade FeatureSet = "DevPreviewNoUpgrade" +) + +func (fs FeatureSet) MaxStage() FeatureStage { + switch fs { + case TechPreviewNoUpgrade: + return TechPreview + case DevPreviewNoUpgrade: + return DevPreview + default: + return GA + } +} + +func (fs FeatureSet) Includes(stage FeatureStage) bool { + return stage <= fs.MaxStage() +} diff --git a/platform-api/internal/codegen/registry/field_metadata.go b/platform-api/internal/codegen/registry/field_metadata.go new file mode 100644 index 00000000..d29abc19 --- /dev/null +++ b/platform-api/internal/codegen/registry/field_metadata.go @@ -0,0 +1,588 @@ +// Code generated by marker-scanner. DO NOT EDIT. + +package registry + +import ( + "github.com/openshift-online/rosa-hyperfleet-api/hack/api-codegen/pkg/markers" +) + +// Type aliases re-exported from markers so that consumers of this package +// do not need to import markers directly. +type WriteMode = markers.WriteMode +type FieldMeta = markers.FieldMeta +type FeatureGateWriteMode = markers.FeatureGateWriteMode + +const ( + Mutable = markers.Mutable + Immutable = markers.Immutable + ServiceSet = markers.ServiceSet +) + +// FieldRegistry maps field paths to their metadata +var FieldRegistry = map[string]FieldMeta{ + "additionalTrustBundle": { + FieldPath: "additionalTrustBundle", + WriteMode: ServiceSet, + Hidden: true, + }, + "allowedUnsafeSysctls": { + FieldPath: "allowedUnsafeSysctls", + WriteMode: ServiceSet, + Hidden: true, + }, + "apiServer": { + FieldPath: "apiServer", + WriteMode: ServiceSet, + Hidden: true, + }, + "arch": { + FieldPath: "arch", + WriteMode: ServiceSet, + Hidden: true, + }, + "auditWebhook": { + FieldPath: "auditWebhook", + WriteMode: ServiceSet, + Hidden: true, + }, + "authentication": { + FieldPath: "authentication", + WriteMode: ServiceSet, + Hidden: true, + }, + "autoNode": { + FieldPath: "autoNode", + WriteMode: ServiceSet, + }, + "autoScaling": { + FieldPath: "autoScaling", + WriteMode: ServiceSet, + Hidden: true, + }, + "autoscaling": { + FieldPath: "autoscaling", + WriteMode: ServiceSet, + Hidden: true, + }, + "capabilities": { + FieldPath: "capabilities", + WriteMode: ServiceSet, + Hidden: true, + }, + "channel": { + FieldPath: "channel", + WriteMode: ServiceSet, + }, + "clusterID": { + FieldPath: "clusterID", + WriteMode: ServiceSet, + Hidden: true, + }, + "clusterName": { + FieldPath: "clusterName", + WriteMode: ServiceSet, + Hidden: true, + }, + "config": { + FieldPath: "config", + WriteMode: ServiceSet, + Hidden: true, + }, + "configuration": { + FieldPath: "configuration", + WriteMode: ServiceSet, + }, + "containerLogMaxFiles": { + FieldPath: "containerLogMaxFiles", + WriteMode: Mutable, + }, + "containerLogMaxSize": { + FieldPath: "containerLogMaxSize", + WriteMode: Mutable, + }, + "controlPlaneRelease": { + FieldPath: "controlPlaneRelease", + WriteMode: ServiceSet, + Hidden: true, + }, + "controllerAvailabilityPolicy": { + FieldPath: "controllerAvailabilityPolicy", + WriteMode: ServiceSet, + Hidden: true, + }, + "cpuManagerPolicy": { + FieldPath: "cpuManagerPolicy", + WriteMode: ServiceSet, + Hidden: true, + }, + "cpuManagerPolicyOptions": { + FieldPath: "cpuManagerPolicyOptions", + WriteMode: ServiceSet, + Hidden: true, + }, + "cpuManagerReconcilePeriod": { + FieldPath: "cpuManagerReconcilePeriod", + WriteMode: ServiceSet, + Hidden: true, + }, + "dns": { + FieldPath: "dns", + WriteMode: ServiceSet, + Hidden: true, + }, + "etcd": { + FieldPath: "etcd", + WriteMode: ServiceSet, + Hidden: true, + }, + "evictionHard": { + FieldPath: "evictionHard", + WriteMode: ServiceSet, + Hidden: true, + }, + "evictionSoft": { + FieldPath: "evictionSoft", + WriteMode: ServiceSet, + Hidden: true, + }, + "evictionSoftGracePeriod": { + FieldPath: "evictionSoftGracePeriod", + WriteMode: ServiceSet, + Hidden: true, + }, + "featureGate": { + FieldPath: "featureGate", + WriteMode: ServiceSet, + Hidden: true, + }, + "fips": { + FieldPath: "fips", + WriteMode: ServiceSet, + }, + "image": { + FieldPath: "image", + WriteMode: ServiceSet, + Hidden: true, + }, + "imageContentSources": { + FieldPath: "imageContentSources", + WriteMode: ServiceSet, + Hidden: true, + }, + "imageGCHighThresholdPercent": { + FieldPath: "imageGCHighThresholdPercent", + WriteMode: Mutable, + }, + "imageGCLowThresholdPercent": { + FieldPath: "imageGCLowThresholdPercent", + WriteMode: Mutable, + }, + "imageMinimumGCAge": { + FieldPath: "imageMinimumGCAge", + WriteMode: Mutable, + }, + "infraID": { + FieldPath: "infraID", + WriteMode: ServiceSet, + Hidden: true, + }, + "infrastructureAvailabilityPolicy": { + FieldPath: "infrastructureAvailabilityPolicy", + WriteMode: ServiceSet, + Hidden: true, + }, + "ingress": { + FieldPath: "ingress", + WriteMode: ServiceSet, + Hidden: true, + }, + "issuerURL": { + FieldPath: "issuerURL", + WriteMode: ServiceSet, + Hidden: true, + }, + "kubeAPIServerDNSName": { + FieldPath: "kubeAPIServerDNSName", + WriteMode: ServiceSet, + Hidden: true, + }, + "kubeReserved": { + FieldPath: "kubeReserved", + WriteMode: Immutable, + }, + "kubelet": { + FieldPath: "kubelet", + WriteMode: ServiceSet, + }, + "kubelet.allowedUnsafeSysctls": { + FieldPath: "kubelet.allowedUnsafeSysctls", + WriteMode: ServiceSet, + Hidden: true, + }, + "kubelet.containerLogMaxFiles": { + FieldPath: "kubelet.containerLogMaxFiles", + WriteMode: Mutable, + }, + "kubelet.containerLogMaxSize": { + FieldPath: "kubelet.containerLogMaxSize", + WriteMode: Mutable, + }, + "kubelet.cpuManagerPolicy": { + FieldPath: "kubelet.cpuManagerPolicy", + WriteMode: ServiceSet, + Hidden: true, + }, + "kubelet.cpuManagerPolicyOptions": { + FieldPath: "kubelet.cpuManagerPolicyOptions", + WriteMode: ServiceSet, + Hidden: true, + }, + "kubelet.cpuManagerReconcilePeriod": { + FieldPath: "kubelet.cpuManagerReconcilePeriod", + WriteMode: ServiceSet, + Hidden: true, + }, + "kubelet.evictionHard": { + FieldPath: "kubelet.evictionHard", + WriteMode: ServiceSet, + Hidden: true, + }, + "kubelet.evictionSoft": { + FieldPath: "kubelet.evictionSoft", + WriteMode: ServiceSet, + Hidden: true, + }, + "kubelet.evictionSoftGracePeriod": { + FieldPath: "kubelet.evictionSoftGracePeriod", + WriteMode: ServiceSet, + Hidden: true, + }, + "kubelet.imageGCHighThresholdPercent": { + FieldPath: "kubelet.imageGCHighThresholdPercent", + WriteMode: Mutable, + }, + "kubelet.imageGCLowThresholdPercent": { + FieldPath: "kubelet.imageGCLowThresholdPercent", + WriteMode: Mutable, + }, + "kubelet.imageMinimumGCAge": { + FieldPath: "kubelet.imageMinimumGCAge", + WriteMode: Mutable, + }, + "kubelet.kubeReserved": { + FieldPath: "kubelet.kubeReserved", + WriteMode: Immutable, + }, + "kubelet.maxPods": { + FieldPath: "kubelet.maxPods", + WriteMode: Mutable, + }, + "kubelet.memoryThrottlingFactor": { + FieldPath: "kubelet.memoryThrottlingFactor", + WriteMode: ServiceSet, + Hidden: true, + }, + "kubelet.podPidsLimit": { + FieldPath: "kubelet.podPidsLimit", + WriteMode: Mutable, + }, + "kubelet.registryBurst": { + FieldPath: "kubelet.registryBurst", + WriteMode: Mutable, + FeatureGate: "HyperFleetKubeletAdvanced", + }, + "kubelet.registryPullQPS": { + FieldPath: "kubelet.registryPullQPS", + WriteMode: Mutable, + FeatureGate: "HyperFleetKubeletAdvanced", + }, + "kubelet.serializeImagePulls": { + FieldPath: "kubelet.serializeImagePulls", + WriteMode: Mutable, + FeatureGate: "HyperFleetKubeletAdvanced", + }, + "kubelet.streamingConnectionIdleTimeout": { + FieldPath: "kubelet.streamingConnectionIdleTimeout", + WriteMode: Mutable, + }, + "kubelet.systemReserved": { + FieldPath: "kubelet.systemReserved", + WriteMode: Immutable, + }, + "kubelet.topologyManagerPolicy": { + FieldPath: "kubelet.topologyManagerPolicy", + WriteMode: ServiceSet, + Hidden: true, + }, + "kubelet.topologyManagerScope": { + FieldPath: "kubelet.topologyManagerScope", + WriteMode: ServiceSet, + Hidden: true, + }, + "labels": { + FieldPath: "labels", + WriteMode: ServiceSet, + Hidden: true, + }, + "machineConfig": { + FieldPath: "machineConfig", + WriteMode: ServiceSet, + }, + "machineConfig.allowedKernelArguments": { + FieldPath: "machineConfig.allowedKernelArguments", + WriteMode: Immutable, + FeatureGate: "HyperFleetMachineConfig", + }, + "machineConfig.extensions": { + FieldPath: "machineConfig.extensions", + WriteMode: ServiceSet, + Hidden: true, + }, + "machineConfig.files": { + FieldPath: "machineConfig.files", + WriteMode: ServiceSet, + Hidden: true, + }, + "machineConfig.fips": { + FieldPath: "machineConfig.fips", + WriteMode: Immutable, + }, + "machineConfig.kernelArguments": { + FieldPath: "machineConfig.kernelArguments", + WriteMode: ServiceSet, + Hidden: true, + }, + "machineConfig.kernelType": { + FieldPath: "machineConfig.kernelType", + WriteMode: ServiceSet, + Hidden: true, + }, + "machineConfig.systemdUnits": { + FieldPath: "machineConfig.systemdUnits", + WriteMode: ServiceSet, + Hidden: true, + }, + "management": { + FieldPath: "management", + WriteMode: ServiceSet, + Hidden: true, + }, + "maxPods": { + FieldPath: "maxPods", + WriteMode: Mutable, + }, + "memoryThrottlingFactor": { + FieldPath: "memoryThrottlingFactor", + WriteMode: ServiceSet, + Hidden: true, + }, + "network": { + FieldPath: "network", + WriteMode: ServiceSet, + Hidden: true, + }, + "networking": { + FieldPath: "networking", + WriteMode: ServiceSet, + Hidden: true, + }, + "nodeDrainTimeout": { + FieldPath: "nodeDrainTimeout", + WriteMode: ServiceSet, + Hidden: true, + }, + "nodeLabels": { + FieldPath: "nodeLabels", + WriteMode: ServiceSet, + Hidden: true, + }, + "nodeSelector": { + FieldPath: "nodeSelector", + WriteMode: ServiceSet, + Hidden: true, + }, + "nodeVolumeDetachTimeout": { + FieldPath: "nodeVolumeDetachTimeout", + WriteMode: ServiceSet, + Hidden: true, + }, + "oauth": { + FieldPath: "oauth", + WriteMode: ServiceSet, + Hidden: true, + }, + "olmCatalogPlacement": { + FieldPath: "olmCatalogPlacement", + WriteMode: ServiceSet, + Hidden: true, + }, + "operatorConfiguration": { + FieldPath: "operatorConfiguration", + WriteMode: ServiceSet, + }, + "osImageStream": { + FieldPath: "osImageStream", + WriteMode: ServiceSet, + Hidden: true, + }, + "pausedUntil": { + FieldPath: "pausedUntil", + WriteMode: ServiceSet, + }, + "platform": { + FieldPath: "platform", + WriteMode: ServiceSet, + Hidden: true, + }, + "podPidsLimit": { + FieldPath: "podPidsLimit", + WriteMode: Mutable, + }, + "proxy": { + FieldPath: "proxy", + WriteMode: ServiceSet, + Hidden: true, + }, + "pullSecret": { + FieldPath: "pullSecret", + WriteMode: ServiceSet, + Hidden: true, + }, + "registryBurst": { + FieldPath: "registryBurst", + WriteMode: Mutable, + FeatureGate: "HyperFleetKubeletAdvanced", + }, + "registryPullQPS": { + FieldPath: "registryPullQPS", + WriteMode: Mutable, + FeatureGate: "HyperFleetKubeletAdvanced", + }, + "release": { + FieldPath: "release", + WriteMode: ServiceSet, + Hidden: true, + }, + "replicas": { + FieldPath: "replicas", + WriteMode: ServiceSet, + Hidden: true, + }, + "scheduler": { + FieldPath: "scheduler", + WriteMode: ServiceSet, + Hidden: true, + }, + "secretEncryption": { + FieldPath: "secretEncryption", + WriteMode: ServiceSet, + Hidden: true, + }, + "serializeImagePulls": { + FieldPath: "serializeImagePulls", + WriteMode: Mutable, + FeatureGate: "HyperFleetKubeletAdvanced", + }, + "serviceAccountSigningKey": { + FieldPath: "serviceAccountSigningKey", + WriteMode: ServiceSet, + Hidden: true, + }, + "services": { + FieldPath: "services", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.accountId": { + FieldPath: "spec.accountId", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.autoRepair": { + FieldPath: "spec.autoRepair", + WriteMode: Mutable, + }, + "spec.creatorARN": { + FieldPath: "spec.creatorARN", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.deleteProtection": { + FieldPath: "spec.deleteProtection", + WriteMode: Mutable, + }, + "spec.displayName": { + FieldPath: "spec.displayName", + WriteMode: Mutable, + }, + "spec.expirationTimestamp": { + FieldPath: "spec.expirationTimestamp", + WriteMode: Mutable, + }, + "spec.internalId": { + FieldPath: "spec.internalId", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.internalPoolId": { + FieldPath: "spec.internalPoolId", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.labels": { + FieldPath: "spec.labels", + WriteMode: Mutable, + }, + "spec.properties": { + FieldPath: "spec.properties", + WriteMode: Mutable, + }, + "spec.tags": { + FieldPath: "spec.tags", + WriteMode: Mutable, + FeatureGate: "HyperFleetAutoScaling", + }, + "sshKey": { + FieldPath: "sshKey", + WriteMode: ServiceSet, + Hidden: true, + }, + "streamingConnectionIdleTimeout": { + FieldPath: "streamingConnectionIdleTimeout", + WriteMode: Mutable, + }, + "systemReserved": { + FieldPath: "systemReserved", + WriteMode: Immutable, + }, + "taints": { + FieldPath: "taints", + WriteMode: ServiceSet, + Hidden: true, + }, + "tolerations": { + FieldPath: "tolerations", + WriteMode: ServiceSet, + Hidden: true, + }, + "topologyManagerPolicy": { + FieldPath: "topologyManagerPolicy", + WriteMode: ServiceSet, + Hidden: true, + }, + "topologyManagerScope": { + FieldPath: "topologyManagerScope", + WriteMode: ServiceSet, + Hidden: true, + }, + "tuningConfig": { + FieldPath: "tuningConfig", + WriteMode: ServiceSet, + Hidden: true, + }, + "updateService": { + FieldPath: "updateService", + WriteMode: ServiceSet, + Hidden: true, + }, +} diff --git a/platform-api/internal/codegen/registry/field_metadata.json b/platform-api/internal/codegen/registry/field_metadata.json new file mode 100644 index 00000000..ac3e3592 --- /dev/null +++ b/platform-api/internal/codegen/registry/field_metadata.json @@ -0,0 +1,567 @@ +[ + { + "fieldPath": "additionalTrustBundle", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "allowedUnsafeSysctls", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "apiServer", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "arch", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "auditWebhook", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "authentication", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "autoNode", + "writeMode": "service-set" + }, + { + "fieldPath": "autoScaling", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "autoscaling", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "capabilities", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "channel", + "writeMode": "service-set" + }, + { + "fieldPath": "clusterID", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "clusterName", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "config", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "configuration", + "writeMode": "service-set" + }, + { + "fieldPath": "containerLogMaxFiles", + "writeMode": "mutable" + }, + { + "fieldPath": "containerLogMaxSize", + "writeMode": "mutable" + }, + { + "fieldPath": "controlPlaneRelease", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "controllerAvailabilityPolicy", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "cpuManagerPolicy", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "cpuManagerPolicyOptions", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "cpuManagerReconcilePeriod", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "dns", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "etcd", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "evictionHard", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "evictionSoft", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "evictionSoftGracePeriod", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "featureGate", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "fips", + "writeMode": "service-set" + }, + { + "fieldPath": "image", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "imageContentSources", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "imageGCHighThresholdPercent", + "writeMode": "mutable" + }, + { + "fieldPath": "imageGCLowThresholdPercent", + "writeMode": "mutable" + }, + { + "fieldPath": "imageMinimumGCAge", + "writeMode": "mutable" + }, + { + "fieldPath": "infraID", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "infrastructureAvailabilityPolicy", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "ingress", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "issuerURL", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "kubeAPIServerDNSName", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "kubeReserved", + "writeMode": "immutable" + }, + { + "fieldPath": "kubelet", + "writeMode": "service-set" + }, + { + "fieldPath": "kubelet.allowedUnsafeSysctls", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "kubelet.containerLogMaxFiles", + "writeMode": "mutable" + }, + { + "fieldPath": "kubelet.containerLogMaxSize", + "writeMode": "mutable" + }, + { + "fieldPath": "kubelet.cpuManagerPolicy", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "kubelet.cpuManagerPolicyOptions", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "kubelet.cpuManagerReconcilePeriod", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "kubelet.evictionHard", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "kubelet.evictionSoft", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "kubelet.evictionSoftGracePeriod", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "kubelet.imageGCHighThresholdPercent", + "writeMode": "mutable" + }, + { + "fieldPath": "kubelet.imageGCLowThresholdPercent", + "writeMode": "mutable" + }, + { + "fieldPath": "kubelet.imageMinimumGCAge", + "writeMode": "mutable" + }, + { + "fieldPath": "kubelet.kubeReserved", + "writeMode": "immutable" + }, + { + "fieldPath": "kubelet.maxPods", + "writeMode": "mutable" + }, + { + "fieldPath": "kubelet.memoryThrottlingFactor", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "kubelet.podPidsLimit", + "writeMode": "mutable" + }, + { + "fieldPath": "kubelet.registryBurst", + "writeMode": "mutable", + "featureGate": "HyperFleetKubeletAdvanced" + }, + { + "fieldPath": "kubelet.registryPullQPS", + "writeMode": "mutable", + "featureGate": "HyperFleetKubeletAdvanced" + }, + { + "fieldPath": "kubelet.serializeImagePulls", + "writeMode": "mutable", + "featureGate": "HyperFleetKubeletAdvanced" + }, + { + "fieldPath": "kubelet.streamingConnectionIdleTimeout", + "writeMode": "mutable" + }, + { + "fieldPath": "kubelet.systemReserved", + "writeMode": "immutable" + }, + { + "fieldPath": "kubelet.topologyManagerPolicy", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "kubelet.topologyManagerScope", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "labels", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "machineConfig", + "writeMode": "service-set" + }, + { + "fieldPath": "machineConfig.allowedKernelArguments", + "writeMode": "immutable", + "featureGate": "HyperFleetMachineConfig" + }, + { + "fieldPath": "machineConfig.extensions", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "machineConfig.files", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "machineConfig.fips", + "writeMode": "immutable" + }, + { + "fieldPath": "machineConfig.kernelArguments", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "machineConfig.kernelType", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "machineConfig.systemdUnits", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "management", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "maxPods", + "writeMode": "mutable" + }, + { + "fieldPath": "memoryThrottlingFactor", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "network", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "networking", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "nodeDrainTimeout", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "nodeLabels", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "nodeSelector", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "nodeVolumeDetachTimeout", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "oauth", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "olmCatalogPlacement", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "operatorConfiguration", + "writeMode": "service-set" + }, + { + "fieldPath": "osImageStream", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "pausedUntil", + "writeMode": "service-set" + }, + { + "fieldPath": "platform", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "podPidsLimit", + "writeMode": "mutable" + }, + { + "fieldPath": "proxy", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "pullSecret", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "registryBurst", + "writeMode": "mutable", + "featureGate": "HyperFleetKubeletAdvanced" + }, + { + "fieldPath": "registryPullQPS", + "writeMode": "mutable", + "featureGate": "HyperFleetKubeletAdvanced" + }, + { + "fieldPath": "release", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "replicas", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "scheduler", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "secretEncryption", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "serializeImagePulls", + "writeMode": "mutable", + "featureGate": "HyperFleetKubeletAdvanced" + }, + { + "fieldPath": "serviceAccountSigningKey", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "services", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.accountId", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.autoRepair", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.creatorARN", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.deleteProtection", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.displayName", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.expirationTimestamp", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.internalId", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.internalPoolId", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.labels", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.properties", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.tags", + "writeMode": "mutable", + "featureGate": "HyperFleetAutoScaling" + }, + { + "fieldPath": "sshKey", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "streamingConnectionIdleTimeout", + "writeMode": "mutable" + }, + { + "fieldPath": "systemReserved", + "writeMode": "immutable" + }, + { + "fieldPath": "taints", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "tolerations", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "topologyManagerPolicy", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "topologyManagerScope", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "tuningConfig", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "updateService", + "writeMode": "service-set", + "hidden": true + } +] \ No newline at end of file From 31efc403f7c7e10da64b0e729061e523d85e9f6e Mon Sep 17 00:00:00 2001 From: Chris Doan Date: Tue, 4 Aug 2026 09:48:48 -0700 Subject: [PATCH 2/7] ROSAENG-61801: fix scanner field collision and add verify-codegen target The marker-scanner produced flat registry keys (e.g. "pausedUntil") with no root-type namespace, so fields with the same JSON name in different passthrough types silently overwrote each other with non-deterministic results. Prefix passthrough fields with their root type context (spec.hostedCluster.* / spec.nodePool.*) to match the paths that downstream consumers already construct. Add a verify-codegen Makefile target that re-runs the full codegen pipeline and fails on git diff, same pattern as verify-clientset, so CI catches stale generated code. Co-Authored-By: Claude Opus 4.6 --- Makefile | 7 +- hack/api-codegen/pkg/markers/scanner.go | 21 +- hack/api-codegen/pkg/markers/scanner_test.go | 76 +++ .../codegen/registry/field_metadata.go | 435 +++++++++--------- .../codegen/registry/field_metadata.json | 359 ++++++++------- 5 files changed, 514 insertions(+), 384 deletions(-) diff --git a/Makefile b/Makefile index 11ea26dc..53037923 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ fmt vet verify deps \ manifests generate generate-clientset verify-clientset \ generate-public-deepcopy setup-envtest \ - codegen-passthrough codegen-registry codegen-verify codegen \ + codegen-passthrough codegen-registry codegen-verify codegen verify-codegen \ image-api image-operator image-push-api image-push-operator # ── Configuration ──────────────────────────────────────────────────────── @@ -116,6 +116,7 @@ help: @echo " codegen-passthrough Generate passthrough types from HyperShift" @echo " codegen-registry Generate field metadata registry from markers" @echo " codegen-verify Verify codegen outputs compile" + @echo " verify-codegen Fail if codegen outputs are out of date" @echo " setup-envtest Install envtest binaries (etcd, kube-apiserver)" @echo " deps Download and tidy all modules" @echo "" @@ -347,6 +348,10 @@ codegen-verify: codegen-registry codegen: codegen-verify +verify-codegen: codegen + git diff --exit-code api/public/v2alpha1/zz_generated.deepcopy.go + git diff --exit-code platform-api/internal/codegen/registry/ + ENVTEST_BIN_DIR ?= $(shell pwd)/.envtest setup-envtest: $(SETUP_ENVTEST) diff --git a/hack/api-codegen/pkg/markers/scanner.go b/hack/api-codegen/pkg/markers/scanner.go index d3db1462..9257fcfb 100644 --- a/hack/api-codegen/pkg/markers/scanner.go +++ b/hack/api-codegen/pkg/markers/scanner.go @@ -83,13 +83,32 @@ func (s *MarkerScanner) scanDir(dir string) error { if isRootType(typeName) { visited := make(map[string]bool) visited[typeName] = true - s.processStruct(typeName, structType, "", visited) + prefix := rootTypePrefix(typeName) + s.processStruct(typeName, structType, prefix, visited) } } return nil } +// rootTypePrefix returns a dotted prefix that namespaces registry keys by root +// type, so fields with the same JSON name in different root types (e.g. +// HostedClusterSpecPassthrough.PausedUntil vs NodePoolSpecPassthrough.PausedUntil) +// don't collide in the flat FieldRegistry map. The prefixes mirror +// conversion/generator.go buildFieldPath so consumers can look up entries +// with the same key they construct. +func rootTypePrefix(typeName string) string { + if strings.HasSuffix(typeName, "Passthrough") { + if strings.HasPrefix(typeName, "HostedCluster") { + return "spec.hostedCluster" + } + if strings.HasPrefix(typeName, "NodePool") { + return "spec.nodePool" + } + } + return "" +} + // isRootType returns true for types that are entry points for marker scanning. // Passthrough types ARE root types since they carry curated markers. func isRootType(typeName string) bool { diff --git a/hack/api-codegen/pkg/markers/scanner_test.go b/hack/api-codegen/pkg/markers/scanner_test.go index 249c48f2..6956b948 100644 --- a/hack/api-codegen/pkg/markers/scanner_test.go +++ b/hack/api-codegen/pkg/markers/scanner_test.go @@ -90,6 +90,82 @@ type EtcdSpec struct { } } +func TestPassthroughTypesPrefixed(t *testing.T) { + tmpDir := t.TempDir() + + testFile := filepath.Join(tmpDir, "types.go") + content := `package test + +type HostedClusterSpecPassthrough struct { + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=service-set + PausedUntil *string ` + "`json:\"pausedUntil,omitempty\"`" + ` + + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=immutable + FIPS bool ` + "`json:\"fips\"`" + ` +} + +type NodePoolSpecPassthrough struct { + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + PausedUntil *string ` + "`json:\"pausedUntil,omitempty\"`" + ` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + Replicas *int ` + "`json:\"replicas,omitempty\"`" + ` +} +` + + if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + scanner := NewScanner([]string{tmpDir}) + if err := scanner.Scan(); err != nil { + t.Fatalf("Scan failed: %v", err) + } + + tests := []struct { + fieldPath string + writeMode WriteMode + hidden bool + }{ + {"spec.hostedCluster.pausedUntil", ServiceSet, false}, + {"spec.hostedCluster.fips", Immutable, false}, + {"spec.nodePool.pausedUntil", ServiceSet, true}, + {"spec.nodePool.replicas", ServiceSet, true}, + } + + for _, tt := range tests { + t.Run(tt.fieldPath, func(t *testing.T) { + meta, found := scanner.Registry[tt.fieldPath] + if !found { + t.Fatalf("Field %s not found in registry (keys: %v)", tt.fieldPath, registryKeys(scanner.Registry)) + } + if meta.WriteMode != tt.writeMode { + t.Errorf("WriteMode = %v, want %v", meta.WriteMode, tt.writeMode) + } + if meta.Hidden != tt.hidden { + t.Errorf("Hidden = %v, want %v", meta.Hidden, tt.hidden) + } + }) + } + + // Verify no flat "pausedUntil" key exists (the old collision) + if _, found := scanner.Registry["pausedUntil"]; found { + t.Error("flat key \"pausedUntil\" should not exist; passthrough fields must be prefixed") + } +} + +func registryKeys(r FieldRegistry) []string { + keys := make([]string, 0, len(r)) + for k := range r { + keys = append(keys, k) + } + return keys +} + func TestValidation(t *testing.T) { tests := []struct { name string diff --git a/platform-api/internal/codegen/registry/field_metadata.go b/platform-api/internal/codegen/registry/field_metadata.go index d29abc19..a91856e3 100644 --- a/platform-api/internal/codegen/registry/field_metadata.go +++ b/platform-api/internal/codegen/registry/field_metadata.go @@ -20,11 +20,6 @@ const ( // FieldRegistry maps field paths to their metadata var FieldRegistry = map[string]FieldMeta{ - "additionalTrustBundle": { - FieldPath: "additionalTrustBundle", - WriteMode: ServiceSet, - Hidden: true, - }, "allowedUnsafeSysctls": { FieldPath: "allowedUnsafeSysctls", WriteMode: ServiceSet, @@ -35,63 +30,11 @@ var FieldRegistry = map[string]FieldMeta{ WriteMode: ServiceSet, Hidden: true, }, - "arch": { - FieldPath: "arch", - WriteMode: ServiceSet, - Hidden: true, - }, - "auditWebhook": { - FieldPath: "auditWebhook", - WriteMode: ServiceSet, - Hidden: true, - }, "authentication": { FieldPath: "authentication", WriteMode: ServiceSet, Hidden: true, }, - "autoNode": { - FieldPath: "autoNode", - WriteMode: ServiceSet, - }, - "autoScaling": { - FieldPath: "autoScaling", - WriteMode: ServiceSet, - Hidden: true, - }, - "autoscaling": { - FieldPath: "autoscaling", - WriteMode: ServiceSet, - Hidden: true, - }, - "capabilities": { - FieldPath: "capabilities", - WriteMode: ServiceSet, - Hidden: true, - }, - "channel": { - FieldPath: "channel", - WriteMode: ServiceSet, - }, - "clusterID": { - FieldPath: "clusterID", - WriteMode: ServiceSet, - Hidden: true, - }, - "clusterName": { - FieldPath: "clusterName", - WriteMode: ServiceSet, - Hidden: true, - }, - "config": { - FieldPath: "config", - WriteMode: ServiceSet, - Hidden: true, - }, - "configuration": { - FieldPath: "configuration", - WriteMode: ServiceSet, - }, "containerLogMaxFiles": { FieldPath: "containerLogMaxFiles", WriteMode: Mutable, @@ -100,16 +43,6 @@ var FieldRegistry = map[string]FieldMeta{ FieldPath: "containerLogMaxSize", WriteMode: Mutable, }, - "controlPlaneRelease": { - FieldPath: "controlPlaneRelease", - WriteMode: ServiceSet, - Hidden: true, - }, - "controllerAvailabilityPolicy": { - FieldPath: "controllerAvailabilityPolicy", - WriteMode: ServiceSet, - Hidden: true, - }, "cpuManagerPolicy": { FieldPath: "cpuManagerPolicy", WriteMode: ServiceSet, @@ -125,16 +58,6 @@ var FieldRegistry = map[string]FieldMeta{ WriteMode: ServiceSet, Hidden: true, }, - "dns": { - FieldPath: "dns", - WriteMode: ServiceSet, - Hidden: true, - }, - "etcd": { - FieldPath: "etcd", - WriteMode: ServiceSet, - Hidden: true, - }, "evictionHard": { FieldPath: "evictionHard", WriteMode: ServiceSet, @@ -155,20 +78,11 @@ var FieldRegistry = map[string]FieldMeta{ WriteMode: ServiceSet, Hidden: true, }, - "fips": { - FieldPath: "fips", - WriteMode: ServiceSet, - }, "image": { FieldPath: "image", WriteMode: ServiceSet, Hidden: true, }, - "imageContentSources": { - FieldPath: "imageContentSources", - WriteMode: ServiceSet, - Hidden: true, - }, "imageGCHighThresholdPercent": { FieldPath: "imageGCHighThresholdPercent", WriteMode: Mutable, @@ -181,31 +95,11 @@ var FieldRegistry = map[string]FieldMeta{ FieldPath: "imageMinimumGCAge", WriteMode: Mutable, }, - "infraID": { - FieldPath: "infraID", - WriteMode: ServiceSet, - Hidden: true, - }, - "infrastructureAvailabilityPolicy": { - FieldPath: "infrastructureAvailabilityPolicy", - WriteMode: ServiceSet, - Hidden: true, - }, "ingress": { FieldPath: "ingress", WriteMode: ServiceSet, Hidden: true, }, - "issuerURL": { - FieldPath: "issuerURL", - WriteMode: ServiceSet, - Hidden: true, - }, - "kubeAPIServerDNSName": { - FieldPath: "kubeAPIServerDNSName", - WriteMode: ServiceSet, - Hidden: true, - }, "kubeReserved": { FieldPath: "kubeReserved", WriteMode: Immutable, @@ -319,11 +213,6 @@ var FieldRegistry = map[string]FieldMeta{ WriteMode: ServiceSet, Hidden: true, }, - "labels": { - FieldPath: "labels", - WriteMode: ServiceSet, - Hidden: true, - }, "machineConfig": { FieldPath: "machineConfig", WriteMode: ServiceSet, @@ -362,11 +251,6 @@ var FieldRegistry = map[string]FieldMeta{ WriteMode: ServiceSet, Hidden: true, }, - "management": { - FieldPath: "management", - WriteMode: ServiceSet, - Hidden: true, - }, "maxPods": { FieldPath: "maxPods", WriteMode: Mutable, @@ -381,143 +265,224 @@ var FieldRegistry = map[string]FieldMeta{ WriteMode: ServiceSet, Hidden: true, }, - "networking": { - FieldPath: "networking", + "oauth": { + FieldPath: "oauth", WriteMode: ServiceSet, Hidden: true, }, - "nodeDrainTimeout": { - FieldPath: "nodeDrainTimeout", + "podPidsLimit": { + FieldPath: "podPidsLimit", + WriteMode: Mutable, + }, + "proxy": { + FieldPath: "proxy", WriteMode: ServiceSet, Hidden: true, }, - "nodeLabels": { - FieldPath: "nodeLabels", + "registryBurst": { + FieldPath: "registryBurst", + WriteMode: Mutable, + FeatureGate: "HyperFleetKubeletAdvanced", + }, + "registryPullQPS": { + FieldPath: "registryPullQPS", + WriteMode: Mutable, + FeatureGate: "HyperFleetKubeletAdvanced", + }, + "scheduler": { + FieldPath: "scheduler", WriteMode: ServiceSet, Hidden: true, }, - "nodeSelector": { - FieldPath: "nodeSelector", + "serializeImagePulls": { + FieldPath: "serializeImagePulls", + WriteMode: Mutable, + FeatureGate: "HyperFleetKubeletAdvanced", + }, + "spec.accountId": { + FieldPath: "spec.accountId", WriteMode: ServiceSet, Hidden: true, }, - "nodeVolumeDetachTimeout": { - FieldPath: "nodeVolumeDetachTimeout", + "spec.autoRepair": { + FieldPath: "spec.autoRepair", + WriteMode: Mutable, + }, + "spec.creatorARN": { + FieldPath: "spec.creatorARN", WriteMode: ServiceSet, Hidden: true, }, - "oauth": { - FieldPath: "oauth", + "spec.deleteProtection": { + FieldPath: "spec.deleteProtection", + WriteMode: Mutable, + }, + "spec.displayName": { + FieldPath: "spec.displayName", + WriteMode: Mutable, + }, + "spec.expirationTimestamp": { + FieldPath: "spec.expirationTimestamp", + WriteMode: Mutable, + }, + "spec.hostedCluster.additionalTrustBundle": { + FieldPath: "spec.hostedCluster.additionalTrustBundle", WriteMode: ServiceSet, Hidden: true, }, - "olmCatalogPlacement": { - FieldPath: "olmCatalogPlacement", + "spec.hostedCluster.auditWebhook": { + FieldPath: "spec.hostedCluster.auditWebhook", WriteMode: ServiceSet, Hidden: true, }, - "operatorConfiguration": { - FieldPath: "operatorConfiguration", + "spec.hostedCluster.autoNode": { + FieldPath: "spec.hostedCluster.autoNode", + WriteMode: ServiceSet, + }, + "spec.hostedCluster.autoscaling": { + FieldPath: "spec.hostedCluster.autoscaling", WriteMode: ServiceSet, + Hidden: true, }, - "osImageStream": { - FieldPath: "osImageStream", + "spec.hostedCluster.capabilities": { + FieldPath: "spec.hostedCluster.capabilities", WriteMode: ServiceSet, Hidden: true, }, - "pausedUntil": { - FieldPath: "pausedUntil", + "spec.hostedCluster.channel": { + FieldPath: "spec.hostedCluster.channel", WriteMode: ServiceSet, }, - "platform": { - FieldPath: "platform", + "spec.hostedCluster.clusterID": { + FieldPath: "spec.hostedCluster.clusterID", WriteMode: ServiceSet, Hidden: true, }, - "podPidsLimit": { - FieldPath: "podPidsLimit", - WriteMode: Mutable, + "spec.hostedCluster.configuration": { + FieldPath: "spec.hostedCluster.configuration", + WriteMode: ServiceSet, }, - "proxy": { - FieldPath: "proxy", + "spec.hostedCluster.controlPlaneRelease": { + FieldPath: "spec.hostedCluster.controlPlaneRelease", WriteMode: ServiceSet, Hidden: true, }, - "pullSecret": { - FieldPath: "pullSecret", + "spec.hostedCluster.controllerAvailabilityPolicy": { + FieldPath: "spec.hostedCluster.controllerAvailabilityPolicy", WriteMode: ServiceSet, Hidden: true, }, - "registryBurst": { - FieldPath: "registryBurst", - WriteMode: Mutable, - FeatureGate: "HyperFleetKubeletAdvanced", + "spec.hostedCluster.dns": { + FieldPath: "spec.hostedCluster.dns", + WriteMode: ServiceSet, + Hidden: true, }, - "registryPullQPS": { - FieldPath: "registryPullQPS", - WriteMode: Mutable, - FeatureGate: "HyperFleetKubeletAdvanced", + "spec.hostedCluster.etcd": { + FieldPath: "spec.hostedCluster.etcd", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.fips": { + FieldPath: "spec.hostedCluster.fips", + WriteMode: ServiceSet, }, - "release": { - FieldPath: "release", + "spec.hostedCluster.imageContentSources": { + FieldPath: "spec.hostedCluster.imageContentSources", WriteMode: ServiceSet, Hidden: true, }, - "replicas": { - FieldPath: "replicas", + "spec.hostedCluster.infraID": { + FieldPath: "spec.hostedCluster.infraID", WriteMode: ServiceSet, Hidden: true, }, - "scheduler": { - FieldPath: "scheduler", + "spec.hostedCluster.infrastructureAvailabilityPolicy": { + FieldPath: "spec.hostedCluster.infrastructureAvailabilityPolicy", WriteMode: ServiceSet, Hidden: true, }, - "secretEncryption": { - FieldPath: "secretEncryption", + "spec.hostedCluster.issuerURL": { + FieldPath: "spec.hostedCluster.issuerURL", WriteMode: ServiceSet, Hidden: true, }, - "serializeImagePulls": { - FieldPath: "serializeImagePulls", - WriteMode: Mutable, - FeatureGate: "HyperFleetKubeletAdvanced", + "spec.hostedCluster.kubeAPIServerDNSName": { + FieldPath: "spec.hostedCluster.kubeAPIServerDNSName", + WriteMode: ServiceSet, + Hidden: true, }, - "serviceAccountSigningKey": { - FieldPath: "serviceAccountSigningKey", + "spec.hostedCluster.labels": { + FieldPath: "spec.hostedCluster.labels", WriteMode: ServiceSet, Hidden: true, }, - "services": { - FieldPath: "services", + "spec.hostedCluster.networking": { + FieldPath: "spec.hostedCluster.networking", WriteMode: ServiceSet, Hidden: true, }, - "spec.accountId": { - FieldPath: "spec.accountId", + "spec.hostedCluster.nodeSelector": { + FieldPath: "spec.hostedCluster.nodeSelector", WriteMode: ServiceSet, Hidden: true, }, - "spec.autoRepair": { - FieldPath: "spec.autoRepair", - WriteMode: Mutable, + "spec.hostedCluster.olmCatalogPlacement": { + FieldPath: "spec.hostedCluster.olmCatalogPlacement", + WriteMode: ServiceSet, + Hidden: true, }, - "spec.creatorARN": { - FieldPath: "spec.creatorARN", + "spec.hostedCluster.operatorConfiguration": { + FieldPath: "spec.hostedCluster.operatorConfiguration", + WriteMode: ServiceSet, + }, + "spec.hostedCluster.pausedUntil": { + FieldPath: "spec.hostedCluster.pausedUntil", + WriteMode: ServiceSet, + }, + "spec.hostedCluster.platform": { + FieldPath: "spec.hostedCluster.platform", WriteMode: ServiceSet, Hidden: true, }, - "spec.deleteProtection": { - FieldPath: "spec.deleteProtection", - WriteMode: Mutable, + "spec.hostedCluster.pullSecret": { + FieldPath: "spec.hostedCluster.pullSecret", + WriteMode: ServiceSet, + Hidden: true, }, - "spec.displayName": { - FieldPath: "spec.displayName", - WriteMode: Mutable, + "spec.hostedCluster.release": { + FieldPath: "spec.hostedCluster.release", + WriteMode: ServiceSet, + Hidden: true, }, - "spec.expirationTimestamp": { - FieldPath: "spec.expirationTimestamp", - WriteMode: Mutable, + "spec.hostedCluster.secretEncryption": { + FieldPath: "spec.hostedCluster.secretEncryption", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.serviceAccountSigningKey": { + FieldPath: "spec.hostedCluster.serviceAccountSigningKey", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.services": { + FieldPath: "spec.hostedCluster.services", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.sshKey": { + FieldPath: "spec.hostedCluster.sshKey", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.tolerations": { + FieldPath: "spec.hostedCluster.tolerations", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.updateService": { + FieldPath: "spec.hostedCluster.updateService", + WriteMode: ServiceSet, + Hidden: true, }, "spec.internalId": { FieldPath: "spec.internalId", @@ -533,6 +498,81 @@ var FieldRegistry = map[string]FieldMeta{ FieldPath: "spec.labels", WriteMode: Mutable, }, + "spec.nodePool.arch": { + FieldPath: "spec.nodePool.arch", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.nodePool.autoScaling": { + FieldPath: "spec.nodePool.autoScaling", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.nodePool.clusterName": { + FieldPath: "spec.nodePool.clusterName", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.nodePool.config": { + FieldPath: "spec.nodePool.config", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.nodePool.management": { + FieldPath: "spec.nodePool.management", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.nodePool.nodeDrainTimeout": { + FieldPath: "spec.nodePool.nodeDrainTimeout", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.nodePool.nodeLabels": { + FieldPath: "spec.nodePool.nodeLabels", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.nodePool.nodeVolumeDetachTimeout": { + FieldPath: "spec.nodePool.nodeVolumeDetachTimeout", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.nodePool.osImageStream": { + FieldPath: "spec.nodePool.osImageStream", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.nodePool.pausedUntil": { + FieldPath: "spec.nodePool.pausedUntil", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.nodePool.platform": { + FieldPath: "spec.nodePool.platform", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.nodePool.release": { + FieldPath: "spec.nodePool.release", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.nodePool.replicas": { + FieldPath: "spec.nodePool.replicas", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.nodePool.taints": { + FieldPath: "spec.nodePool.taints", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.nodePool.tuningConfig": { + FieldPath: "spec.nodePool.tuningConfig", + WriteMode: ServiceSet, + Hidden: true, + }, "spec.properties": { FieldPath: "spec.properties", WriteMode: Mutable, @@ -542,11 +582,6 @@ var FieldRegistry = map[string]FieldMeta{ WriteMode: Mutable, FeatureGate: "HyperFleetAutoScaling", }, - "sshKey": { - FieldPath: "sshKey", - WriteMode: ServiceSet, - Hidden: true, - }, "streamingConnectionIdleTimeout": { FieldPath: "streamingConnectionIdleTimeout", WriteMode: Mutable, @@ -555,16 +590,6 @@ var FieldRegistry = map[string]FieldMeta{ FieldPath: "systemReserved", WriteMode: Immutable, }, - "taints": { - FieldPath: "taints", - WriteMode: ServiceSet, - Hidden: true, - }, - "tolerations": { - FieldPath: "tolerations", - WriteMode: ServiceSet, - Hidden: true, - }, "topologyManagerPolicy": { FieldPath: "topologyManagerPolicy", WriteMode: ServiceSet, @@ -575,14 +600,4 @@ var FieldRegistry = map[string]FieldMeta{ WriteMode: ServiceSet, Hidden: true, }, - "tuningConfig": { - FieldPath: "tuningConfig", - WriteMode: ServiceSet, - Hidden: true, - }, - "updateService": { - FieldPath: "updateService", - WriteMode: ServiceSet, - Hidden: true, - }, } diff --git a/platform-api/internal/codegen/registry/field_metadata.json b/platform-api/internal/codegen/registry/field_metadata.json index ac3e3592..62347632 100644 --- a/platform-api/internal/codegen/registry/field_metadata.json +++ b/platform-api/internal/codegen/registry/field_metadata.json @@ -1,9 +1,4 @@ [ - { - "fieldPath": "additionalTrustBundle", - "writeMode": "service-set", - "hidden": true - }, { "fieldPath": "allowedUnsafeSysctls", "writeMode": "service-set", @@ -14,63 +9,11 @@ "writeMode": "service-set", "hidden": true }, - { - "fieldPath": "arch", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "auditWebhook", - "writeMode": "service-set", - "hidden": true - }, { "fieldPath": "authentication", "writeMode": "service-set", "hidden": true }, - { - "fieldPath": "autoNode", - "writeMode": "service-set" - }, - { - "fieldPath": "autoScaling", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "autoscaling", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "capabilities", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "channel", - "writeMode": "service-set" - }, - { - "fieldPath": "clusterID", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "clusterName", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "config", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "configuration", - "writeMode": "service-set" - }, { "fieldPath": "containerLogMaxFiles", "writeMode": "mutable" @@ -79,16 +22,6 @@ "fieldPath": "containerLogMaxSize", "writeMode": "mutable" }, - { - "fieldPath": "controlPlaneRelease", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "controllerAvailabilityPolicy", - "writeMode": "service-set", - "hidden": true - }, { "fieldPath": "cpuManagerPolicy", "writeMode": "service-set", @@ -104,16 +37,6 @@ "writeMode": "service-set", "hidden": true }, - { - "fieldPath": "dns", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "etcd", - "writeMode": "service-set", - "hidden": true - }, { "fieldPath": "evictionHard", "writeMode": "service-set", @@ -134,20 +57,11 @@ "writeMode": "service-set", "hidden": true }, - { - "fieldPath": "fips", - "writeMode": "service-set" - }, { "fieldPath": "image", "writeMode": "service-set", "hidden": true }, - { - "fieldPath": "imageContentSources", - "writeMode": "service-set", - "hidden": true - }, { "fieldPath": "imageGCHighThresholdPercent", "writeMode": "mutable" @@ -160,31 +74,11 @@ "fieldPath": "imageMinimumGCAge", "writeMode": "mutable" }, - { - "fieldPath": "infraID", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "infrastructureAvailabilityPolicy", - "writeMode": "service-set", - "hidden": true - }, { "fieldPath": "ingress", "writeMode": "service-set", "hidden": true }, - { - "fieldPath": "issuerURL", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "kubeAPIServerDNSName", - "writeMode": "service-set", - "hidden": true - }, { "fieldPath": "kubeReserved", "writeMode": "immutable" @@ -298,11 +192,6 @@ "writeMode": "service-set", "hidden": true }, - { - "fieldPath": "labels", - "writeMode": "service-set", - "hidden": true - }, { "fieldPath": "machineConfig", "writeMode": "service-set" @@ -341,11 +230,6 @@ "writeMode": "service-set", "hidden": true }, - { - "fieldPath": "management", - "writeMode": "service-set", - "hidden": true - }, { "fieldPath": "maxPods", "writeMode": "mutable" @@ -361,142 +245,223 @@ "hidden": true }, { - "fieldPath": "networking", + "fieldPath": "oauth", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "nodeDrainTimeout", + "fieldPath": "podPidsLimit", + "writeMode": "mutable" + }, + { + "fieldPath": "proxy", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "nodeLabels", + "fieldPath": "registryBurst", + "writeMode": "mutable", + "featureGate": "HyperFleetKubeletAdvanced" + }, + { + "fieldPath": "registryPullQPS", + "writeMode": "mutable", + "featureGate": "HyperFleetKubeletAdvanced" + }, + { + "fieldPath": "scheduler", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "nodeSelector", + "fieldPath": "serializeImagePulls", + "writeMode": "mutable", + "featureGate": "HyperFleetKubeletAdvanced" + }, + { + "fieldPath": "spec.accountId", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "nodeVolumeDetachTimeout", + "fieldPath": "spec.autoRepair", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.creatorARN", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "oauth", + "fieldPath": "spec.deleteProtection", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.displayName", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.expirationTimestamp", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.hostedCluster.additionalTrustBundle", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "olmCatalogPlacement", + "fieldPath": "spec.hostedCluster.auditWebhook", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "operatorConfiguration", + "fieldPath": "spec.hostedCluster.autoNode", "writeMode": "service-set" }, { - "fieldPath": "osImageStream", + "fieldPath": "spec.hostedCluster.autoscaling", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "pausedUntil", + "fieldPath": "spec.hostedCluster.capabilities", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.channel", "writeMode": "service-set" }, { - "fieldPath": "platform", + "fieldPath": "spec.hostedCluster.clusterID", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "podPidsLimit", - "writeMode": "mutable" + "fieldPath": "spec.hostedCluster.configuration", + "writeMode": "service-set" }, { - "fieldPath": "proxy", + "fieldPath": "spec.hostedCluster.controlPlaneRelease", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "pullSecret", + "fieldPath": "spec.hostedCluster.controllerAvailabilityPolicy", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "registryBurst", - "writeMode": "mutable", - "featureGate": "HyperFleetKubeletAdvanced" + "fieldPath": "spec.hostedCluster.dns", + "writeMode": "service-set", + "hidden": true }, { - "fieldPath": "registryPullQPS", - "writeMode": "mutable", - "featureGate": "HyperFleetKubeletAdvanced" + "fieldPath": "spec.hostedCluster.etcd", + "writeMode": "service-set", + "hidden": true }, { - "fieldPath": "release", + "fieldPath": "spec.hostedCluster.fips", + "writeMode": "service-set" + }, + { + "fieldPath": "spec.hostedCluster.imageContentSources", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "replicas", + "fieldPath": "spec.hostedCluster.infraID", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "scheduler", + "fieldPath": "spec.hostedCluster.infrastructureAvailabilityPolicy", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "secretEncryption", + "fieldPath": "spec.hostedCluster.issuerURL", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "serializeImagePulls", - "writeMode": "mutable", - "featureGate": "HyperFleetKubeletAdvanced" + "fieldPath": "spec.hostedCluster.kubeAPIServerDNSName", + "writeMode": "service-set", + "hidden": true }, { - "fieldPath": "serviceAccountSigningKey", + "fieldPath": "spec.hostedCluster.labels", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "services", + "fieldPath": "spec.hostedCluster.networking", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "spec.accountId", + "fieldPath": "spec.hostedCluster.nodeSelector", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "spec.autoRepair", - "writeMode": "mutable" + "fieldPath": "spec.hostedCluster.olmCatalogPlacement", + "writeMode": "service-set", + "hidden": true }, { - "fieldPath": "spec.creatorARN", + "fieldPath": "spec.hostedCluster.operatorConfiguration", + "writeMode": "service-set" + }, + { + "fieldPath": "spec.hostedCluster.pausedUntil", + "writeMode": "service-set" + }, + { + "fieldPath": "spec.hostedCluster.platform", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "spec.deleteProtection", - "writeMode": "mutable" + "fieldPath": "spec.hostedCluster.pullSecret", + "writeMode": "service-set", + "hidden": true }, { - "fieldPath": "spec.displayName", - "writeMode": "mutable" + "fieldPath": "spec.hostedCluster.release", + "writeMode": "service-set", + "hidden": true }, { - "fieldPath": "spec.expirationTimestamp", - "writeMode": "mutable" + "fieldPath": "spec.hostedCluster.secretEncryption", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.serviceAccountSigningKey", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.services", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.sshKey", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.tolerations", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.updateService", + "writeMode": "service-set", + "hidden": true }, { "fieldPath": "spec.internalId", @@ -513,54 +478,104 @@ "writeMode": "mutable" }, { - "fieldPath": "spec.properties", - "writeMode": "mutable" + "fieldPath": "spec.nodePool.arch", + "writeMode": "service-set", + "hidden": true }, { - "fieldPath": "spec.tags", - "writeMode": "mutable", - "featureGate": "HyperFleetAutoScaling" + "fieldPath": "spec.nodePool.autoScaling", + "writeMode": "service-set", + "hidden": true }, { - "fieldPath": "sshKey", + "fieldPath": "spec.nodePool.clusterName", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "streamingConnectionIdleTimeout", - "writeMode": "mutable" + "fieldPath": "spec.nodePool.config", + "writeMode": "service-set", + "hidden": true }, { - "fieldPath": "systemReserved", - "writeMode": "immutable" + "fieldPath": "spec.nodePool.management", + "writeMode": "service-set", + "hidden": true }, { - "fieldPath": "taints", + "fieldPath": "spec.nodePool.nodeDrainTimeout", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "tolerations", + "fieldPath": "spec.nodePool.nodeLabels", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "topologyManagerPolicy", + "fieldPath": "spec.nodePool.nodeVolumeDetachTimeout", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "topologyManagerScope", + "fieldPath": "spec.nodePool.osImageStream", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.nodePool.pausedUntil", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.nodePool.platform", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.nodePool.release", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "tuningConfig", + "fieldPath": "spec.nodePool.replicas", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "updateService", + "fieldPath": "spec.nodePool.taints", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.nodePool.tuningConfig", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.properties", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.tags", + "writeMode": "mutable", + "featureGate": "HyperFleetAutoScaling" + }, + { + "fieldPath": "streamingConnectionIdleTimeout", + "writeMode": "mutable" + }, + { + "fieldPath": "systemReserved", + "writeMode": "immutable" + }, + { + "fieldPath": "topologyManagerPolicy", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "topologyManagerScope", "writeMode": "service-set", "hidden": true } From c73b04696c7264dc1ba7f7c6fd96492529177638 Mon Sep 17 00:00:00 2001 From: Chris Doan Date: Tue, 4 Aug 2026 10:35:01 -0700 Subject: [PATCH 3/7] ROSAENG-61802: add field validation middleware using codegen registry Wire the generated FieldRegistry into cluster and nodepool handlers to enforce write-mode and feature-gate rules on create/update requests. - Service-set fields rejected if sent by customers (422) - Immutable fields rejected on update if changed (422) - Feature-gated fields rejected without the gate enabled (422) - Validation errors returned with per-field detail Co-Authored-By: Claude Opus 4.6 --- platform-api/pkg/handlers/cluster.go | 26 +++ platform-api/pkg/handlers/nodepool.go | 34 ++- .../pkg/validation/field_validator.go | 189 ++++++++++++++++ .../pkg/validation/field_validator_test.go | 210 ++++++++++++++++++ 4 files changed, 455 insertions(+), 4 deletions(-) create mode 100644 platform-api/pkg/validation/field_validator.go create mode 100644 platform-api/pkg/validation/field_validator_test.go diff --git a/platform-api/pkg/handlers/cluster.go b/platform-api/pkg/handlers/cluster.go index 1eb90e9c..310a2c62 100644 --- a/platform-api/pkg/handlers/cluster.go +++ b/platform-api/pkg/handlers/cluster.go @@ -12,9 +12,11 @@ import ( "github.com/gorilla/mux" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/openshift-online/rosa-hyperfleet-api/platform-api/internal/codegen/featuregate" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/clients/hyperfleetdb" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/middleware" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/types" + "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/validation" ) // ClusterHandler handles cluster-related HTTP requests @@ -22,6 +24,7 @@ type ClusterHandler struct { db *hyperfleetdb.Client oidcIssuerBaseURL string defaultClusterExpiration time.Duration + validator *validation.FieldValidator logger *slog.Logger } @@ -31,6 +34,7 @@ func NewClusterHandler(db *hyperfleetdb.Client, oidcIssuerBaseURL string, defaul db: db, oidcIssuerBaseURL: oidcIssuerBaseURL, defaultClusterExpiration: defaultClusterExpiration, + validator: validation.NewFieldValidator(), logger: logger, } } @@ -108,6 +112,11 @@ func (h *ClusterHandler) Create(w http.ResponseWriter, r *http.Request) { return } + if errs := h.validator.ValidateCreate(req.Spec, featuregate.Default); errs != nil { + h.writeValidationErrors(w, errs) + return + } + existing, err := h.db.ListClusters(ctx, accountID) if err != nil { h.logger.Error("failed to check cluster name uniqueness", "error", err, "account_id", accountID) @@ -214,6 +223,11 @@ func (h *ClusterHandler) Update(w http.ResponseWriter, r *http.Request) { return } + if errs := h.validator.ValidateUpdate(req.Spec, &cr.Spec, featuregate.Default); errs != nil { + h.writeValidationErrors(w, errs) + return + } + existingIssuerURL := cr.Spec.HostedCluster.IssuerURL existingExpiration := cr.Spec.ExpirationTimestamp @@ -295,6 +309,18 @@ func (h *ClusterHandler) writeJSON(w http.ResponseWriter, status int, data any) _ = json.NewEncoder(w).Encode(data) } +func (h *ClusterHandler) writeValidationErrors(w http.ResponseWriter, errs validation.ValidationErrors) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnprocessableEntity) + resp := map[string]any{ + "kind": "Error", + "code": "CLUSTERS-MGMT-VALIDATION-001", + "reason": "Request validation failed", + "errors": errs, + } + _ = json.NewEncoder(w).Encode(resp) +} + func (h *ClusterHandler) writeError(w http.ResponseWriter, status int, code, reason string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) diff --git a/platform-api/pkg/handlers/nodepool.go b/platform-api/pkg/handlers/nodepool.go index d2c7bf14..38d93951 100644 --- a/platform-api/pkg/handlers/nodepool.go +++ b/platform-api/pkg/handlers/nodepool.go @@ -7,20 +7,24 @@ import ( "strconv" "github.com/gorilla/mux" + "github.com/openshift-online/rosa-hyperfleet-api/platform-api/internal/codegen/featuregate" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/clients/hyperfleetdb" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/middleware" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/types" + "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/validation" ) type NodePoolHandler struct { - db *hyperfleetdb.Client - logger *slog.Logger + db *hyperfleetdb.Client + validator *validation.FieldValidator + logger *slog.Logger } func NewNodePoolHandler(db *hyperfleetdb.Client, logger *slog.Logger) *NodePoolHandler { return &NodePoolHandler{ - db: db, - logger: logger, + db: db, + validator: validation.NewFieldValidator(), + logger: logger, } } @@ -95,6 +99,11 @@ func (h *NodePoolHandler) Create(w http.ResponseWriter, r *http.Request) { return } + if errs := h.validator.ValidateCreate(req.Spec, featuregate.Default); errs != nil { + h.writeValidationErrors(w, errs) + return + } + if _, err := h.db.GetCluster(ctx, accountID, req.ClusterID); err != nil { if hyperfleetdb.IsNotFound(err) { h.writeError(w, http.StatusNotFound, "NODEPOOLS-MGMT-CREATE-004", "Referenced cluster not found") @@ -179,6 +188,11 @@ func (h *NodePoolHandler) Update(w http.ResponseWriter, r *http.Request) { return } + if errs := h.validator.ValidateUpdate(req.Spec, &cr.Spec, featuregate.Default); errs != nil { + h.writeValidationErrors(w, errs) + return + } + if err := hyperfleetdb.ApplyPlatformUpdateToNodePoolCR(cr, &req); err != nil { h.logger.Error("failed to merge nodepool spec", "error", err) h.writeError(w, http.StatusBadRequest, "NODEPOOLS-MGMT-UPDATE-002", "Invalid nodepool spec") @@ -249,6 +263,18 @@ func (h *NodePoolHandler) writeJSON(w http.ResponseWriter, status int, data any) _ = json.NewEncoder(w).Encode(data) } +func (h *NodePoolHandler) writeValidationErrors(w http.ResponseWriter, errs validation.ValidationErrors) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnprocessableEntity) + resp := map[string]any{ + "kind": "Error", + "code": "NODEPOOLS-MGMT-VALIDATION-001", + "reason": "Request validation failed", + "errors": errs, + } + _ = json.NewEncoder(w).Encode(resp) +} + func (h *NodePoolHandler) writeError(w http.ResponseWriter, status int, code, reason string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) diff --git a/platform-api/pkg/validation/field_validator.go b/platform-api/pkg/validation/field_validator.go new file mode 100644 index 00000000..b8961d49 --- /dev/null +++ b/platform-api/pkg/validation/field_validator.go @@ -0,0 +1,189 @@ +package validation + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + + "github.com/openshift-online/rosa-hyperfleet-api/platform-api/internal/codegen/featuregate" + "github.com/openshift-online/rosa-hyperfleet-api/platform-api/internal/codegen/registry" +) + +type Operation string + +const ( + OperationCreate Operation = "create" + OperationUpdate Operation = "update" +) + +type ValidationError struct { + Field string `json:"field"` + Reason string `json:"reason"` +} + +func (e *ValidationError) Error() string { + return fmt.Sprintf("field %s: %s", e.Field, e.Reason) +} + +type ValidationErrors []*ValidationError + +func (e ValidationErrors) Error() string { + if len(e) == 0 { + return "no validation errors" + } + var sb strings.Builder + sb.WriteString("validation failed:\n") + for _, err := range e { + sb.WriteString(" ") + sb.WriteString(err.Error()) + sb.WriteString("\n") + } + return sb.String() +} + +type FieldValidator struct { + registry map[string]registry.FieldMeta +} + +func NewFieldValidator() *FieldValidator { + return &FieldValidator{ + registry: registry.FieldRegistry, + } +} + +// ValidateCreate checks that a create request does not set service-set or +// feature-gated fields. The spec is JSON-roundtripped to extract field paths. +func (v *FieldValidator) ValidateCreate(spec any, fs featuregate.FeatureSet) ValidationErrors { + if spec == nil { + return nil + } + fields := flattenToFieldPaths(spec) + return v.validate(fields, nil, OperationCreate, fs) +} + +// ValidateUpdate checks that an update request does not set service-set fields, +// change immutable fields, or use feature-gated fields without the gate enabled. +func (v *FieldValidator) ValidateUpdate(newSpec, existingSpec any, fs featuregate.FeatureSet) ValidationErrors { + if newSpec == nil { + return nil + } + newFields := flattenToFieldPaths(newSpec) + var existingFields map[string]any + if existingSpec != nil { + existingFields = flattenToFieldPaths(existingSpec) + } + return v.validate(newFields, existingFields, OperationUpdate, fs) +} + +func (v *FieldValidator) validate(fields, existingFields map[string]any, op Operation, fs featuregate.FeatureSet) ValidationErrors { + var errs ValidationErrors + + for fieldPath := range fields { + meta, exists := v.registry[fieldPath] + if !exists { + continue + } + + if meta.FeatureGate != "" { + if !featuregate.IsGateEnabled(meta.FeatureGate, fs) { + errs = append(errs, &ValidationError{ + Field: fieldPath, + Reason: fmt.Sprintf("requires feature gate %s which is not enabled in %s feature set", meta.FeatureGate, fs), + }) + continue + } + } + + if err := v.validateWriteMode(fieldPath, meta, op, fields, existingFields, fs); err != nil { + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return errs + } + return nil +} + +func (v *FieldValidator) validateWriteMode(fieldPath string, meta registry.FieldMeta, op Operation, fields, existingFields map[string]any, fs featuregate.FeatureSet) *ValidationError { + effectiveMode := meta.WriteMode + + if len(meta.FeatureGateAwareWriteModes) > 0 { + matched := false + for _, override := range meta.FeatureGateAwareWriteModes { + if override.FeatureGate != "" && featuregate.IsGateEnabled(override.FeatureGate, fs) { + effectiveMode = override.WriteMode + matched = true + break + } + } + if !matched { + for _, override := range meta.FeatureGateAwareWriteModes { + if override.FeatureGate == "" { + effectiveMode = override.WriteMode + break + } + } + } + } + + switch effectiveMode { + case registry.ServiceSet: + return &ValidationError{ + Field: fieldPath, + Reason: "field is platform-managed and cannot be set by customers", + } + case registry.Immutable: + if op == OperationUpdate && existingFields != nil { + oldVal, existsInOld := existingFields[fieldPath] + if existsInOld { + newVal := fields[fieldPath] + if !reflect.DeepEqual(oldVal, newVal) { + return &ValidationError{ + Field: fieldPath, + Reason: "field is immutable and cannot be changed after creation", + } + } + } + } + return nil + case registry.Mutable: + return nil + default: + return nil + } +} + +func flattenToFieldPaths(v any) map[string]any { + data, err := json.Marshal(v) + if err != nil { + return nil + } + + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + return nil + } + + result := make(map[string]any) + flattenMap("spec", m, result) + return result +} + +func flattenMap(prefix string, m map[string]any, result map[string]any) { + for key, val := range m { + var path string + if prefix == "" { + path = key + } else { + path = prefix + "." + key + } + + result[path] = val + + if nested, ok := val.(map[string]any); ok { + flattenMap(path, nested, result) + } + } +} diff --git a/platform-api/pkg/validation/field_validator_test.go b/platform-api/pkg/validation/field_validator_test.go new file mode 100644 index 00000000..31a76eb5 --- /dev/null +++ b/platform-api/pkg/validation/field_validator_test.go @@ -0,0 +1,210 @@ +package validation + +import ( + "testing" + + "github.com/openshift-online/rosa-hyperfleet-api/platform-api/internal/codegen/featuregate" + "github.com/openshift-online/rosa-hyperfleet-api/platform-api/internal/codegen/registry" +) + +func newTestValidator(entries map[string]registry.FieldMeta) *FieldValidator { + return &FieldValidator{registry: entries} +} + +func TestValidateCreate_RejectsServiceSetFields(t *testing.T) { + v := newTestValidator(map[string]registry.FieldMeta{ + "spec.accountId": {FieldPath: "spec.accountId", WriteMode: registry.ServiceSet}, + "spec.name": {FieldPath: "spec.name", WriteMode: registry.Mutable}, + }) + + spec := map[string]any{ + "accountId": "123", + "name": "my-cluster", + } + + errs := v.ValidateCreate(spec, featuregate.Default) + if errs == nil { + t.Fatal("expected validation errors, got nil") + } + + found := false + for _, e := range errs { + if e.Field == "spec.accountId" { + found = true + } + if e.Field == "spec.name" { + t.Error("mutable field 'spec.name' should not be rejected") + } + } + if !found { + t.Error("expected error for service-set field 'spec.accountId'") + } +} + +func TestValidateCreate_AllowsMutableFields(t *testing.T) { + v := newTestValidator(map[string]registry.FieldMeta{ + "spec.displayName": {FieldPath: "spec.displayName", WriteMode: registry.Mutable}, + }) + + spec := map[string]any{ + "displayName": "test", + } + + errs := v.ValidateCreate(spec, featuregate.Default) + if errs != nil { + t.Errorf("expected no errors, got %v", errs) + } +} + +func TestValidateCreate_AllowsImmutableFields(t *testing.T) { + v := newTestValidator(map[string]registry.FieldMeta{ + "spec.fips": {FieldPath: "spec.fips", WriteMode: registry.Immutable}, + }) + + spec := map[string]any{ + "fips": true, + } + + errs := v.ValidateCreate(spec, featuregate.Default) + if errs != nil { + t.Errorf("expected no errors on create for immutable field, got %v", errs) + } +} + +func TestValidateUpdate_RejectsImmutableFieldChange(t *testing.T) { + v := newTestValidator(map[string]registry.FieldMeta{ + "spec.fips": {FieldPath: "spec.fips", WriteMode: registry.Immutable}, + }) + + existing := map[string]any{"fips": true} + updated := map[string]any{"fips": false} + + errs := v.ValidateUpdate(updated, existing, featuregate.Default) + if errs == nil { + t.Fatal("expected validation error for immutable field change") + } + + if errs[0].Field != "spec.fips" { + t.Errorf("expected error on spec.fips, got %s", errs[0].Field) + } +} + +func TestValidateUpdate_AllowsImmutableFieldSameValue(t *testing.T) { + v := newTestValidator(map[string]registry.FieldMeta{ + "spec.fips": {FieldPath: "spec.fips", WriteMode: registry.Immutable}, + }) + + existing := map[string]any{"fips": true} + updated := map[string]any{"fips": true} + + errs := v.ValidateUpdate(updated, existing, featuregate.Default) + if errs != nil { + t.Errorf("expected no errors when immutable field unchanged, got %v", errs) + } +} + +func TestValidateCreate_RejectsFeatureGatedField(t *testing.T) { + v := newTestValidator(map[string]registry.FieldMeta{ + "spec.tags": {FieldPath: "spec.tags", WriteMode: registry.Mutable, FeatureGate: "HyperFleetAutoScaling"}, + }) + + spec := map[string]any{ + "tags": map[string]string{"env": "prod"}, + } + + errs := v.ValidateCreate(spec, featuregate.Default) + if errs == nil { + t.Fatal("expected validation error for feature-gated field") + } + + if errs[0].Field != "spec.tags" { + t.Errorf("expected error on spec.tags, got %s", errs[0].Field) + } +} + +func TestValidateCreate_AllowsFeatureGatedFieldWhenEnabled(t *testing.T) { + v := newTestValidator(map[string]registry.FieldMeta{ + "spec.tags": {FieldPath: "spec.tags", WriteMode: registry.Mutable, FeatureGate: "HyperFleetAutoScaling"}, + }) + + spec := map[string]any{ + "tags": map[string]string{"env": "prod"}, + } + + errs := v.ValidateCreate(spec, featuregate.TechPreviewNoUpgrade) + if errs != nil { + t.Errorf("expected no errors with gate enabled, got %v", errs) + } +} + +func TestValidateCreate_NilSpecReturnsNil(t *testing.T) { + v := newTestValidator(map[string]registry.FieldMeta{}) + errs := v.ValidateCreate(nil, featuregate.Default) + if errs != nil { + t.Errorf("expected nil for nil spec, got %v", errs) + } +} + +func TestValidateUpdate_RejectsServiceSetFields(t *testing.T) { + v := newTestValidator(map[string]registry.FieldMeta{ + "spec.creatorARN": {FieldPath: "spec.creatorARN", WriteMode: registry.ServiceSet}, + }) + + existing := map[string]any{"creatorARN": "old-arn"} + updated := map[string]any{"creatorARN": "new-arn"} + + errs := v.ValidateUpdate(updated, existing, featuregate.Default) + if errs == nil { + t.Fatal("expected validation error for service-set field on update") + } +} + +func TestFlattenToFieldPaths(t *testing.T) { + spec := map[string]any{ + "name": "test", + "hostedCluster": map[string]any{ + "fips": true, + "channel": "stable-4.16", + }, + } + + result := flattenToFieldPaths(spec) + + expected := []string{ + "spec.name", + "spec.hostedCluster", + "spec.hostedCluster.fips", + "spec.hostedCluster.channel", + } + + for _, path := range expected { + if _, found := result[path]; !found { + t.Errorf("expected path %s in result, not found", path) + } + } +} + +func TestValidateCreate_FeatureGateAwareWriteMode(t *testing.T) { + v := newTestValidator(map[string]registry.FieldMeta{ + "spec.maxPods": { + FieldPath: "spec.maxPods", + WriteMode: registry.ServiceSet, + FeatureGateAwareWriteModes: []registry.FeatureGateWriteMode{ + {FeatureGate: "HyperFleetKubeletAdvanced", WriteMode: registry.Mutable}, + {FeatureGate: "", WriteMode: registry.ServiceSet}, + }, + }, + }) + + spec := map[string]any{"maxPods": 110} + + errs := v.ValidateCreate(spec, featuregate.Default) + if errs == nil { + t.Fatal("expected error without feature gate") + } + + errs = v.ValidateCreate(spec, featuregate.TechPreviewNoUpgrade) + if errs != nil { + t.Errorf("expected no errors with gate enabled, got %v", errs) + } +} From 5214a978eb69f55f7bbb7ae1cd47ff55db4f401c Mon Sep 17 00:00:00 2001 From: Chris Doan Date: Tue, 4 Aug 2026 20:47:42 -0700 Subject: [PATCH 4/7] ROSAENG-61802: skip service-set validation for zero-value fields Go structs without omitempty serialize zero values to JSON, causing the field validator to reject fields the client never explicitly set. Skip ServiceSet enforcement when the field value is at its zero value. Co-Authored-By: Claude Opus 4.6 --- .../pkg/validation/field_validator.go | 31 +++++++++++++ .../pkg/validation/field_validator_test.go | 43 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/platform-api/pkg/validation/field_validator.go b/platform-api/pkg/validation/field_validator.go index b8961d49..fa47ba84 100644 --- a/platform-api/pkg/validation/field_validator.go +++ b/platform-api/pkg/validation/field_validator.go @@ -130,6 +130,9 @@ func (v *FieldValidator) validateWriteMode(fieldPath string, meta registry.Field switch effectiveMode { case registry.ServiceSet: + if isZeroValue(fields[fieldPath]) { + return nil + } return &ValidationError{ Field: fieldPath, Reason: "field is platform-managed and cannot be set by customers", @@ -171,6 +174,34 @@ func flattenToFieldPaths(v any) map[string]any { return result } +// isZeroValue returns true when v is a JSON-deserialized zero value. +// After json.Unmarshal into map[string]any, Go zero values appear as: +// nil (null slices/pointers), "" (strings), false (bools), 0.0 (numbers), +// and empty maps (zero-value structs). +func isZeroValue(v any) bool { + if v == nil { + return true + } + switch val := v.(type) { + case string: + return val == "" + case bool: + return !val + case float64: + return val == 0 + case map[string]any: + for _, child := range val { + if !isZeroValue(child) { + return false + } + } + return true + case []any: + return len(val) == 0 + } + return false +} + func flattenMap(prefix string, m map[string]any, result map[string]any) { for key, val := range m { var path string diff --git a/platform-api/pkg/validation/field_validator_test.go b/platform-api/pkg/validation/field_validator_test.go index 31a76eb5..69294a7c 100644 --- a/platform-api/pkg/validation/field_validator_test.go +++ b/platform-api/pkg/validation/field_validator_test.go @@ -41,6 +41,49 @@ func TestValidateCreate_RejectsServiceSetFields(t *testing.T) { } } +func TestValidateCreate_SkipsServiceSetFieldsAtZeroValue(t *testing.T) { + v := newTestValidator(map[string]registry.FieldMeta{ + "spec.networking": {FieldPath: "spec.networking", WriteMode: registry.ServiceSet}, + "spec.etcd": {FieldPath: "spec.etcd", WriteMode: registry.ServiceSet}, + "spec.fips": {FieldPath: "spec.fips", WriteMode: registry.ServiceSet}, + "spec.services": {FieldPath: "spec.services", WriteMode: registry.ServiceSet}, + "spec.pullSecret": {FieldPath: "spec.pullSecret", WriteMode: registry.ServiceSet}, + }) + + spec := map[string]any{ + "networking": map[string]any{}, + "etcd": map[string]any{"managementType": "", "managed": map[string]any{}}, + "fips": false, + "services": nil, + "pullSecret": map[string]any{"name": ""}, + } + + errs := v.ValidateCreate(spec, featuregate.Default) + if errs != nil { + t.Errorf("expected no errors for zero-value service-set fields, got %v", errs) + } +} + +func TestValidateCreate_RejectsNonZeroServiceSetFields(t *testing.T) { + v := newTestValidator(map[string]registry.FieldMeta{ + "spec.networking": {FieldPath: "spec.networking", WriteMode: registry.ServiceSet}, + "spec.fips": {FieldPath: "spec.fips", WriteMode: registry.ServiceSet}, + }) + + spec := map[string]any{ + "networking": map[string]any{"clusterNetwork": []any{map[string]any{"cidr": "10.0.0.0/8"}}}, + "fips": true, + } + + errs := v.ValidateCreate(spec, featuregate.Default) + if errs == nil { + t.Fatal("expected validation errors for non-zero service-set fields, got nil") + } + if len(errs) != 2 { + t.Errorf("expected 2 errors, got %d: %v", len(errs), errs) + } +} + func TestValidateCreate_AllowsMutableFields(t *testing.T) { v := newTestValidator(map[string]registry.FieldMeta{ "spec.displayName": {FieldPath: "spec.displayName", WriteMode: registry.Mutable}, From 8f689792f595c3c26f03b09a7fce375b35c9f069 Mon Sep 17 00:00:00 2001 From: Chris Doan Date: Tue, 4 Aug 2026 12:29:26 -0700 Subject: [PATCH 5/7] ROSAENG-61803: preserve service-set fields on update - Add AccountID, InternalID to internal ClusterSpec; AccountID, InternalPoolID to internal NodePoolSpec - Restore all service-set fields after full spec replacement in cluster and nodepool update handlers - Add verbose mode to marker-scanner (make codegen VERBOSE=1) Co-Authored-By: Claude Opus 4.6 --- Makefile | 3 ++- hack/api-codegen/cmd/marker-scanner/main.go | 2 +- .../pkg/markers/gated_writemode_test.go | 2 +- hack/api-codegen/pkg/markers/scanner.go | 18 ++++++++++++++++-- hack/api-codegen/pkg/markers/scanner_test.go | 6 +++--- hack/api-codegen/pkg/markers/types.go | 3 +++ .../api/v1alpha1/cluster_types.go | 8 ++++++++ .../api/v1alpha1/nodepool_types.go | 8 ++++++++ .../crd/bases/hyperfleet.io_clusters.yaml | 8 ++++++++ .../crd/bases/hyperfleet.io_nodepools.yaml | 8 ++++++++ platform-api/pkg/handlers/cluster.go | 11 +++++++---- platform-api/pkg/handlers/nodepool.go | 6 ++++++ 12 files changed, 71 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 53037923..7b96c398 100644 --- a/Makefile +++ b/Makefile @@ -340,7 +340,8 @@ generate-public-deepcopy: codegen-passthrough $(CONTROLLER_GEN) codegen-registry: generate-public-deepcopy build-api-codegen ./bin/marker-scanner \ -input-dirs api/public/v2alpha1 \ - -output-file platform-api/internal/codegen/registry/field_metadata.go + -output-file platform-api/internal/codegen/registry/field_metadata.go \ + $(if $(VERBOSE),-verbose) codegen-verify: codegen-registry cd api/public/v2alpha1 && go build ./... diff --git a/hack/api-codegen/cmd/marker-scanner/main.go b/hack/api-codegen/cmd/marker-scanner/main.go index 8d59be3f..6af0133d 100644 --- a/hack/api-codegen/cmd/marker-scanner/main.go +++ b/hack/api-codegen/cmd/marker-scanner/main.go @@ -37,7 +37,7 @@ func main() { } // Create scanner and scan directories - scanner := markers.NewScanner(dirs) + scanner := markers.NewScanner(dirs, verbose) log.Printf("Scanning directories: %v", dirs) if err := scanner.Scan(); err != nil { diff --git a/hack/api-codegen/pkg/markers/gated_writemode_test.go b/hack/api-codegen/pkg/markers/gated_writemode_test.go index cd3cfb9d..3033a566 100644 --- a/hack/api-codegen/pkg/markers/gated_writemode_test.go +++ b/hack/api-codegen/pkg/markers/gated_writemode_test.go @@ -38,7 +38,7 @@ type Spec struct { t.Fatalf("Failed to write test file: %v", err) } - scanner := NewScanner([]string{tmpDir}) + scanner := NewScanner([]string{tmpDir}, false) if err := scanner.Scan(); err != nil { t.Fatalf("Scan failed: %v", err) } diff --git a/hack/api-codegen/pkg/markers/scanner.go b/hack/api-codegen/pkg/markers/scanner.go index 9257fcfb..ac4045b8 100644 --- a/hack/api-codegen/pkg/markers/scanner.go +++ b/hack/api-codegen/pkg/markers/scanner.go @@ -18,22 +18,32 @@ var ( featureGateAwareWriteModePattern = regexp.MustCompile(`\+hyperfleet:validation:FeatureGateAwareWriteMode:featureGate="([^"]*)",writeMode="(mutable|immutable|service-set)"`) ) -// NewScanner creates a new marker scanner -func NewScanner(inputDirs []string) *MarkerScanner { +// NewScanner creates a new marker scanner. +// Pass verbose=true to log each type, field, and marker as the scanner processes them. +func NewScanner(inputDirs []string, verbose bool) *MarkerScanner { return &MarkerScanner{ InputDirs: inputDirs, Registry: make(FieldRegistry), typeCache: make(map[string]*ast.StructType), + verbose: verbose, + } +} + +func (s *MarkerScanner) logf(format string, args ...any) { + if s.verbose { + fmt.Fprintf(os.Stderr, "[scanner] "+format+"\n", args...) } } // Scan walks the input directories and extracts marker metadata func (s *MarkerScanner) Scan() error { for _, dir := range s.InputDirs { + s.logf("scanning directory: %s", dir) if err := s.scanDir(dir); err != nil { return fmt.Errorf("scanning directory %s: %w", dir, err) } } + s.logf("scan complete: %d fields in registry", len(s.Registry)) return nil } @@ -78,12 +88,15 @@ func (s *MarkerScanner) scanDir(dir string) error { // Install this directory's cache for nested-type resolution s.typeCache = dirCache + s.logf(" cached %d struct types", len(dirCache)) + // Second pass: process root types once with the full cache available for typeName, structType := range dirCache { if isRootType(typeName) { visited := make(map[string]bool) visited[typeName] = true prefix := rootTypePrefix(typeName) + s.logf(" root type: %s (prefix=%q)", typeName, prefix) s.processStruct(typeName, structType, prefix, visited) } } @@ -147,6 +160,7 @@ func (s *MarkerScanner) processField(field *ast.Field, parentPath string, visite // Extract markers from comments meta := s.extractMarkers(field, fieldPath) if meta != nil { + s.logf(" field: %s write-mode=%s hidden=%v gate=%s", fieldPath, meta.WriteMode, meta.Hidden, meta.FeatureGate) s.Registry[fieldPath] = *meta } diff --git a/hack/api-codegen/pkg/markers/scanner_test.go b/hack/api-codegen/pkg/markers/scanner_test.go index 6956b948..4f75d281 100644 --- a/hack/api-codegen/pkg/markers/scanner_test.go +++ b/hack/api-codegen/pkg/markers/scanner_test.go @@ -49,7 +49,7 @@ type EtcdSpec struct { } // Create scanner and scan - scanner := NewScanner([]string{tmpDir}) + scanner := NewScanner([]string{tmpDir}, false) if err := scanner.Scan(); err != nil { t.Fatalf("Scan failed: %v", err) } @@ -121,7 +121,7 @@ type NodePoolSpecPassthrough struct { t.Fatalf("Failed to write test file: %v", err) } - scanner := NewScanner([]string{tmpDir}) + scanner := NewScanner([]string{tmpDir}, false) if err := scanner.Scan(); err != nil { t.Fatalf("Scan failed: %v", err) } @@ -220,7 +220,7 @@ type Spec struct { t.Fatalf("Failed to write test file: %v", err) } - scanner := NewScanner([]string{tmpDir}) + scanner := NewScanner([]string{tmpDir}, false) if err := scanner.Scan(); err != nil { t.Fatalf("Scan failed: %v", err) } diff --git a/hack/api-codegen/pkg/markers/types.go b/hack/api-codegen/pkg/markers/types.go index 27dc4062..8f8309b0 100644 --- a/hack/api-codegen/pkg/markers/types.go +++ b/hack/api-codegen/pkg/markers/types.go @@ -57,4 +57,7 @@ type MarkerScanner struct { // typeCache maps type names to their struct definitions typeCache map[string]*ast.StructType + + // verbose enables detailed logging to stderr during scanning + verbose bool } diff --git a/hyperfleet-operator/api/v1alpha1/cluster_types.go b/hyperfleet-operator/api/v1alpha1/cluster_types.go index 3f3e7c60..47832142 100644 --- a/hyperfleet-operator/api/v1alpha1/cluster_types.go +++ b/hyperfleet-operator/api/v1alpha1/cluster_types.go @@ -37,6 +37,14 @@ const ( // metadata.Name is the human-readable cluster name; metadata.Namespace is the cluster UUID. // The owning AWS account is stored as the label hyperfleet.io/account-id. type ClusterSpec struct { + // AccountID is the AWS account ID that owns this cluster (platform-managed). + // +optional + AccountID string `json:"accountId,omitempty"` + + // InternalID is an internal platform identifier for this cluster (platform-managed). + // +optional + InternalID string `json:"internalId,omitempty"` + // CreatorARN is the IAM ARN of the user who created this cluster. // Used to bootstrap the initial cluster-admin RBAC mapping. // +optional diff --git a/hyperfleet-operator/api/v1alpha1/nodepool_types.go b/hyperfleet-operator/api/v1alpha1/nodepool_types.go index 133f3e7c..75f573e9 100644 --- a/hyperfleet-operator/api/v1alpha1/nodepool_types.go +++ b/hyperfleet-operator/api/v1alpha1/nodepool_types.go @@ -36,6 +36,14 @@ const ( // NodePoolSpec defines the desired state of a NodePool. // The parent Cluster is identified by the shared metadata.Namespace (cluster UUID). type NodePoolSpec struct { + // AccountID is the AWS account ID that owns this node pool (platform-managed). + // +optional + AccountID string `json:"accountId,omitempty"` + + // InternalPoolID is an internal platform identifier for this node pool (platform-managed). + // +optional + InternalPoolID string `json:"internalPoolId,omitempty"` + // NodePool is the full HyperShift NodePoolSpec. The customer provides replicas, // platform, release, etc. The operator overrides ClusterName and adds system // resource tags at render time. diff --git a/hyperfleet-operator/config/crd/bases/hyperfleet.io_clusters.yaml b/hyperfleet-operator/config/crd/bases/hyperfleet.io_clusters.yaml index 20750447..0003636d 100644 --- a/hyperfleet-operator/config/crd/bases/hyperfleet.io_clusters.yaml +++ b/hyperfleet-operator/config/crd/bases/hyperfleet.io_clusters.yaml @@ -66,6 +66,10 @@ spec: metadata.Name is the human-readable cluster name; metadata.Namespace is the cluster UUID. The owning AWS account is stored as the label hyperfleet.io/account-id. properties: + accountId: + description: AccountID is the AWS account ID that owns this cluster + (platform-managed). + type: string creatorARN: description: |- CreatorARN is the IAM ARN of the user who created this cluster. @@ -8572,6 +8576,10 @@ spec: !has(self.operatorConfiguration.clusterNetworkOperator.ovnKubernetesConfig) - message: secretEncryption cannot be removed once configured rule: '!has(oldSelf.secretEncryption) || has(self.secretEncryption)' + internalId: + description: InternalID is an internal platform identifier for this + cluster (platform-managed). + type: string required: - hostedCluster type: object diff --git a/hyperfleet-operator/config/crd/bases/hyperfleet.io_nodepools.yaml b/hyperfleet-operator/config/crd/bases/hyperfleet.io_nodepools.yaml index f96d8f9b..4df29edb 100644 --- a/hyperfleet-operator/config/crd/bases/hyperfleet.io_nodepools.yaml +++ b/hyperfleet-operator/config/crd/bases/hyperfleet.io_nodepools.yaml @@ -53,6 +53,14 @@ spec: NodePoolSpec defines the desired state of a NodePool. The parent Cluster is identified by the shared metadata.Namespace (cluster UUID). properties: + accountId: + description: AccountID is the AWS account ID that owns this node pool + (platform-managed). + type: string + internalPoolId: + description: InternalPoolID is an internal platform identifier for + this node pool (platform-managed). + type: string nodePool: description: |- NodePool is the full HyperShift NodePoolSpec. The customer provides replicas, diff --git a/platform-api/pkg/handlers/cluster.go b/platform-api/pkg/handlers/cluster.go index 310a2c62..21b21657 100644 --- a/platform-api/pkg/handlers/cluster.go +++ b/platform-api/pkg/handlers/cluster.go @@ -228,8 +228,7 @@ func (h *ClusterHandler) Update(w http.ResponseWriter, r *http.Request) { return } - existingIssuerURL := cr.Spec.HostedCluster.IssuerURL - existingExpiration := cr.Spec.ExpirationTimestamp + snapshot := cr.Spec if err := hyperfleetdb.ApplyPlatformUpdateToClusterCR(cr, &req); err != nil { h.logger.Error("failed to merge cluster spec", "error", err) @@ -237,9 +236,13 @@ func (h *ClusterHandler) Update(w http.ResponseWriter, r *http.Request) { return } - cr.Spec.HostedCluster.IssuerURL = existingIssuerURL + // Restore service-set fields wiped by the full spec replacement. + cr.Spec.AccountID = snapshot.AccountID + cr.Spec.InternalID = snapshot.InternalID + cr.Spec.CreatorARN = snapshot.CreatorARN + cr.Spec.HostedCluster.IssuerURL = snapshot.HostedCluster.IssuerURL if cr.Spec.ExpirationTimestamp == nil { - cr.Spec.ExpirationTimestamp = existingExpiration + cr.Spec.ExpirationTimestamp = snapshot.ExpirationTimestamp } if err := h.db.UpdateCluster(ctx, cr); err != nil { diff --git a/platform-api/pkg/handlers/nodepool.go b/platform-api/pkg/handlers/nodepool.go index 38d93951..4c222dbb 100644 --- a/platform-api/pkg/handlers/nodepool.go +++ b/platform-api/pkg/handlers/nodepool.go @@ -193,12 +193,18 @@ func (h *NodePoolHandler) Update(w http.ResponseWriter, r *http.Request) { return } + snapshot := cr.Spec + if err := hyperfleetdb.ApplyPlatformUpdateToNodePoolCR(cr, &req); err != nil { h.logger.Error("failed to merge nodepool spec", "error", err) h.writeError(w, http.StatusBadRequest, "NODEPOOLS-MGMT-UPDATE-002", "Invalid nodepool spec") return } + // Restore service-set fields wiped by the full spec replacement. + cr.Spec.AccountID = snapshot.AccountID + cr.Spec.InternalPoolID = snapshot.InternalPoolID + if err := h.db.UpdateNodePool(ctx, cr); err != nil { h.logger.Error("failed to update nodepool", "error", err, "account_id", accountID, "nodepool_id", nodepoolID) h.writeError(w, http.StatusInternalServerError, "NODEPOOLS-MGMT-UPDATE-004", "Failed to update nodepool") From 8f04f71bc1ab8e41fcc07cbd2dca3f17977e305a Mon Sep 17 00:00:00 2001 From: Chris Doan Date: Tue, 4 Aug 2026 16:39:12 -0700 Subject: [PATCH 6/7] ROSAENG-61803: populate service-set fields in create conversion functions PlatformCreateToClusterCR and PlatformCreateToNodePoolCR now set AccountID/InternalID/InternalPoolID from authoritative platform sources instead of relying on client-provided req.Spec values. Co-Authored-By: Claude Opus 4.6 --- .../pkg/clients/hyperfleetdb/convert.go | 6 +++++- .../pkg/clients/hyperfleetdb/convert_test.go | 18 +++++++++++++++++- platform-api/pkg/handlers/nodepool.go | 4 +++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/platform-api/pkg/clients/hyperfleetdb/convert.go b/platform-api/pkg/clients/hyperfleetdb/convert.go index 7c1c8377..f6d76b0f 100644 --- a/platform-api/pkg/clients/hyperfleetdb/convert.go +++ b/platform-api/pkg/clients/hyperfleetdb/convert.go @@ -75,6 +75,8 @@ func ClusterCRToPlatform(cr *hyperfleetv1alpha1.Cluster) *types.Cluster { // metadata.Name = human-readable cluster name. func PlatformCreateToClusterCR(clusterID, accountID string, req *types.ClusterCreateRequest) (*hyperfleetv1alpha1.Cluster, error) { spec := *req.Spec + spec.AccountID = accountID + spec.InternalID = clusterID return &hyperfleetv1alpha1.Cluster{ ObjectMeta: metav1.ObjectMeta{ @@ -147,8 +149,10 @@ func NodePoolCRToPlatform(cr *hyperfleetv1alpha1.NodePool) *types.NodePool { // PlatformCreateToNodePoolCR converts a platform NodePoolCreateRequest into a // v1alpha1.NodePool CR. metadata.Namespace = clusterID, metadata.Name = human name. -func PlatformCreateToNodePoolCR(accountID string, req *types.NodePoolCreateRequest) (*hyperfleetv1alpha1.NodePool, error) { +func PlatformCreateToNodePoolCR(accountID, internalPoolID string, req *types.NodePoolCreateRequest) (*hyperfleetv1alpha1.NodePool, error) { spec := *req.Spec + spec.AccountID = accountID + spec.InternalPoolID = internalPoolID return &hyperfleetv1alpha1.NodePool{ ObjectMeta: metav1.ObjectMeta{ diff --git a/platform-api/pkg/clients/hyperfleetdb/convert_test.go b/platform-api/pkg/clients/hyperfleetdb/convert_test.go index 59829c07..7181e663 100644 --- a/platform-api/pkg/clients/hyperfleetdb/convert_test.go +++ b/platform-api/pkg/clients/hyperfleetdb/convert_test.go @@ -25,7 +25,7 @@ func TestPlatformCreateToNodePoolCR_SetsAccountLabel(t *testing.T) { }, } - np, err := PlatformCreateToNodePoolCR("acct-123", req) + np, err := PlatformCreateToNodePoolCR("acct-123", "pool-uuid-1", req) if err != nil { t.Fatalf("PlatformCreateToNodePoolCR: %v", err) } @@ -37,6 +37,14 @@ func TestPlatformCreateToNodePoolCR_SetsAccountLabel(t *testing.T) { if got := np.Namespace; got != "cluster-test-cluster-id" { t.Errorf("namespace = %q, want %q", got, "cluster-test-cluster-id") } + + if got := np.Spec.AccountID; got != "acct-123" { + t.Errorf("spec.AccountID = %q, want %q", got, "acct-123") + } + + if got := np.Spec.InternalPoolID; got != "pool-uuid-1" { + t.Errorf("spec.InternalPoolID = %q, want %q", got, "pool-uuid-1") + } } func TestPlatformCreateToClusterCR_SetsAccountLabel(t *testing.T) { @@ -62,4 +70,12 @@ func TestPlatformCreateToClusterCR_SetsAccountLabel(t *testing.T) { if got := cr.Labels["hyperfleet.io/account-id"]; got != "acct-456" { t.Errorf("account-id label = %q, want %q", got, "acct-456") } + + if got := cr.Spec.AccountID; got != "acct-456" { + t.Errorf("spec.AccountID = %q, want %q", got, "acct-456") + } + + if got := cr.Spec.InternalID; got != "cluster-uuid" { + t.Errorf("spec.InternalID = %q, want %q", got, "cluster-uuid") + } } diff --git a/platform-api/pkg/handlers/nodepool.go b/platform-api/pkg/handlers/nodepool.go index 4c222dbb..4d4d6b7a 100644 --- a/platform-api/pkg/handlers/nodepool.go +++ b/platform-api/pkg/handlers/nodepool.go @@ -6,6 +6,7 @@ import ( "net/http" "strconv" + "github.com/google/uuid" "github.com/gorilla/mux" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/internal/codegen/featuregate" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/clients/hyperfleetdb" @@ -116,7 +117,8 @@ func (h *NodePoolHandler) Create(w http.ResponseWriter, r *http.Request) { h.logger.Info("creating nodepool", "account_id", accountID, "cluster_id", req.ClusterID, "nodepool_name", req.Name) - cr, err := hyperfleetdb.PlatformCreateToNodePoolCR(accountID, &req) + internalPoolID := uuid.New().String() + cr, err := hyperfleetdb.PlatformCreateToNodePoolCR(accountID, internalPoolID, &req) if err != nil { h.logger.Error("failed to convert nodepool spec", "error", err, "account_id", accountID) h.writeError(w, http.StatusBadRequest, "NODEPOOLS-MGMT-CREATE-002", "Invalid nodepool spec") From 7e36ead9927ea2d3e357816f44f490f0858b50ec Mon Sep 17 00:00:00 2001 From: Chris Doan Date: Tue, 4 Aug 2026 17:47:16 -0700 Subject: [PATCH 7/7] ROSAENG-61805: OpenAPI spec alignment with codegen pipeline Generate typed OpenAPI schemas from Go public API types using controller-tools, filter hidden fields via the codegen registry, and merge into the handwritten OpenAPI spec. Switch passthrough spec to use local ClusterConfiguration type with hidden/visible markers so nested sub-configs are properly filtered. - Add openapi-gen and openapi-merge codegen tools - Wire $ref chains for cluster and nodepool schemas - Consolidate field registry to single source in hack/api-codegen - Add generate-openapi, verify-openapi, and swagger-ui make targets - Add regression test for nested configuration marker paths Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + Makefile | 36 +- .../v2alpha1/hostedclusterspec.passthrough.go | 6 +- api/public/v2alpha1/zz_generated.deepcopy.go | 2 +- hack/api-codegen/cmd/openapi-merge/main.go | 283 ++++++++ hack/api-codegen/pkg/openapi/generator.go | 94 ++- .../api-codegen/pkg/openapi/generator_test.go | 65 ++ .../pkg/registry/field_metadata.go | 200 +++++- .../pkg/registry/field_metadata.json | 200 +++++- .../codegen/registry/field_metadata.go | 603 ------------------ .../codegen/registry/field_metadata.json | 582 ----------------- platform-api/openapi/openapi.yaml | 197 +++++- .../pkg/validation/field_validator.go | 2 +- .../pkg/validation/field_validator_test.go | 2 +- 14 files changed, 1040 insertions(+), 1233 deletions(-) create mode 100644 hack/api-codegen/cmd/openapi-merge/main.go delete mode 100644 platform-api/internal/codegen/registry/field_metadata.go delete mode 100644 platform-api/internal/codegen/registry/field_metadata.json diff --git a/.gitignore b/.gitignore index ac28e79a..5da77987 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,4 @@ hyperfleet-operator/compactor # Codegen intermediate files *.passthrough.go.raw +platform-api/openapi/generated-schemas.json diff --git a/Makefile b/Makefile index 7b96c398..890c8a73 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ manifests generate generate-clientset verify-clientset \ generate-public-deepcopy setup-envtest \ codegen-passthrough codegen-registry codegen-verify codegen verify-codegen \ + generate-openapi verify-openapi swagger-ui \ image-api image-operator image-push-api image-push-operator # ── Configuration ──────────────────────────────────────────────────────── @@ -117,6 +118,9 @@ help: @echo " codegen-registry Generate field metadata registry from markers" @echo " codegen-verify Verify codegen outputs compile" @echo " verify-codegen Fail if codegen outputs are out of date" + @echo " generate-openapi Generate and merge typed schemas into OpenAPI spec" + @echo " verify-openapi Fail if OpenAPI spec is out of date with codegen" + @echo " swagger-ui Run Swagger UI locally (default port 8282)" @echo " setup-envtest Install envtest binaries (etcd, kube-apiserver)" @echo " deps Download and tidy all modules" @echo "" @@ -142,6 +146,7 @@ build-api-codegen: cd hack/api-codegen && go build -o ../../bin/passthrough-gen ./cmd/passthrough-gen cd hack/api-codegen && go build -o ../../bin/marker-scanner ./cmd/marker-scanner cd hack/api-codegen && go build -o ../../bin/openapi-gen ./cmd/openapi-gen + cd hack/api-codegen && go build -o ../../bin/openapi-merge ./cmd/openapi-merge cd hack/api-codegen && go build -o ../../bin/conversion-gen ./cmd/conversion-gen cd hack/api-codegen && go build -o ../../bin/crd-variants ./cmd/crd-variants cd hack/api-codegen && go build -o ../../bin/featuregate-info ./cmd/featuregate-info @@ -340,18 +345,43 @@ generate-public-deepcopy: codegen-passthrough $(CONTROLLER_GEN) codegen-registry: generate-public-deepcopy build-api-codegen ./bin/marker-scanner \ -input-dirs api/public/v2alpha1 \ - -output-file platform-api/internal/codegen/registry/field_metadata.go \ + -output-file hack/api-codegen/pkg/registry/field_metadata.go \ $(if $(VERBOSE),-verbose) codegen-verify: codegen-registry cd api/public/v2alpha1 && go build ./... - cd platform-api && go build ./internal/codegen/... + cd platform-api && go build ./... codegen: codegen-verify verify-codegen: codegen git diff --exit-code api/public/v2alpha1/zz_generated.deepcopy.go - git diff --exit-code platform-api/internal/codegen/registry/ + git diff --exit-code hack/api-codegen/pkg/registry/ + +OPENAPI_GENERATED ?= platform-api/openapi/generated-schemas.json +OPENAPI_SPEC ?= platform-api/openapi/openapi.yaml + +generate-openapi: codegen-registry + cd hack/api-codegen && go build -o ../../bin/openapi-gen ./cmd/openapi-gen + ./bin/openapi-gen \ + -input-dirs ./api/public/v2alpha1 \ + -output-file $(OPENAPI_GENERATED) + ./bin/openapi-merge \ + -spec $(OPENAPI_SPEC) \ + -generated $(OPENAPI_GENERATED) \ + -schemas ClusterSpec,NodePoolSpec,HostedClusterSpecPassthrough,NodePoolSpecPassthrough,ClusterConfiguration,KubeletConfig,MachineConfigSpec + +verify-openapi: generate-openapi + git diff --exit-code $(OPENAPI_SPEC) + +SWAGGER_UI_PORT ?= 8282 + +swagger-ui: + @echo "Swagger UI available at http://localhost:$(SWAGGER_UI_PORT)" + $(CONTAINER_ENGINE) run --rm -p $(SWAGGER_UI_PORT):8080 \ + -e SWAGGER_JSON=/spec/openapi.yaml \ + -v $(CURDIR)/$(OPENAPI_SPEC):/spec/openapi.yaml:ro \ + swaggerapi/swagger-ui ENVTEST_BIN_DIR ?= $(shell pwd)/.envtest diff --git a/api/public/v2alpha1/hostedclusterspec.passthrough.go b/api/public/v2alpha1/hostedclusterspec.passthrough.go index e1d5dc6d..510cbca3 100644 --- a/api/public/v2alpha1/hostedclusterspec.passthrough.go +++ b/api/public/v2alpha1/hostedclusterspec.passthrough.go @@ -94,7 +94,7 @@ type HostedClusterSpecPassthrough struct { // configuration specifies configuration for individual OCP components in the // +k8s:openapi-gen=true // +hyperfleet:write-mode=service-set - Configuration *hypershiftv1beta1.ClusterConfiguration `json:"configuration,omitempty"` + Configuration *ClusterConfiguration `json:"configuration,omitempty"` // operatorConfiguration specifies configuration for individual OCP operators in the cluster. // +k8s:openapi-gen=true // +hyperfleet:write-mode=service-set @@ -104,8 +104,8 @@ type HostedClusterSpecPassthrough struct { // +hyperfleet:write-mode=service-set AuditWebhook *corev1.LocalObjectReference `json:"auditWebhook,omitempty"` // imageContentSources specifies image mirrors that can be used by cluster - // +k8s:openapi-gen=false - // +hyperfleet:write-mode=service-set + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=mutable ImageContentSources []hypershiftv1beta1.ImageContentSource `json:"imageContentSources,omitempty"` // additionalTrustBundle is a local reference to a ConfigMap that must have a "ca-bundle.crt" key // +k8s:openapi-gen=false diff --git a/api/public/v2alpha1/zz_generated.deepcopy.go b/api/public/v2alpha1/zz_generated.deepcopy.go index 41f8e35d..0cf8b826 100644 --- a/api/public/v2alpha1/zz_generated.deepcopy.go +++ b/api/public/v2alpha1/zz_generated.deepcopy.go @@ -318,7 +318,7 @@ func (in *HostedClusterSpecPassthrough) DeepCopyInto(out *HostedClusterSpecPasst } if in.Configuration != nil { in, out := &in.Configuration, &out.Configuration - *out = new(v1beta1.ClusterConfiguration) + *out = new(ClusterConfiguration) (*in).DeepCopyInto(*out) } if in.OperatorConfiguration != nil { diff --git a/hack/api-codegen/cmd/openapi-merge/main.go b/hack/api-codegen/cmd/openapi-merge/main.go new file mode 100644 index 00000000..f8e2c8ec --- /dev/null +++ b/hack/api-codegen/cmd/openapi-merge/main.go @@ -0,0 +1,283 @@ +package main + +import ( + "bufio" + "bytes" + "encoding/json" + "flag" + "fmt" + "log" + "os" + "sort" + "strings" + + "gopkg.in/yaml.v3" +) + +func main() { + var ( + specFile string + generatedFile string + outputFile string + schemas string + ) + + flag.StringVar(&specFile, "spec", "", "Path to the handwritten OpenAPI spec (YAML)") + flag.StringVar(&generatedFile, "generated", "", "Path to the generated schema (JSON from openapi-gen)") + flag.StringVar(&outputFile, "output", "", "Output file (defaults to overwriting -spec)") + flag.StringVar(&schemas, "schemas", "ClusterSpec,NodePoolSpec", "Comma-separated schema names to replace") + flag.Parse() + + if specFile == "" || generatedFile == "" { + flag.Usage() + os.Exit(1) + } + if outputFile == "" { + outputFile = specFile + } + + specData, err := os.ReadFile(specFile) + if err != nil { + log.Fatalf("reading spec: %v", err) + } + + genData, err := os.ReadFile(generatedFile) + if err != nil { + log.Fatalf("reading generated: %v", err) + } + + var genDoc struct { + Definitions map[string]json.RawMessage `json:"definitions"` + } + if err := json.Unmarshal(genData, &genDoc); err != nil { + log.Fatalf("parsing generated JSON: %v", err) + } + + schemaList := splitCSV(schemas) + + merged := 0 + result := specData + for _, name := range schemaList { + raw, ok := genDoc.Definitions[name] + if !ok { + log.Printf("warning: schema %q not found in generated output, skipping", name) + continue + } + + yamlBlock, err := jsonSchemaToYAML(raw, 6) + if err != nil { + log.Fatalf("converting %s to YAML: %v", name, err) + } + + updated, found := replaceSchemaBlock(result, name, yamlBlock) + if found { + result = updated + merged++ + log.Printf("replaced schema: %s", name) + } else { + result = insertSchemaBlock(result, name, yamlBlock) + merged++ + log.Printf("inserted schema: %s", name) + } + } + + if merged == 0 { + log.Fatal("no schemas were merged") + } + + if err := os.WriteFile(outputFile, result, 0644); err != nil { + log.Fatalf("writing output: %v", err) + } + + fmt.Printf("Merged %d schemas into %s\n", merged, outputFile) +} + +// replaceSchemaBlock finds a schema definition block in the YAML by looking +// for ` :` at the expected indentation under components.schemas, +// and replaces everything from that line until the next sibling definition. +func replaceSchemaBlock(spec []byte, schemaName string, replacement []byte) ([]byte, bool) { + lines := splitLines(spec) + header := " " + schemaName + ":" + + startIdx := -1 + for i, line := range lines { + if strings.TrimRight(line, " \r") == header { + startIdx = i + break + } + } + if startIdx < 0 { + return nil, false + } + + endIdx := startIdx + 1 + for endIdx < len(lines) { + line := lines[endIdx] + if line == "" || strings.TrimSpace(line) == "" { + break + } + indent := countLeadingSpaces(line) + if indent <= 4 { + break + } + endIdx++ + } + + var buf bytes.Buffer + for _, line := range lines[:startIdx] { + buf.WriteString(line) + buf.WriteByte('\n') + } + buf.WriteString(header) + buf.WriteByte('\n') + buf.Write(replacement) + for _, line := range lines[endIdx:] { + buf.WriteString(line) + buf.WriteByte('\n') + } + + return buf.Bytes(), true +} + +// insertSchemaBlock appends a new schema definition at the end of the +// components.schemas section (just before the next top-level YAML key or EOF). +func insertSchemaBlock(spec []byte, schemaName string, replacement []byte) []byte { + lines := splitLines(spec) + header := " " + schemaName + ":" + + // Find the end of the schemas section: the last line at indent >= 4 + // after we've entered the schemas block. + schemasStart := -1 + for i, line := range lines { + trimmed := strings.TrimRight(line, " \r") + if trimmed == " schemas:" || trimmed == " schemas:" { + schemasStart = i + break + } + } + if schemasStart < 0 { + log.Printf("warning: could not find schemas section for insertion of %s", schemaName) + return spec + } + + // Walk forward to find where the schemas section ends + insertIdx := len(lines) + for i := schemasStart + 1; i < len(lines); i++ { + line := lines[i] + if line == "" || strings.TrimSpace(line) == "" { + continue + } + indent := countLeadingSpaces(line) + if indent < 4 { + insertIdx = i + break + } + } + + var buf bytes.Buffer + for _, line := range lines[:insertIdx] { + buf.WriteString(line) + buf.WriteByte('\n') + } + // Add a blank separator only if the preceding line isn't already blank + if insertIdx > 0 && strings.TrimSpace(lines[insertIdx-1]) != "" { + buf.WriteByte('\n') + } + buf.WriteString(header) + buf.WriteByte('\n') + buf.Write(replacement) + for _, line := range lines[insertIdx:] { + buf.WriteString(line) + buf.WriteByte('\n') + } + + return buf.Bytes() +} + +// jsonSchemaToYAML converts a JSON schema object to a YAML block indented at +// the given base level (number of spaces for the first property level). +// It rewrites $ref paths from #/definitions/ to #/components/schemas/ for +// OpenAPI 3.0 compatibility. +func jsonSchemaToYAML(raw json.RawMessage, baseIndent int) ([]byte, error) { + var obj map[string]any + if err := json.Unmarshal(raw, &obj); err != nil { + return nil, err + } + + rewriteRefs(obj) + + yamlBytes, err := yaml.Marshal(obj) + if err != nil { + return nil, err + } + + prefix := strings.Repeat(" ", baseIndent) + var buf bytes.Buffer + scanner := bufio.NewScanner(bytes.NewReader(yamlBytes)) + for scanner.Scan() { + line := scanner.Text() + if strings.TrimSpace(line) == "" { + continue + } + buf.WriteString(prefix) + buf.WriteString(line) + buf.WriteByte('\n') + } + + return buf.Bytes(), nil +} + +// rewriteRefs recursively converts $ref paths from the internal +// #/definitions/ format to OpenAPI 3.0's #/components/schemas/. +func rewriteRefs(obj map[string]any) { + for k, v := range obj { + if k == "$ref" { + if s, ok := v.(string); ok { + obj[k] = strings.Replace(s, "#/definitions/", "#/components/schemas/", 1) + } + } + switch val := v.(type) { + case map[string]any: + rewriteRefs(val) + case []any: + for _, item := range val { + if m, ok := item.(map[string]any); ok { + rewriteRefs(m) + } + } + } + } +} + +func splitLines(data []byte) []string { + var lines []string + scanner := bufio.NewScanner(bytes.NewReader(data)) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + return lines +} + +func countLeadingSpaces(s string) int { + n := 0 + for _, c := range s { + if c == ' ' { + n++ + } else { + break + } + } + return n +} + +func splitCSV(s string) []string { + var out []string + for _, part := range strings.Split(s, ",") { + trimmed := strings.TrimSpace(part) + if trimmed != "" { + out = append(out, trimmed) + } + } + sort.Strings(out) + return out +} diff --git a/hack/api-codegen/pkg/openapi/generator.go b/hack/api-codegen/pkg/openapi/generator.go index fe1e4afa..e6045cef 100644 --- a/hack/api-codegen/pkg/openapi/generator.go +++ b/hack/api-codegen/pkg/openapi/generator.go @@ -48,6 +48,11 @@ func (g *Generator) Generate() error { // Filter hidden fields using the registry filterHiddenFields(definitions) + // Collapse deeply-nested passthrough types into opaque objects. + // The public API exposes the top-level wrapper fields but treats the + // embedded HyperShift spec as a pass-through object. + collapsePassthroughTypes(definitions) + output := schemaOutput{ OpenAPI: "3.0.0", Info: schemaInfo{ @@ -145,6 +150,20 @@ func (g *Generator) generateDefinitions(roots []*loader.Package) (map[string]api return definitions, nil } +// typeToRegistryPrefix maps Go definition type names to the dotted prefix +// used in the FieldRegistry. The scanner stores paths like "spec.accountId" +// (rooted at the Cluster/NodePool root type) while openapi-gen definitions +// are keyed by the Go struct name (e.g., "ClusterSpec"). +var typeToRegistryPrefix = map[string]string{ + "ClusterSpec": "spec", + "NodePoolSpec": "spec", + "HostedClusterSpecPassthrough": "spec.hostedCluster", + "NodePoolSpecPassthrough": "spec.nodePool", + "ClusterConfiguration": "", + "KubeletConfig": "kubelet", + "MachineConfigSpec": "machineConfig", +} + // filterHiddenFields removes fields marked as hidden in the registry from // all schema definitions. A field is hidden when its FieldRegistry entry has // Hidden == true (i.e., +k8s:openapi-gen=false). @@ -161,7 +180,11 @@ func filterHiddenFields(definitions map[string]apiextensionsv1.JSONSchemaProps) } for typeName, schema := range definitions { - pruned := pruneHiddenProperties(&schema, typeName, hiddenPaths) + prefix, ok := typeToRegistryPrefix[typeName] + if !ok { + continue + } + pruned := pruneHiddenProperties(&schema, prefix, hiddenPaths) definitions[typeName] = *pruned } } @@ -174,7 +197,12 @@ func pruneHiddenProperties(schema *apiextensionsv1.JSONSchemaProps, pathPrefix s } for propName, propSchema := range schema.Properties { - fieldPath := pathPrefix + "." + propName + var fieldPath string + if pathPrefix == "" { + fieldPath = propName + } else { + fieldPath = pathPrefix + "." + propName + } if hidden[fieldPath] { delete(schema.Properties, propName) // Also remove from required list @@ -197,3 +225,65 @@ func removeRequired(schema *apiextensionsv1.JSONSchemaProps, field string) { } } } + +// refTargets are types whose properties are replaced with $ref pointers. +// The key is the parent type, the value maps field name → definition name. +var refTargets = map[string]map[string]string{ + "ClusterSpec": {"hostedCluster": "HostedClusterSpecPassthrough"}, + "NodePoolSpec": {"nodePool": "NodePoolSpecPassthrough"}, + "HostedClusterSpecPassthrough": {"configuration": "ClusterConfiguration"}, + "ClusterConfiguration": {"kubelet": "KubeletConfig", "machineConfig": "MachineConfigSpec"}, +} + +// collapsePassthroughTypes replaces inlined nested type properties with +// $ref pointers to their named definitions and marks passthrough types +// with additionalProperties: true. Types not in refTargets have their +// nested properties stripped to keep the spec shallow. +func collapsePassthroughTypes(definitions map[string]apiextensionsv1.JSONSchemaProps) { + // Mark passthrough types as accepting additional properties + for _, typeName := range []string{"HostedClusterSpecPassthrough", "NodePoolSpecPassthrough"} { + if schema, ok := definitions[typeName]; ok { + schema.AdditionalProperties = &apiextensionsv1.JSONSchemaPropsOrBool{Allows: true} + definitions[typeName] = schema + } + } + + // Replace inlined properties with $ref for all configured targets + for parentType, fields := range refTargets { + schema, ok := definitions[parentType] + if !ok || schema.Properties == nil { + continue + } + for fieldName, defName := range fields { + if _, exists := schema.Properties[fieldName]; exists { + schema.Properties[fieldName] = apiextensionsv1.JSONSchemaProps{ + Ref: strPtr("#/definitions/" + defName), + } + } + } + definitions[parentType] = schema + } + + // Strip remaining inlined sub-properties from passthrough type fields + // that aren't wired to $ref (keeps them as shallow type: object). + for _, typeName := range []string{"HostedClusterSpecPassthrough", "NodePoolSpecPassthrough"} { + schema, ok := definitions[typeName] + if !ok { + continue + } + refs := refTargets[typeName] + for propName, propSchema := range schema.Properties { + if refs != nil { + if _, isRef := refs[propName]; isRef { + continue + } + } + propSchema.Properties = nil + propSchema.Required = nil + schema.Properties[propName] = propSchema + } + definitions[typeName] = schema + } +} + +func strPtr(s string) *string { return &s } diff --git a/hack/api-codegen/pkg/openapi/generator_test.go b/hack/api-codegen/pkg/openapi/generator_test.go index acf0583d..fe5399e7 100644 --- a/hack/api-codegen/pkg/openapi/generator_test.go +++ b/hack/api-codegen/pkg/openapi/generator_test.go @@ -6,6 +6,71 @@ import ( "testing" ) +func TestConfigurationUsesLocalType(t *testing.T) { + tmpFile := t.TempDir() + "/openapi.json" + + // Resolve the v2alpha1 package relative to this test file's location + // (hack/api-codegen/pkg/openapi/) → ../../../../api/public/v2alpha1 + v2alpha1Dir := "../../../../api/public/v2alpha1" + if _, err := os.Stat(v2alpha1Dir); err != nil { + t.Skipf("v2alpha1 source not available: %v", err) + } + + gen := NewGenerator([]string{v2alpha1Dir}, tmpFile) + gen.Title = "Test" + gen.Version = "v1" + + if err := gen.Generate(); err != nil { + t.Fatalf("Generate failed: %v", err) + } + + data, err := os.ReadFile(tmpFile) + if err != nil { + t.Fatalf("Read output: %v", err) + } + + var output schemaOutput + if err := json.Unmarshal(data, &output); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + + // ClusterConfiguration must exist as its own definition (from the local type) + cc, ok := output.Definitions["ClusterConfiguration"] + if !ok { + t.Fatal("ClusterConfiguration definition not found") + } + + // The local type's markers hide all sub-configs except kubelet and machineConfig. + // If the upstream hypershiftv1beta1.ClusterConfiguration were used instead, + // all 10 sub-config fields would be present (no hidden markers). + for _, visible := range []string{"kubelet", "machineConfig"} { + if _, found := cc.Properties[visible]; !found { + t.Errorf("expected visible property %q in ClusterConfiguration", visible) + } + } + for _, hidden := range []string{"apiServer", "authentication", "featureGate", "image", "ingress", "network", "oauth", "scheduler", "proxy"} { + if _, found := cc.Properties[hidden]; found { + t.Errorf("property %q should be hidden in ClusterConfiguration (local markers not applied?)", hidden) + } + } + + // KubeletConfig must retain its visible fields (nested path test) + kc, ok := output.Definitions["KubeletConfig"] + if !ok { + t.Fatal("KubeletConfig definition not found") + } + for _, visible := range []string{"podPidsLimit", "maxPods", "containerLogMaxFiles"} { + if _, found := kc.Properties[visible]; !found { + t.Errorf("expected visible property %q in KubeletConfig", visible) + } + } + for _, hidden := range []string{"evictionHard", "cpuManagerPolicy", "topologyManagerPolicy"} { + if _, found := kc.Properties[hidden]; found { + t.Errorf("property %q should be hidden in KubeletConfig", hidden) + } + } +} + func TestGenerateMinimal(t *testing.T) { tmpFile := "/tmp/openapi-test.json" defer func() { _ = os.Remove(tmpFile) }() diff --git a/hack/api-codegen/pkg/registry/field_metadata.go b/hack/api-codegen/pkg/registry/field_metadata.go index f6889728..8080b17e 100644 --- a/hack/api-codegen/pkg/registry/field_metadata.go +++ b/hack/api-codegen/pkg/registry/field_metadata.go @@ -362,6 +362,198 @@ var FieldRegistry = map[string]FieldMeta{ FieldPath: "spec.hostedCluster.configuration", WriteMode: ServiceSet, }, + "spec.hostedCluster.configuration.apiServer": { + FieldPath: "spec.hostedCluster.configuration.apiServer", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.authentication": { + FieldPath: "spec.hostedCluster.configuration.authentication", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.featureGate": { + FieldPath: "spec.hostedCluster.configuration.featureGate", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.image": { + FieldPath: "spec.hostedCluster.configuration.image", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.ingress": { + FieldPath: "spec.hostedCluster.configuration.ingress", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.kubelet": { + FieldPath: "spec.hostedCluster.configuration.kubelet", + WriteMode: ServiceSet, + }, + "spec.hostedCluster.configuration.kubelet.allowedUnsafeSysctls": { + FieldPath: "spec.hostedCluster.configuration.kubelet.allowedUnsafeSysctls", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.kubelet.containerLogMaxFiles": { + FieldPath: "spec.hostedCluster.configuration.kubelet.containerLogMaxFiles", + WriteMode: Mutable, + }, + "spec.hostedCluster.configuration.kubelet.containerLogMaxSize": { + FieldPath: "spec.hostedCluster.configuration.kubelet.containerLogMaxSize", + WriteMode: Mutable, + }, + "spec.hostedCluster.configuration.kubelet.cpuManagerPolicy": { + FieldPath: "spec.hostedCluster.configuration.kubelet.cpuManagerPolicy", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.kubelet.cpuManagerPolicyOptions": { + FieldPath: "spec.hostedCluster.configuration.kubelet.cpuManagerPolicyOptions", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.kubelet.cpuManagerReconcilePeriod": { + FieldPath: "spec.hostedCluster.configuration.kubelet.cpuManagerReconcilePeriod", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.kubelet.evictionHard": { + FieldPath: "spec.hostedCluster.configuration.kubelet.evictionHard", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.kubelet.evictionSoft": { + FieldPath: "spec.hostedCluster.configuration.kubelet.evictionSoft", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.kubelet.evictionSoftGracePeriod": { + FieldPath: "spec.hostedCluster.configuration.kubelet.evictionSoftGracePeriod", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.kubelet.imageGCHighThresholdPercent": { + FieldPath: "spec.hostedCluster.configuration.kubelet.imageGCHighThresholdPercent", + WriteMode: Mutable, + }, + "spec.hostedCluster.configuration.kubelet.imageGCLowThresholdPercent": { + FieldPath: "spec.hostedCluster.configuration.kubelet.imageGCLowThresholdPercent", + WriteMode: Mutable, + }, + "spec.hostedCluster.configuration.kubelet.imageMinimumGCAge": { + FieldPath: "spec.hostedCluster.configuration.kubelet.imageMinimumGCAge", + WriteMode: Mutable, + }, + "spec.hostedCluster.configuration.kubelet.kubeReserved": { + FieldPath: "spec.hostedCluster.configuration.kubelet.kubeReserved", + WriteMode: Immutable, + }, + "spec.hostedCluster.configuration.kubelet.maxPods": { + FieldPath: "spec.hostedCluster.configuration.kubelet.maxPods", + WriteMode: Mutable, + }, + "spec.hostedCluster.configuration.kubelet.memoryThrottlingFactor": { + FieldPath: "spec.hostedCluster.configuration.kubelet.memoryThrottlingFactor", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.kubelet.podPidsLimit": { + FieldPath: "spec.hostedCluster.configuration.kubelet.podPidsLimit", + WriteMode: Mutable, + }, + "spec.hostedCluster.configuration.kubelet.registryBurst": { + FieldPath: "spec.hostedCluster.configuration.kubelet.registryBurst", + WriteMode: Mutable, + FeatureGate: "HyperFleetKubeletAdvanced", + }, + "spec.hostedCluster.configuration.kubelet.registryPullQPS": { + FieldPath: "spec.hostedCluster.configuration.kubelet.registryPullQPS", + WriteMode: Mutable, + FeatureGate: "HyperFleetKubeletAdvanced", + }, + "spec.hostedCluster.configuration.kubelet.serializeImagePulls": { + FieldPath: "spec.hostedCluster.configuration.kubelet.serializeImagePulls", + WriteMode: Mutable, + FeatureGate: "HyperFleetKubeletAdvanced", + }, + "spec.hostedCluster.configuration.kubelet.streamingConnectionIdleTimeout": { + FieldPath: "spec.hostedCluster.configuration.kubelet.streamingConnectionIdleTimeout", + WriteMode: Mutable, + }, + "spec.hostedCluster.configuration.kubelet.systemReserved": { + FieldPath: "spec.hostedCluster.configuration.kubelet.systemReserved", + WriteMode: Immutable, + }, + "spec.hostedCluster.configuration.kubelet.topologyManagerPolicy": { + FieldPath: "spec.hostedCluster.configuration.kubelet.topologyManagerPolicy", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.kubelet.topologyManagerScope": { + FieldPath: "spec.hostedCluster.configuration.kubelet.topologyManagerScope", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.machineConfig": { + FieldPath: "spec.hostedCluster.configuration.machineConfig", + WriteMode: ServiceSet, + }, + "spec.hostedCluster.configuration.machineConfig.allowedKernelArguments": { + FieldPath: "spec.hostedCluster.configuration.machineConfig.allowedKernelArguments", + WriteMode: Immutable, + FeatureGate: "HyperFleetMachineConfig", + }, + "spec.hostedCluster.configuration.machineConfig.extensions": { + FieldPath: "spec.hostedCluster.configuration.machineConfig.extensions", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.machineConfig.files": { + FieldPath: "spec.hostedCluster.configuration.machineConfig.files", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.machineConfig.fips": { + FieldPath: "spec.hostedCluster.configuration.machineConfig.fips", + WriteMode: Immutable, + }, + "spec.hostedCluster.configuration.machineConfig.kernelArguments": { + FieldPath: "spec.hostedCluster.configuration.machineConfig.kernelArguments", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.machineConfig.kernelType": { + FieldPath: "spec.hostedCluster.configuration.machineConfig.kernelType", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.machineConfig.systemdUnits": { + FieldPath: "spec.hostedCluster.configuration.machineConfig.systemdUnits", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.network": { + FieldPath: "spec.hostedCluster.configuration.network", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.oauth": { + FieldPath: "spec.hostedCluster.configuration.oauth", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.proxy": { + FieldPath: "spec.hostedCluster.configuration.proxy", + WriteMode: ServiceSet, + Hidden: true, + }, + "spec.hostedCluster.configuration.scheduler": { + FieldPath: "spec.hostedCluster.configuration.scheduler", + WriteMode: ServiceSet, + Hidden: true, + }, "spec.hostedCluster.controlPlaneRelease": { FieldPath: "spec.hostedCluster.controlPlaneRelease", WriteMode: ServiceSet, @@ -388,8 +580,7 @@ var FieldRegistry = map[string]FieldMeta{ }, "spec.hostedCluster.imageContentSources": { FieldPath: "spec.hostedCluster.imageContentSources", - WriteMode: ServiceSet, - Hidden: true, + WriteMode: Mutable, }, "spec.hostedCluster.infraID": { FieldPath: "spec.hostedCluster.infraID", @@ -538,6 +729,11 @@ var FieldRegistry = map[string]FieldMeta{ WriteMode: ServiceSet, Hidden: true, }, + "spec.nodePool.osImageStream": { + FieldPath: "spec.nodePool.osImageStream", + WriteMode: ServiceSet, + Hidden: true, + }, "spec.nodePool.pausedUntil": { FieldPath: "spec.nodePool.pausedUntil", WriteMode: ServiceSet, diff --git a/hack/api-codegen/pkg/registry/field_metadata.json b/hack/api-codegen/pkg/registry/field_metadata.json index 167ecff8..f2c75c32 100644 --- a/hack/api-codegen/pkg/registry/field_metadata.json +++ b/hack/api-codegen/pkg/registry/field_metadata.json @@ -341,6 +341,198 @@ "fieldPath": "spec.hostedCluster.configuration", "writeMode": "service-set" }, + { + "fieldPath": "spec.hostedCluster.configuration.apiServer", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.authentication", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.featureGate", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.image", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.ingress", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet", + "writeMode": "service-set" + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.allowedUnsafeSysctls", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.containerLogMaxFiles", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.containerLogMaxSize", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.cpuManagerPolicy", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.cpuManagerPolicyOptions", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.cpuManagerReconcilePeriod", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.evictionHard", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.evictionSoft", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.evictionSoftGracePeriod", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.imageGCHighThresholdPercent", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.imageGCLowThresholdPercent", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.imageMinimumGCAge", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.kubeReserved", + "writeMode": "immutable" + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.maxPods", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.memoryThrottlingFactor", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.podPidsLimit", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.registryBurst", + "writeMode": "mutable", + "featureGate": "HyperFleetKubeletAdvanced" + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.registryPullQPS", + "writeMode": "mutable", + "featureGate": "HyperFleetKubeletAdvanced" + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.serializeImagePulls", + "writeMode": "mutable", + "featureGate": "HyperFleetKubeletAdvanced" + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.streamingConnectionIdleTimeout", + "writeMode": "mutable" + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.systemReserved", + "writeMode": "immutable" + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.topologyManagerPolicy", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.kubelet.topologyManagerScope", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.machineConfig", + "writeMode": "service-set" + }, + { + "fieldPath": "spec.hostedCluster.configuration.machineConfig.allowedKernelArguments", + "writeMode": "immutable", + "featureGate": "HyperFleetMachineConfig" + }, + { + "fieldPath": "spec.hostedCluster.configuration.machineConfig.extensions", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.machineConfig.files", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.machineConfig.fips", + "writeMode": "immutable" + }, + { + "fieldPath": "spec.hostedCluster.configuration.machineConfig.kernelArguments", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.machineConfig.kernelType", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.machineConfig.systemdUnits", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.network", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.oauth", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.proxy", + "writeMode": "service-set", + "hidden": true + }, + { + "fieldPath": "spec.hostedCluster.configuration.scheduler", + "writeMode": "service-set", + "hidden": true + }, { "fieldPath": "spec.hostedCluster.controlPlaneRelease", "writeMode": "service-set", @@ -367,8 +559,7 @@ }, { "fieldPath": "spec.hostedCluster.imageContentSources", - "writeMode": "service-set", - "hidden": true + "writeMode": "mutable" }, { "fieldPath": "spec.hostedCluster.infraID", @@ -517,6 +708,11 @@ "writeMode": "service-set", "hidden": true }, + { + "fieldPath": "spec.nodePool.osImageStream", + "writeMode": "service-set", + "hidden": true + }, { "fieldPath": "spec.nodePool.pausedUntil", "writeMode": "service-set", diff --git a/platform-api/internal/codegen/registry/field_metadata.go b/platform-api/internal/codegen/registry/field_metadata.go deleted file mode 100644 index a91856e3..00000000 --- a/platform-api/internal/codegen/registry/field_metadata.go +++ /dev/null @@ -1,603 +0,0 @@ -// Code generated by marker-scanner. DO NOT EDIT. - -package registry - -import ( - "github.com/openshift-online/rosa-hyperfleet-api/hack/api-codegen/pkg/markers" -) - -// Type aliases re-exported from markers so that consumers of this package -// do not need to import markers directly. -type WriteMode = markers.WriteMode -type FieldMeta = markers.FieldMeta -type FeatureGateWriteMode = markers.FeatureGateWriteMode - -const ( - Mutable = markers.Mutable - Immutable = markers.Immutable - ServiceSet = markers.ServiceSet -) - -// FieldRegistry maps field paths to their metadata -var FieldRegistry = map[string]FieldMeta{ - "allowedUnsafeSysctls": { - FieldPath: "allowedUnsafeSysctls", - WriteMode: ServiceSet, - Hidden: true, - }, - "apiServer": { - FieldPath: "apiServer", - WriteMode: ServiceSet, - Hidden: true, - }, - "authentication": { - FieldPath: "authentication", - WriteMode: ServiceSet, - Hidden: true, - }, - "containerLogMaxFiles": { - FieldPath: "containerLogMaxFiles", - WriteMode: Mutable, - }, - "containerLogMaxSize": { - FieldPath: "containerLogMaxSize", - WriteMode: Mutable, - }, - "cpuManagerPolicy": { - FieldPath: "cpuManagerPolicy", - WriteMode: ServiceSet, - Hidden: true, - }, - "cpuManagerPolicyOptions": { - FieldPath: "cpuManagerPolicyOptions", - WriteMode: ServiceSet, - Hidden: true, - }, - "cpuManagerReconcilePeriod": { - FieldPath: "cpuManagerReconcilePeriod", - WriteMode: ServiceSet, - Hidden: true, - }, - "evictionHard": { - FieldPath: "evictionHard", - WriteMode: ServiceSet, - Hidden: true, - }, - "evictionSoft": { - FieldPath: "evictionSoft", - WriteMode: ServiceSet, - Hidden: true, - }, - "evictionSoftGracePeriod": { - FieldPath: "evictionSoftGracePeriod", - WriteMode: ServiceSet, - Hidden: true, - }, - "featureGate": { - FieldPath: "featureGate", - WriteMode: ServiceSet, - Hidden: true, - }, - "image": { - FieldPath: "image", - WriteMode: ServiceSet, - Hidden: true, - }, - "imageGCHighThresholdPercent": { - FieldPath: "imageGCHighThresholdPercent", - WriteMode: Mutable, - }, - "imageGCLowThresholdPercent": { - FieldPath: "imageGCLowThresholdPercent", - WriteMode: Mutable, - }, - "imageMinimumGCAge": { - FieldPath: "imageMinimumGCAge", - WriteMode: Mutable, - }, - "ingress": { - FieldPath: "ingress", - WriteMode: ServiceSet, - Hidden: true, - }, - "kubeReserved": { - FieldPath: "kubeReserved", - WriteMode: Immutable, - }, - "kubelet": { - FieldPath: "kubelet", - WriteMode: ServiceSet, - }, - "kubelet.allowedUnsafeSysctls": { - FieldPath: "kubelet.allowedUnsafeSysctls", - WriteMode: ServiceSet, - Hidden: true, - }, - "kubelet.containerLogMaxFiles": { - FieldPath: "kubelet.containerLogMaxFiles", - WriteMode: Mutable, - }, - "kubelet.containerLogMaxSize": { - FieldPath: "kubelet.containerLogMaxSize", - WriteMode: Mutable, - }, - "kubelet.cpuManagerPolicy": { - FieldPath: "kubelet.cpuManagerPolicy", - WriteMode: ServiceSet, - Hidden: true, - }, - "kubelet.cpuManagerPolicyOptions": { - FieldPath: "kubelet.cpuManagerPolicyOptions", - WriteMode: ServiceSet, - Hidden: true, - }, - "kubelet.cpuManagerReconcilePeriod": { - FieldPath: "kubelet.cpuManagerReconcilePeriod", - WriteMode: ServiceSet, - Hidden: true, - }, - "kubelet.evictionHard": { - FieldPath: "kubelet.evictionHard", - WriteMode: ServiceSet, - Hidden: true, - }, - "kubelet.evictionSoft": { - FieldPath: "kubelet.evictionSoft", - WriteMode: ServiceSet, - Hidden: true, - }, - "kubelet.evictionSoftGracePeriod": { - FieldPath: "kubelet.evictionSoftGracePeriod", - WriteMode: ServiceSet, - Hidden: true, - }, - "kubelet.imageGCHighThresholdPercent": { - FieldPath: "kubelet.imageGCHighThresholdPercent", - WriteMode: Mutable, - }, - "kubelet.imageGCLowThresholdPercent": { - FieldPath: "kubelet.imageGCLowThresholdPercent", - WriteMode: Mutable, - }, - "kubelet.imageMinimumGCAge": { - FieldPath: "kubelet.imageMinimumGCAge", - WriteMode: Mutable, - }, - "kubelet.kubeReserved": { - FieldPath: "kubelet.kubeReserved", - WriteMode: Immutable, - }, - "kubelet.maxPods": { - FieldPath: "kubelet.maxPods", - WriteMode: Mutable, - }, - "kubelet.memoryThrottlingFactor": { - FieldPath: "kubelet.memoryThrottlingFactor", - WriteMode: ServiceSet, - Hidden: true, - }, - "kubelet.podPidsLimit": { - FieldPath: "kubelet.podPidsLimit", - WriteMode: Mutable, - }, - "kubelet.registryBurst": { - FieldPath: "kubelet.registryBurst", - WriteMode: Mutable, - FeatureGate: "HyperFleetKubeletAdvanced", - }, - "kubelet.registryPullQPS": { - FieldPath: "kubelet.registryPullQPS", - WriteMode: Mutable, - FeatureGate: "HyperFleetKubeletAdvanced", - }, - "kubelet.serializeImagePulls": { - FieldPath: "kubelet.serializeImagePulls", - WriteMode: Mutable, - FeatureGate: "HyperFleetKubeletAdvanced", - }, - "kubelet.streamingConnectionIdleTimeout": { - FieldPath: "kubelet.streamingConnectionIdleTimeout", - WriteMode: Mutable, - }, - "kubelet.systemReserved": { - FieldPath: "kubelet.systemReserved", - WriteMode: Immutable, - }, - "kubelet.topologyManagerPolicy": { - FieldPath: "kubelet.topologyManagerPolicy", - WriteMode: ServiceSet, - Hidden: true, - }, - "kubelet.topologyManagerScope": { - FieldPath: "kubelet.topologyManagerScope", - WriteMode: ServiceSet, - Hidden: true, - }, - "machineConfig": { - FieldPath: "machineConfig", - WriteMode: ServiceSet, - }, - "machineConfig.allowedKernelArguments": { - FieldPath: "machineConfig.allowedKernelArguments", - WriteMode: Immutable, - FeatureGate: "HyperFleetMachineConfig", - }, - "machineConfig.extensions": { - FieldPath: "machineConfig.extensions", - WriteMode: ServiceSet, - Hidden: true, - }, - "machineConfig.files": { - FieldPath: "machineConfig.files", - WriteMode: ServiceSet, - Hidden: true, - }, - "machineConfig.fips": { - FieldPath: "machineConfig.fips", - WriteMode: Immutable, - }, - "machineConfig.kernelArguments": { - FieldPath: "machineConfig.kernelArguments", - WriteMode: ServiceSet, - Hidden: true, - }, - "machineConfig.kernelType": { - FieldPath: "machineConfig.kernelType", - WriteMode: ServiceSet, - Hidden: true, - }, - "machineConfig.systemdUnits": { - FieldPath: "machineConfig.systemdUnits", - WriteMode: ServiceSet, - Hidden: true, - }, - "maxPods": { - FieldPath: "maxPods", - WriteMode: Mutable, - }, - "memoryThrottlingFactor": { - FieldPath: "memoryThrottlingFactor", - WriteMode: ServiceSet, - Hidden: true, - }, - "network": { - FieldPath: "network", - WriteMode: ServiceSet, - Hidden: true, - }, - "oauth": { - FieldPath: "oauth", - WriteMode: ServiceSet, - Hidden: true, - }, - "podPidsLimit": { - FieldPath: "podPidsLimit", - WriteMode: Mutable, - }, - "proxy": { - FieldPath: "proxy", - WriteMode: ServiceSet, - Hidden: true, - }, - "registryBurst": { - FieldPath: "registryBurst", - WriteMode: Mutable, - FeatureGate: "HyperFleetKubeletAdvanced", - }, - "registryPullQPS": { - FieldPath: "registryPullQPS", - WriteMode: Mutable, - FeatureGate: "HyperFleetKubeletAdvanced", - }, - "scheduler": { - FieldPath: "scheduler", - WriteMode: ServiceSet, - Hidden: true, - }, - "serializeImagePulls": { - FieldPath: "serializeImagePulls", - WriteMode: Mutable, - FeatureGate: "HyperFleetKubeletAdvanced", - }, - "spec.accountId": { - FieldPath: "spec.accountId", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.autoRepair": { - FieldPath: "spec.autoRepair", - WriteMode: Mutable, - }, - "spec.creatorARN": { - FieldPath: "spec.creatorARN", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.deleteProtection": { - FieldPath: "spec.deleteProtection", - WriteMode: Mutable, - }, - "spec.displayName": { - FieldPath: "spec.displayName", - WriteMode: Mutable, - }, - "spec.expirationTimestamp": { - FieldPath: "spec.expirationTimestamp", - WriteMode: Mutable, - }, - "spec.hostedCluster.additionalTrustBundle": { - FieldPath: "spec.hostedCluster.additionalTrustBundle", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.auditWebhook": { - FieldPath: "spec.hostedCluster.auditWebhook", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.autoNode": { - FieldPath: "spec.hostedCluster.autoNode", - WriteMode: ServiceSet, - }, - "spec.hostedCluster.autoscaling": { - FieldPath: "spec.hostedCluster.autoscaling", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.capabilities": { - FieldPath: "spec.hostedCluster.capabilities", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.channel": { - FieldPath: "spec.hostedCluster.channel", - WriteMode: ServiceSet, - }, - "spec.hostedCluster.clusterID": { - FieldPath: "spec.hostedCluster.clusterID", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.configuration": { - FieldPath: "spec.hostedCluster.configuration", - WriteMode: ServiceSet, - }, - "spec.hostedCluster.controlPlaneRelease": { - FieldPath: "spec.hostedCluster.controlPlaneRelease", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.controllerAvailabilityPolicy": { - FieldPath: "spec.hostedCluster.controllerAvailabilityPolicy", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.dns": { - FieldPath: "spec.hostedCluster.dns", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.etcd": { - FieldPath: "spec.hostedCluster.etcd", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.fips": { - FieldPath: "spec.hostedCluster.fips", - WriteMode: ServiceSet, - }, - "spec.hostedCluster.imageContentSources": { - FieldPath: "spec.hostedCluster.imageContentSources", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.infraID": { - FieldPath: "spec.hostedCluster.infraID", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.infrastructureAvailabilityPolicy": { - FieldPath: "spec.hostedCluster.infrastructureAvailabilityPolicy", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.issuerURL": { - FieldPath: "spec.hostedCluster.issuerURL", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.kubeAPIServerDNSName": { - FieldPath: "spec.hostedCluster.kubeAPIServerDNSName", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.labels": { - FieldPath: "spec.hostedCluster.labels", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.networking": { - FieldPath: "spec.hostedCluster.networking", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.nodeSelector": { - FieldPath: "spec.hostedCluster.nodeSelector", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.olmCatalogPlacement": { - FieldPath: "spec.hostedCluster.olmCatalogPlacement", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.operatorConfiguration": { - FieldPath: "spec.hostedCluster.operatorConfiguration", - WriteMode: ServiceSet, - }, - "spec.hostedCluster.pausedUntil": { - FieldPath: "spec.hostedCluster.pausedUntil", - WriteMode: ServiceSet, - }, - "spec.hostedCluster.platform": { - FieldPath: "spec.hostedCluster.platform", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.pullSecret": { - FieldPath: "spec.hostedCluster.pullSecret", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.release": { - FieldPath: "spec.hostedCluster.release", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.secretEncryption": { - FieldPath: "spec.hostedCluster.secretEncryption", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.serviceAccountSigningKey": { - FieldPath: "spec.hostedCluster.serviceAccountSigningKey", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.services": { - FieldPath: "spec.hostedCluster.services", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.sshKey": { - FieldPath: "spec.hostedCluster.sshKey", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.tolerations": { - FieldPath: "spec.hostedCluster.tolerations", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.updateService": { - FieldPath: "spec.hostedCluster.updateService", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.internalId": { - FieldPath: "spec.internalId", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.internalPoolId": { - FieldPath: "spec.internalPoolId", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.labels": { - FieldPath: "spec.labels", - WriteMode: Mutable, - }, - "spec.nodePool.arch": { - FieldPath: "spec.nodePool.arch", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.autoScaling": { - FieldPath: "spec.nodePool.autoScaling", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.clusterName": { - FieldPath: "spec.nodePool.clusterName", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.config": { - FieldPath: "spec.nodePool.config", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.management": { - FieldPath: "spec.nodePool.management", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.nodeDrainTimeout": { - FieldPath: "spec.nodePool.nodeDrainTimeout", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.nodeLabels": { - FieldPath: "spec.nodePool.nodeLabels", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.nodeVolumeDetachTimeout": { - FieldPath: "spec.nodePool.nodeVolumeDetachTimeout", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.osImageStream": { - FieldPath: "spec.nodePool.osImageStream", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.pausedUntil": { - FieldPath: "spec.nodePool.pausedUntil", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.platform": { - FieldPath: "spec.nodePool.platform", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.release": { - FieldPath: "spec.nodePool.release", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.replicas": { - FieldPath: "spec.nodePool.replicas", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.taints": { - FieldPath: "spec.nodePool.taints", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.tuningConfig": { - FieldPath: "spec.nodePool.tuningConfig", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.properties": { - FieldPath: "spec.properties", - WriteMode: Mutable, - }, - "spec.tags": { - FieldPath: "spec.tags", - WriteMode: Mutable, - FeatureGate: "HyperFleetAutoScaling", - }, - "streamingConnectionIdleTimeout": { - FieldPath: "streamingConnectionIdleTimeout", - WriteMode: Mutable, - }, - "systemReserved": { - FieldPath: "systemReserved", - WriteMode: Immutable, - }, - "topologyManagerPolicy": { - FieldPath: "topologyManagerPolicy", - WriteMode: ServiceSet, - Hidden: true, - }, - "topologyManagerScope": { - FieldPath: "topologyManagerScope", - WriteMode: ServiceSet, - Hidden: true, - }, -} diff --git a/platform-api/internal/codegen/registry/field_metadata.json b/platform-api/internal/codegen/registry/field_metadata.json deleted file mode 100644 index 62347632..00000000 --- a/platform-api/internal/codegen/registry/field_metadata.json +++ /dev/null @@ -1,582 +0,0 @@ -[ - { - "fieldPath": "allowedUnsafeSysctls", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "apiServer", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "authentication", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "containerLogMaxFiles", - "writeMode": "mutable" - }, - { - "fieldPath": "containerLogMaxSize", - "writeMode": "mutable" - }, - { - "fieldPath": "cpuManagerPolicy", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "cpuManagerPolicyOptions", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "cpuManagerReconcilePeriod", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "evictionHard", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "evictionSoft", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "evictionSoftGracePeriod", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "featureGate", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "image", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "imageGCHighThresholdPercent", - "writeMode": "mutable" - }, - { - "fieldPath": "imageGCLowThresholdPercent", - "writeMode": "mutable" - }, - { - "fieldPath": "imageMinimumGCAge", - "writeMode": "mutable" - }, - { - "fieldPath": "ingress", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "kubeReserved", - "writeMode": "immutable" - }, - { - "fieldPath": "kubelet", - "writeMode": "service-set" - }, - { - "fieldPath": "kubelet.allowedUnsafeSysctls", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "kubelet.containerLogMaxFiles", - "writeMode": "mutable" - }, - { - "fieldPath": "kubelet.containerLogMaxSize", - "writeMode": "mutable" - }, - { - "fieldPath": "kubelet.cpuManagerPolicy", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "kubelet.cpuManagerPolicyOptions", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "kubelet.cpuManagerReconcilePeriod", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "kubelet.evictionHard", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "kubelet.evictionSoft", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "kubelet.evictionSoftGracePeriod", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "kubelet.imageGCHighThresholdPercent", - "writeMode": "mutable" - }, - { - "fieldPath": "kubelet.imageGCLowThresholdPercent", - "writeMode": "mutable" - }, - { - "fieldPath": "kubelet.imageMinimumGCAge", - "writeMode": "mutable" - }, - { - "fieldPath": "kubelet.kubeReserved", - "writeMode": "immutable" - }, - { - "fieldPath": "kubelet.maxPods", - "writeMode": "mutable" - }, - { - "fieldPath": "kubelet.memoryThrottlingFactor", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "kubelet.podPidsLimit", - "writeMode": "mutable" - }, - { - "fieldPath": "kubelet.registryBurst", - "writeMode": "mutable", - "featureGate": "HyperFleetKubeletAdvanced" - }, - { - "fieldPath": "kubelet.registryPullQPS", - "writeMode": "mutable", - "featureGate": "HyperFleetKubeletAdvanced" - }, - { - "fieldPath": "kubelet.serializeImagePulls", - "writeMode": "mutable", - "featureGate": "HyperFleetKubeletAdvanced" - }, - { - "fieldPath": "kubelet.streamingConnectionIdleTimeout", - "writeMode": "mutable" - }, - { - "fieldPath": "kubelet.systemReserved", - "writeMode": "immutable" - }, - { - "fieldPath": "kubelet.topologyManagerPolicy", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "kubelet.topologyManagerScope", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "machineConfig", - "writeMode": "service-set" - }, - { - "fieldPath": "machineConfig.allowedKernelArguments", - "writeMode": "immutable", - "featureGate": "HyperFleetMachineConfig" - }, - { - "fieldPath": "machineConfig.extensions", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "machineConfig.files", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "machineConfig.fips", - "writeMode": "immutable" - }, - { - "fieldPath": "machineConfig.kernelArguments", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "machineConfig.kernelType", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "machineConfig.systemdUnits", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "maxPods", - "writeMode": "mutable" - }, - { - "fieldPath": "memoryThrottlingFactor", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "network", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "oauth", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "podPidsLimit", - "writeMode": "mutable" - }, - { - "fieldPath": "proxy", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "registryBurst", - "writeMode": "mutable", - "featureGate": "HyperFleetKubeletAdvanced" - }, - { - "fieldPath": "registryPullQPS", - "writeMode": "mutable", - "featureGate": "HyperFleetKubeletAdvanced" - }, - { - "fieldPath": "scheduler", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "serializeImagePulls", - "writeMode": "mutable", - "featureGate": "HyperFleetKubeletAdvanced" - }, - { - "fieldPath": "spec.accountId", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.autoRepair", - "writeMode": "mutable" - }, - { - "fieldPath": "spec.creatorARN", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.deleteProtection", - "writeMode": "mutable" - }, - { - "fieldPath": "spec.displayName", - "writeMode": "mutable" - }, - { - "fieldPath": "spec.expirationTimestamp", - "writeMode": "mutable" - }, - { - "fieldPath": "spec.hostedCluster.additionalTrustBundle", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.auditWebhook", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.autoNode", - "writeMode": "service-set" - }, - { - "fieldPath": "spec.hostedCluster.autoscaling", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.capabilities", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.channel", - "writeMode": "service-set" - }, - { - "fieldPath": "spec.hostedCluster.clusterID", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.configuration", - "writeMode": "service-set" - }, - { - "fieldPath": "spec.hostedCluster.controlPlaneRelease", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.controllerAvailabilityPolicy", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.dns", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.etcd", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.fips", - "writeMode": "service-set" - }, - { - "fieldPath": "spec.hostedCluster.imageContentSources", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.infraID", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.infrastructureAvailabilityPolicy", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.issuerURL", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.kubeAPIServerDNSName", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.labels", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.networking", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.nodeSelector", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.olmCatalogPlacement", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.operatorConfiguration", - "writeMode": "service-set" - }, - { - "fieldPath": "spec.hostedCluster.pausedUntil", - "writeMode": "service-set" - }, - { - "fieldPath": "spec.hostedCluster.platform", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.pullSecret", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.release", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.secretEncryption", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.serviceAccountSigningKey", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.services", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.sshKey", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.tolerations", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.updateService", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.internalId", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.internalPoolId", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.labels", - "writeMode": "mutable" - }, - { - "fieldPath": "spec.nodePool.arch", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.autoScaling", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.clusterName", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.config", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.management", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.nodeDrainTimeout", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.nodeLabels", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.nodeVolumeDetachTimeout", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.osImageStream", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.pausedUntil", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.platform", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.release", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.replicas", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.taints", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.tuningConfig", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.properties", - "writeMode": "mutable" - }, - { - "fieldPath": "spec.tags", - "writeMode": "mutable", - "featureGate": "HyperFleetAutoScaling" - }, - { - "fieldPath": "streamingConnectionIdleTimeout", - "writeMode": "mutable" - }, - { - "fieldPath": "systemReserved", - "writeMode": "immutable" - }, - { - "fieldPath": "topologyManagerPolicy", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "topologyManagerScope", - "writeMode": "service-set", - "hidden": true - } -] \ No newline at end of file diff --git a/platform-api/openapi/openapi.yaml b/platform-api/openapi/openapi.yaml index 8fdabf7e..8a6dc23e 100644 --- a/platform-api/openapi/openapi.yaml +++ b/platform-api/openapi/openapi.yaml @@ -2257,42 +2257,54 @@ components: # Cluster Schemas ClusterSpec: - type: object - description: | - Cluster specification following the hyperfleet-operator v1alpha1.ClusterSpec - type. Contains a `creatorARN` and a nested `hostedCluster` field that follows - the HyperShift v1beta1 HostedClusterSpec schema. + description: ClusterSpec defines the desired state of a ROSA HCP cluster. properties: - creatorARN: - type: string - description: ARN of the user who created this cluster (auto-populated by the API) - hostedCluster: - type: object - description: | - HyperShift v1beta1 HostedClusterSpec. Key fields include: - - platform: Cloud provider configuration (type, aws) - - networking: Cluster/service/machine network CIDRs - - release: OpenShift release image - - issuerURL: OIDC issuer URL (auto-populated) - additionalProperties: true - - NodePoolSpec: + deleteProtection: + description: DeleteProtection prevents accidental deletion when enabled. + type: boolean + displayName: + description: DisplayName is a human-readable name for the cluster. + maxLength: 256 + type: string + expirationTimestamp: + description: ExpirationTimestamp marks when this cluster should be automatically deleted. + format: date-time + type: string + hostedCluster: + $ref: '#/components/schemas/HostedClusterSpecPassthrough' + properties: + additionalProperties: + type: string + description: Properties are arbitrary key-value pairs for customer metadata. + type: object + tags: + additionalProperties: + type: string + description: Tags are customer-defined labels for organizational purposes. + type: object + required: + - hostedCluster type: object - description: | - NodePool specification following the hyperfleet-operator v1alpha1.NodePoolSpec - type. Contains a nested `nodePool` field that follows the HyperShift v1beta1 - NodePoolSpec schema. + NodePoolSpec: + description: NodePoolSpec defines the desired state of a NodePool. properties: - nodePool: - type: object - description: | - HyperShift v1beta1 NodePoolSpec. Key fields include: - - platform: Cloud provider configuration (type, aws with instanceType, rootVolume) - - replicas: Number of worker nodes (default: 2) - - release: OpenShift release image - - management: Upgrade and repair configuration - additionalProperties: true - + autoRepair: + description: AutoRepair enables automatic repair of unhealthy nodes. + type: boolean + displayName: + description: DisplayName is a human-readable name for the node pool. + maxLength: 256 + type: string + labels: + additionalProperties: + type: string + description: Labels are customer-defined labels applied to nodes. + type: object + nodePool: + $ref: '#/components/schemas/NodePoolSpecPassthrough' + required: + - nodePool + type: object Cluster: type: object description: A user cluster resource @@ -2942,6 +2954,125 @@ components: total: type: integer + + HostedClusterSpecPassthrough: + additionalProperties: true + description: HostedClusterSpecPassthrough mirrors HostedClusterSpec from upstream HyperShift + properties: + autoNode: + description: autoNode specifies the configuration for automatic node provisioning and lifecycle management. + type: object + channel: + description: channel is an identifier for explicitly requesting that a non-default set of updates be applied to this cluster. + type: string + configuration: + $ref: '#/components/schemas/ClusterConfiguration' + fips: + description: fips indicates whether this cluster's nodes will be running in FIPS mode. + type: boolean + imageContentSources: + description: imageContentSources specifies image mirrors that can be used by cluster + items: + description: |- + ImageContentSource specifies image mirrors that can be used by cluster nodes + to pull content. For cluster workloads, if a container image registry host of + the pullspec matches Source then one of the Mirrors are substituted as hosts + in the pullspec and tried in order to fetch the image. + properties: + mirrors: + description: mirrors are one or more repositories that may also contain the same images. + items: + maxLength: 255 + type: string + maxItems: 255 + type: array + x-kubernetes-list-type: set + source: + description: |- + source is the repository that users refer to, e.g. in image pull + specifications. + maxLength: 255 + type: string + required: + - source + type: object + type: array + operatorConfiguration: + description: operatorConfiguration specifies configuration for individual OCP operators in the cluster. + type: object + pausedUntil: + description: pausedUntil is a field that can be used to pause reconciliation on the HostedCluster controller, resulting in any change to the HostedCluster being ignored. + type: string + required: + - autoNode + - fips + type: object + NodePoolSpecPassthrough: + additionalProperties: true + description: NodePoolSpecPassthrough mirrors NodePoolSpec from upstream HyperShift + type: object + ClusterConfiguration: + description: |- + ClusterConfiguration specifies configuration for individual OCP components in the cluster. + This is a HyperFleet-owned mirror of hypershiftv1beta1.ClusterConfiguration that allows + us to add granular markers to nested fields like kubelet config. + properties: + kubelet: + $ref: '#/components/schemas/KubeletConfig' + machineConfig: + $ref: '#/components/schemas/MachineConfigSpec' + type: object + KubeletConfig: + description: KubeletConfig specifies kubelet configuration with granular markers for customer control. + properties: + containerLogMaxFiles: + format: int32 + type: integer + containerLogMaxSize: + type: string + imageGCHighThresholdPercent: + format: int32 + type: integer + imageGCLowThresholdPercent: + format: int32 + type: integer + imageMinimumGCAge: + type: string + kubeReserved: + additionalProperties: + type: string + type: object + maxPods: + format: int32 + type: integer + podPidsLimit: + format: int64 + type: integer + registryBurst: + format: int32 + type: integer + registryPullQPS: + format: int32 + type: integer + serializeImagePulls: + type: boolean + streamingConnectionIdleTimeout: + type: string + systemReserved: + additionalProperties: + type: string + type: object + type: object + MachineConfigSpec: + description: MachineConfigSpec specifies machine-level configuration. + properties: + allowedKernelArguments: + items: + type: string + type: array + fips: + type: boolean + type: object responses: BadRequest: description: Bad request diff --git a/platform-api/pkg/validation/field_validator.go b/platform-api/pkg/validation/field_validator.go index fa47ba84..eae425c4 100644 --- a/platform-api/pkg/validation/field_validator.go +++ b/platform-api/pkg/validation/field_validator.go @@ -6,8 +6,8 @@ import ( "reflect" "strings" + "github.com/openshift-online/rosa-hyperfleet-api/hack/api-codegen/pkg/registry" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/internal/codegen/featuregate" - "github.com/openshift-online/rosa-hyperfleet-api/platform-api/internal/codegen/registry" ) type Operation string diff --git a/platform-api/pkg/validation/field_validator_test.go b/platform-api/pkg/validation/field_validator_test.go index 69294a7c..900e083c 100644 --- a/platform-api/pkg/validation/field_validator_test.go +++ b/platform-api/pkg/validation/field_validator_test.go @@ -3,8 +3,8 @@ package validation import ( "testing" + "github.com/openshift-online/rosa-hyperfleet-api/hack/api-codegen/pkg/registry" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/internal/codegen/featuregate" - "github.com/openshift-online/rosa-hyperfleet-api/platform-api/internal/codegen/registry" ) func newTestValidator(entries map[string]registry.FieldMeta) *FieldValidator {