diff --git a/.gitignore b/.gitignore index 802359dd..ee75d03c 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,7 @@ hyperfleet-operator/bin/ # Compiled operator binaries (built outside bin/) hyperfleet-operator/manager hyperfleet-operator/compactor + +# Codegen intermediates +*.passthrough.go.raw +api/v1alpha1/public/generated-schemas.json diff --git a/.tekton/rosa-hyperfleet-api-pull-request.yaml b/.tekton/rosa-hyperfleet-api-pull-request.yaml index 7df17d01..99c39741 100644 --- a/.tekton/rosa-hyperfleet-api-pull-request.yaml +++ b/.tekton/rosa-hyperfleet-api-pull-request.yaml @@ -36,7 +36,7 @@ spec: - name: hermetic value: "true" - name: prefetch-input - value: '[{"type": "gomod", "path": "hyperfleet-db"}, {"type": "gomod", "path": "hyperfleet-operator/api"}, {"type": "gomod", "path": "platform-api"}]' + value: '[{"type": "gomod", "path": "hyperfleet-db"}, {"type": "gomod", "path": "api"}, {"type": "gomod", "path": "hack/api-codegen"}, {"type": "gomod", "path": "platform-api"}]' - name: build-source-image value: "true" pipelineSpec: diff --git a/.tekton/rosa-hyperfleet-api-push.yaml b/.tekton/rosa-hyperfleet-api-push.yaml index c1a63921..e7f6ff1d 100644 --- a/.tekton/rosa-hyperfleet-api-push.yaml +++ b/.tekton/rosa-hyperfleet-api-push.yaml @@ -33,7 +33,7 @@ spec: - name: hermetic value: "true" - name: prefetch-input - value: '[{"type": "gomod", "path": "hyperfleet-db"}, {"type": "gomod", "path": "hyperfleet-operator/api"}, {"type": "gomod", "path": "platform-api"}]' + value: '[{"type": "gomod", "path": "hyperfleet-db"}, {"type": "gomod", "path": "api"}, {"type": "gomod", "path": "hack/api-codegen"}, {"type": "gomod", "path": "platform-api"}]' - name: build-source-image value: "true" pipelineSpec: diff --git a/.tekton/rosa-hyperfleet-operator-pull-request.yaml b/.tekton/rosa-hyperfleet-operator-pull-request.yaml index 67336ad4..43f7cc17 100644 --- a/.tekton/rosa-hyperfleet-operator-pull-request.yaml +++ b/.tekton/rosa-hyperfleet-operator-pull-request.yaml @@ -8,7 +8,7 @@ metadata: build.appstudio.redhat.com/target_branch: '{{target_branch}}' pipelinesascode.tekton.dev/cancel-in-progress: "true" pipelinesascode.tekton.dev/max-keep-runs: "3" - pipelinesascode.tekton.dev/on-cel-expression: event == "pull_request" && target_branch == "main" && ( "hyperfleet-operator/***".pathChanged() || "hyperfleet-db/***".pathChanged() || ".tekton/rosa-hyperfleet-operator-pull-request.yaml".pathChanged() || "hyperfleet-operator/Containerfile".pathChanged() ) + pipelinesascode.tekton.dev/on-cel-expression: event == "pull_request" && target_branch == "main" && ( "api/***".pathChanged() || "hyperfleet-operator/***".pathChanged() || "hyperfleet-db/***".pathChanged() || ".tekton/rosa-hyperfleet-operator-pull-request.yaml".pathChanged() || "hyperfleet-operator/Containerfile".pathChanged() ) labels: appstudio.openshift.io/application: rosa-hyperfleet appstudio.openshift.io/component: rosa-hyperfleet-operator @@ -35,7 +35,7 @@ spec: - name: hermetic value: "true" - name: prefetch-input - value: '[{"type": "gomod", "path": "hyperfleet-db"}, {"type": "gomod", "path": "hyperfleet-operator/api"}, {"type": "gomod", "path": "hyperfleet-operator"}]' + value: '[{"type": "gomod", "path": "hyperfleet-db"}, {"type": "gomod", "path": "api"}, {"type": "gomod", "path": "hyperfleet-operator"}]' - name: build-source-image value: "true" pipelineSpec: diff --git a/.tekton/rosa-hyperfleet-operator-push.yaml b/.tekton/rosa-hyperfleet-operator-push.yaml index 928e4889..926264b8 100644 --- a/.tekton/rosa-hyperfleet-operator-push.yaml +++ b/.tekton/rosa-hyperfleet-operator-push.yaml @@ -7,7 +7,7 @@ metadata: build.appstudio.redhat.com/target_branch: '{{target_branch}}' pipelinesascode.tekton.dev/cancel-in-progress: "false" pipelinesascode.tekton.dev/max-keep-runs: "3" - pipelinesascode.tekton.dev/on-cel-expression: event == "push" && target_branch == "main" && ( "force_konflux_release".pathChanged() || "hyperfleet-operator/***".pathChanged() || "hyperfleet-db/***".pathChanged() || ".tekton/rosa-hyperfleet-operator-push.yaml".pathChanged() || "hyperfleet-operator/Containerfile".pathChanged() ) + pipelinesascode.tekton.dev/on-cel-expression: event == "push" && target_branch == "main" && ( "force_konflux_release".pathChanged() || "api/***".pathChanged() || "hyperfleet-operator/***".pathChanged() || "hyperfleet-db/***".pathChanged() || ".tekton/rosa-hyperfleet-operator-push.yaml".pathChanged() || "hyperfleet-operator/Containerfile".pathChanged() ) labels: appstudio.openshift.io/application: rosa-hyperfleet appstudio.openshift.io/component: rosa-hyperfleet-operator @@ -32,7 +32,7 @@ spec: - name: hermetic value: "true" - name: prefetch-input - value: '[{"type": "gomod", "path": "hyperfleet-db"}, {"type": "gomod", "path": "hyperfleet-operator/api"}, {"type": "gomod", "path": "hyperfleet-operator"}]' + value: '[{"type": "gomod", "path": "hyperfleet-db"}, {"type": "gomod", "path": "api"}, {"type": "gomod", "path": "hyperfleet-operator"}]' - name: build-source-image value: "true" pipelineSpec: diff --git a/CLAUDE.md b/CLAUDE.md index ed855d02..28d7055e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,9 +35,9 @@ make generate # Generate deepcopy ``` hyperfleet-db/go.mod ← standalone -hyperfleet-operator/api/go.mod ← standalone (CRD types sub-module) -hyperfleet-operator/go.mod ← requires: fleetdb, hyperfleet-operator/api -platform-api/go.mod ← requires: fleetdb, hyperfleet-operator/api +api/go.mod ← standalone (CRD types sub-module) +hyperfleet-operator/go.mod ← requires: fleetdb, api +platform-api/go.mod ← requires: fleetdb, api ``` Cross-module refs use permanent `replace` directives to sibling dirs. diff --git a/Makefile b/Makefile index 2d1b379b..2cac68e4 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,9 @@ e2e-authz-infra-up e2e-authz-infra-down e2e-init-db \ fmt vet verify deps \ manifests generate generate-clientset verify-clientset setup-envtest \ + codegen-passthrough codegen-registry codegen-verify codegen verify-codegen \ + codegen-conversion verify-conversion \ + generate-openapi verify-openapi swagger-ui \ image-api image-operator image-push-api image-push-operator # ── Configuration ──────────────────────────────────────────────────────── @@ -40,20 +43,25 @@ GINKGO := $(abspath $(TOOLS_BIN_DIR)/ginkgo) # ── SDK generation ─────────────────────────────────────────────────────── SDK_MODULE ?= github.com/openshift-online/rosa-hyperfleet-api -SDK_API_PKG ?= $(SDK_MODULE)/hyperfleet-operator/api +SDK_API_PKG ?= $(SDK_MODULE)/api SDK_INPUT ?= v1alpha1 SDK_CLIENTSET ?= generated SDK_OUTPUT_DIR ?= $(abspath clientset) SDK_OUTPUT_PKG ?= $(SDK_MODULE)/clientset -WIRE_INPUT_DIR ?= $(abspath hyperfleet-operator/api/v1alpha1) +WIRE_INPUT_DIR ?= $(abspath api/v1alpha1) WIRE_OUTPUT_DIR ?= $(abspath clientset/transport) WIRE_OUTPUT_PKG ?= transport WRAPPERS_OUTPUT_DIR ?= $(abspath clientset/wrappers) WRAPPERS_OUTPUT_PKG ?= wrappers TYPED_PKG_IMPORT ?= $(SDK_MODULE)/clientset/generated/typed/v1alpha1/internalversion -API_PKG_IMPORT ?= $(SDK_MODULE)/hyperfleet-operator/api/v1alpha1 +API_PKG_IMPORT ?= $(SDK_MODULE)/api/v1alpha1 SDK_HEADER_FILE ?= $(abspath hack/clientset/license-boilerplate.go.txt) +# ── Code generation ─────────────────────────────────────────────────────── +OPENAPI_GENERATED ?= api/v1alpha1/public/generated-schemas.json +OPENAPI_SPEC ?= api/v1alpha1/public/openapi.yaml +SWAGGER_UI_PORT ?= 8282 + $(GOLANGCI_LINT): $(TOOLS_DIR)/go.mod cd $(TOOLS_DIR); go build -tags=tools -o $(abspath $(TOOLS_BIN_DIR))/golangci-lint github.com/golangci/golangci-lint/v2/cmd/golangci-lint @@ -109,6 +117,16 @@ help: @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 " codegen-passthrough Generate passthrough types from HyperShift" + @echo " codegen-registry Generate field metadata registry from markers" + @echo " codegen-verify Verify codegen outputs compile" + @echo " codegen Run full codegen pipeline (passthrough + registry + verify)" + @echo " verify-codegen Fail if codegen outputs are out of date" + @echo " codegen-conversion Generate REST types and conversion functions from CRD types" + @echo " verify-conversion Fail if conversion 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 "" @@ -134,6 +152,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 @@ -258,7 +277,7 @@ lint: $(GOLANGCI_LINT) verify: cd hyperfleet-db && go mod tidy - cd hyperfleet-operator/api && go mod tidy + cd api && go mod tidy cd hyperfleet-operator && go mod tidy cd platform-api && go mod tidy cd test && go mod tidy @@ -266,7 +285,7 @@ verify: cd hack/api-codegen && go mod tidy git diff --exit-code \ hyperfleet-db/go.mod hyperfleet-db/go.sum \ - hyperfleet-operator/api/go.mod hyperfleet-operator/api/go.sum \ + api/go.mod api/go.sum \ hyperfleet-operator/go.mod hyperfleet-operator/go.sum \ platform-api/go.mod platform-api/go.sum \ test/go.mod test/go.sum \ @@ -275,7 +294,7 @@ verify: deps: cd hyperfleet-db && go mod download && go mod tidy - cd hyperfleet-operator/api && go mod download && go mod tidy + cd api && go mod download && go mod tidy cd hyperfleet-operator && go mod download && go mod tidy cd platform-api && go mod download && go mod tidy cd test && go mod download && go mod tidy @@ -284,13 +303,13 @@ deps: # ── Code Generation ────────────────────────────────────────────────────── manifests: $(CONTROLLER_GEN) - cd hyperfleet-operator && $(CONTROLLER_GEN) crd paths="./api/..." output:crd:dir=config/crd/bases + cd hyperfleet-operator && $(CONTROLLER_GEN) crd:allowDangerousTypes=true paths="../api/..." output:crd:dir=config/crd/bases generate: $(CONTROLLER_GEN) - cd hyperfleet-operator && $(CONTROLLER_GEN) object paths="./api/..." + $(CONTROLLER_GEN) object paths="./api/..." generate-clientset: $(CLIENT_GEN) $(WIRE_GEN) - cd hyperfleet-operator/api && $(CLIENT_GEN) \ + cd api && $(CLIENT_GEN) \ --input-base "$(SDK_API_PKG)" \ --input "$(SDK_INPUT)" \ --clientset-name "$(SDK_CLIENTSET)" \ @@ -315,6 +334,70 @@ generate-clientset: $(CLIENT_GEN) $(WIRE_GEN) verify-clientset: generate-clientset git diff --exit-code clientset/ +codegen-passthrough: build-api-codegen + cd api && ../bin/passthrough-gen \ + -import-path github.com/openshift/hypershift/api/hypershift/v1beta1 \ + -types HostedClusterSpec,NodePoolSpec \ + -output-dir v1alpha1 \ + -package v1alpha1 + +codegen-registry: generate build-api-codegen + ./bin/marker-scanner \ + -input-dirs api/v1alpha1 \ + -output-file hack/api-codegen/pkg/registry/field_metadata.go \ + $(if $(VERBOSE),-verbose) + +codegen-verify: codegen-registry + cd api && go build ./... + cd platform-api && go build ./... + +codegen: codegen-verify + +verify-codegen: codegen + git diff --exit-code api/v1alpha1/zz_generated.deepcopy.go + git diff --exit-code hack/api-codegen/pkg/registry/ + +CONVERSION_OUTPUT_DIR ?= platform-api/pkg/conversion/v1alpha1 +CONVERSION_OUTPUT_PKG ?= github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/conversion +CONVERSION_CRD_PKG ?= github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1 +CONVERSION_REST_DIR ?= api/v1alpha1/public +CONVERSION_REST_PKG ?= github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1/public + +codegen-conversion: codegen-registry build-api-codegen + ./bin/conversion-gen \ + --api-version=v1alpha1 \ + --crd-package=$(CONVERSION_CRD_PKG) \ + --input-dirs=./api/v1alpha1 \ + --output-dir=$(CONVERSION_OUTPUT_DIR) \ + --output-package=$(CONVERSION_OUTPUT_PKG) \ + --rest-output-dir=$(CONVERSION_REST_DIR) \ + --rest-package=$(CONVERSION_REST_PKG) + +verify-conversion: codegen-conversion + cd api && go build ./... + cd platform-api && go build ./... + git diff --exit-code $(CONVERSION_REST_DIR)/ $(CONVERSION_OUTPUT_DIR)/ platform-api/pkg/conversion/types.go + +generate-openapi: codegen-registry + cd hack/api-codegen && go build -o ../../bin/openapi-gen ./cmd/openapi-gen + ./bin/openapi-gen \ + -input-dirs ./api/v1alpha1 \ + -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: + @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 setup-envtest: $(SETUP_ENVTEST) diff --git a/README.md b/README.md index e4e3aa88..3d1cfda2 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,14 @@ make help # full target list ``` hyperfleet-db/go.mod ← standalone -hyperfleet-operator/api/go.mod ← standalone (CRD types) -hyperfleet-operator/go.mod ← requires: hyperfleet-db, hyperfleet-operator/api -platform-api/go.mod ← requires: hyperfleet-db, hyperfleet-operator/api +api/go.mod ← standalone (CRD types) +hyperfleet-operator/go.mod ← requires: hyperfleet-db, api +platform-api/go.mod ← requires: hyperfleet-db, api ``` ## Docs -- [OpenAPI spec](platform-api/openapi/openapi.yaml) +- [OpenAPI spec](api/v1alpha1/public/openapi.yaml) - [ZOA Trusted Actions](docs/api/zoa-endpoints.md) - [Authorization](docs/authz.md) - [Konflux / Quay image tags](docs/konflux/quay-image-tags.md) diff --git a/hyperfleet-operator/api/go.mod b/api/go.mod similarity index 83% rename from hyperfleet-operator/api/go.mod rename to api/go.mod index 3ead2192..c5148657 100644 --- a/hyperfleet-operator/api/go.mod +++ b/api/go.mod @@ -1,9 +1,11 @@ -module github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api +module github.com/openshift-online/rosa-hyperfleet-api/api 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.35.1 k8s.io/apimachinery v0.36.0 ) @@ -13,13 +15,11 @@ require ( 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/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/net v0.56.0 // indirect golang.org/x/text v0.38.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - k8s.io/api v0.35.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 diff --git a/hyperfleet-operator/api/go.sum b/api/go.sum similarity index 100% rename from hyperfleet-operator/api/go.sum rename to api/go.sum diff --git a/hyperfleet-operator/api/v1alpha1/cluster_types.go b/api/v1alpha1/cluster_types.go similarity index 74% rename from hyperfleet-operator/api/v1alpha1/cluster_types.go rename to api/v1alpha1/cluster_types.go index 3f3e7c60..9bc205f6 100644 --- a/hyperfleet-operator/api/v1alpha1/cluster_types.go +++ b/api/v1alpha1/cluster_types.go @@ -37,22 +37,58 @@ 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 { + // 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. + // +hyperfleet:write-mode=mutable + // +optional + ExpirationTimestamp *metav1.Time `json:"expirationTimestamp,omitempty"` + + // Properties are arbitrary key-value pairs for customer metadata. + // +hyperfleet:write-mode=mutable + // +kubebuilder:validation:MaxProperties=100 + // +optional + Properties map[string]string `json:"properties,omitempty"` + + // Tags are customer-defined labels for organizational purposes. + // +hyperfleet:write-mode=mutable + // +openshift:enable:FeatureGate=HyperFleetAutoScaling + // +kubebuilder:validation:MaxProperties=100 + // +optional + Tags map[string]string `json:"tags,omitempty"` + + // AccountID is the AWS account that owns this cluster. + // +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. - // Used to bootstrap the initial cluster-admin RBAC mapping. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set // +optional // +kubebuilder:validation:Pattern=`^arn:aws:` CreatorARN string `json:"creatorARN,omitempty"` - // ExpirationTimestamp is the time after which the cluster will be - // automatically deleted. If nil, the cluster has no expiration. + // InternalID is a platform-assigned unique identifier. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set // +optional - ExpirationTimestamp *metav1.Time `json:"expirationTimestamp,omitempty"` + InternalID string `json:"internalId,omitempty"` - // HostedCluster is the full HyperShift HostedClusterSpec. The customer provides - // the fields they care about; the operator overrides platform-managed fields - // (InfraID, DNS, PullSecret, Services, etc.) at render time. + // HostedCluster contains the upstream HyperShift fields, mirrored as + // passthrough types with per-field visibility and write-mode markers. // +kubebuilder:validation:Required - HostedCluster hypershiftv1beta1.HostedClusterSpec `json:"hostedCluster"` + HostedCluster HostedClusterSpecPassthrough `json:"hostedCluster"` } // ClusterStatus defines the observed state of a Cluster. diff --git a/api/v1alpha1/configuration.go b/api/v1alpha1/configuration.go new file mode 100644 index 00000000..93249365 --- /dev/null +++ b/api/v1alpha1/configuration.go @@ -0,0 +1,232 @@ +package v1alpha1 + +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 + // +kubebuilder:validation:MaxProperties=32 + SystemReserved map[string]string `json:"systemReserved,omitempty"` + + // +hyperfleet:write-mode=immutable + // +kubebuilder:validation:MaxProperties=32 + KubeReserved map[string]string `json:"kubeReserved,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +kubebuilder:validation:MaxProperties=32 + EvictionHard map[string]string `json:"evictionHard,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +kubebuilder:validation:MaxProperties=32 + EvictionSoft map[string]string `json:"evictionSoft,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +kubebuilder:validation:MaxProperties=32 + 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 + // +kubebuilder:validation:MaxProperties=32 + 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 + // +kubebuilder:validation:MaxItems=256 + 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 + // +kubebuilder:validation:MaxItems=128 + AllowedKernelArguments []string `json:"allowedKernelArguments,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +kubebuilder:validation:MaxItems=128 + KernelArguments []string `json:"kernelArguments,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +kubebuilder:validation:MaxItems=64 + SystemdUnits []SystemdUnit `json:"systemdUnits,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +kubebuilder:validation:MaxItems=256 + Files []FileSpec `json:"files,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + KernelType *string `json:"kernelType,omitempty"` + + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +kubebuilder:validation:MaxItems=64 + Extensions []string `json:"extensions,omitempty"` +} + +type SystemdUnit struct { + Name string `json:"name"` + Enabled *bool `json:"enabled,omitempty"` + // +kubebuilder:validation:MaxLength=65536 + Contents string `json:"contents,omitempty"` + // +kubebuilder:validation:MaxItems=16 + Dropins []SystemdDropin `json:"dropins,omitempty"` +} + +type SystemdDropin struct { + Name string `json:"name"` + // +kubebuilder:validation:MaxLength=32768 + Contents string `json:"contents,omitempty"` +} + +type FileSpec struct { + Path string `json:"path"` + // +kubebuilder:validation:MaxLength=262144 + 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/hyperfleet-operator/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go similarity index 100% rename from hyperfleet-operator/api/v1alpha1/groupversion_info.go rename to api/v1alpha1/groupversion_info.go diff --git a/api/v1alpha1/hostedclusterspec.passthrough.go b/api/v1alpha1/hostedclusterspec.passthrough.go new file mode 100644 index 00000000..8fbe4203 --- /dev/null +++ b/api/v1alpha1/hostedclusterspec.passthrough.go @@ -0,0 +1,219 @@ +// Code generated by passthrough-gen. DO NOT EDIT. + +package v1alpha1 + +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=true + // +hyperfleet:write-mode=mutable + 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=true + // +hyperfleet:write-mode=mutable + 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=true + // +hyperfleet:write-mode=mutable + 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 + // +optional + AutoNode hypershiftv1beta1.AutoNode `json:"autoNode,omitzero"` + // etcd specifies configuration for the control plane etcd cluster. The + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=mutable + 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 + // +kubebuilder:validation:MaxItems=10 + 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 *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=true + // +hyperfleet:write-mode=mutable + // +kubebuilder:validation:MaxItems=50 + 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 + // +kubebuilder:validation:MaxProperties=100 + 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 + // +kubebuilder:validation:MaxItems=50 + 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 + // +kubebuilder:validation:MaxProperties=100 + 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=true + // +hyperfleet:write-mode=mutable + 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=true + // +hyperfleet:write-mode=mutable + 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 + // +kubebuilder:validation:MaxProperties=100 + 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 + // +kubebuilder:validation:MaxItems=50 + 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 + // +optional + OSImageStream hypershiftv1beta1.OSImageStreamReference `json:"osImageStream,omitzero"` +} diff --git a/hyperfleet-operator/api/v1alpha1/install/install.go b/api/v1alpha1/install/install.go similarity index 90% rename from hyperfleet-operator/api/v1alpha1/install/install.go rename to api/v1alpha1/install/install.go index e1da6de4..8c8a6eef 100644 --- a/hyperfleet-operator/api/v1alpha1/install/install.go +++ b/api/v1alpha1/install/install.go @@ -18,7 +18,7 @@ limitations under the License. package install import ( - v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" "k8s.io/apimachinery/pkg/runtime" ) diff --git a/hyperfleet-operator/api/v1alpha1/managementcluster_types.go b/api/v1alpha1/managementcluster_types.go similarity index 100% rename from hyperfleet-operator/api/v1alpha1/managementcluster_types.go rename to api/v1alpha1/managementcluster_types.go diff --git a/hyperfleet-operator/api/v1alpha1/manifest_types.go b/api/v1alpha1/manifest_types.go similarity index 100% rename from hyperfleet-operator/api/v1alpha1/manifest_types.go rename to api/v1alpha1/manifest_types.go diff --git a/hyperfleet-operator/api/v1alpha1/nodepool_types.go b/api/v1alpha1/nodepool_types.go similarity index 73% rename from hyperfleet-operator/api/v1alpha1/nodepool_types.go rename to api/v1alpha1/nodepool_types.go index 133f3e7c..d7567309 100644 --- a/hyperfleet-operator/api/v1alpha1/nodepool_types.go +++ b/api/v1alpha1/nodepool_types.go @@ -17,7 +17,6 @@ limitations under the License. package v1alpha1 import ( - hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -36,11 +35,39 @@ const ( // NodePoolSpec defines the desired state of a NodePool. // The parent Cluster is identified by the shared metadata.Namespace (cluster UUID). type NodePoolSpec struct { - // 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. + // 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 controls whether unhealthy nodes are automatically replaced. + // +hyperfleet:write-mode=mutable + // +optional + AutoRepair *bool `json:"autoRepair,omitempty"` + + // Labels are customer-defined labels applied to nodes in this pool. + // +hyperfleet:write-mode=mutable + // +kubebuilder:validation:MaxProperties=100 + // +optional + Labels map[string]string `json:"labels,omitempty"` + + // AccountID is the AWS account that owns this node pool. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +optional + AccountID string `json:"accountId,omitempty"` + + // InternalPoolID is a platform-assigned unique identifier. + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +optional + InternalPoolID string `json:"internalPoolId,omitempty"` + + // NodePool contains the upstream HyperShift fields, mirrored as + // passthrough types with per-field visibility and write-mode markers. // +kubebuilder:validation:Required - NodePool hypershiftv1beta1.NodePoolSpec `json:"nodePool"` + NodePool NodePoolSpecPassthrough `json:"nodePool"` } // NodePoolStatus defines the observed state of a NodePool. diff --git a/hyperfleet-operator/api/v1alpha1/placement_types.go b/api/v1alpha1/placement_types.go similarity index 100% rename from hyperfleet-operator/api/v1alpha1/placement_types.go rename to api/v1alpha1/placement_types.go diff --git a/api/v1alpha1/public/.gitkeep b/api/v1alpha1/public/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/api/v1alpha1/public/cluster_types.go b/api/v1alpha1/public/cluster_types.go new file mode 100644 index 00000000..7cb63e9c --- /dev/null +++ b/api/v1alpha1/public/cluster_types.go @@ -0,0 +1,14 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +// Cluster is the Schema for the clusters API. +// It represents a ROSA HCP cluster whose lifecycle is managed by the hyperfleet-operator. +// metadata.Name is the human-readable cluster name; metadata.Namespace is the cluster UUID. +// The owning account is the label hyperfleet.io/account-id. +type Cluster struct { + // +required + Spec ClusterSpec `json:"spec"` + // +optional + Status ClusterStatus `json:"status,omitzero"` +} diff --git a/api/v1alpha1/public/clusterconfiguration_types.go b/api/v1alpha1/public/clusterconfiguration_types.go new file mode 100644 index 00000000..6d0eb159 --- /dev/null +++ b/api/v1alpha1/public/clusterconfiguration_types.go @@ -0,0 +1,15 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +// 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 { + // 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"` +} diff --git a/api/v1alpha1/public/clusterspec_types.go b/api/v1alpha1/public/clusterspec_types.go new file mode 100644 index 00000000..9d405e9d --- /dev/null +++ b/api/v1alpha1/public/clusterspec_types.go @@ -0,0 +1,41 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ClusterSpec defines the desired state of a ROSA HCP cluster. +// 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 { + // 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. + // +hyperfleet:write-mode=mutable + // +optional + ExpirationTimestamp *metav1.Time `json:"expirationTimestamp,omitempty"` + // Properties are arbitrary key-value pairs for customer metadata. + // +hyperfleet:write-mode=mutable + // +kubebuilder:validation:MaxProperties=100 + // +optional + Properties map[string]string `json:"properties,omitempty"` + // Tags are customer-defined labels for organizational purposes. + // +hyperfleet:write-mode=mutable + // +openshift:enable:FeatureGate=HyperFleetAutoScaling + // +kubebuilder:validation:MaxProperties=100 + // +optional + Tags map[string]string `json:"tags,omitempty"` + // HostedCluster contains the upstream HyperShift fields, mirrored as + // passthrough types with per-field visibility and write-mode markers. + // +kubebuilder:validation:Required + HostedCluster HostedClusterSpecPassthrough `json:"hostedCluster"` +} diff --git a/api/v1alpha1/public/clusterstatus_types.go b/api/v1alpha1/public/clusterstatus_types.go new file mode 100644 index 00000000..795c290b --- /dev/null +++ b/api/v1alpha1/public/clusterstatus_types.go @@ -0,0 +1,34 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +import ( + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" + hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ClusterStatus defines the observed state of a Cluster. +type ClusterStatus struct { + // Conditions represent the latest observations of the cluster's state. + // Known condition types: Synced, Available, Degraded. + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + // Phase summarizes the cluster's lifecycle state. + // +optional + Phase v1alpha1.ClusterPhase `json:"phase,omitempty"` + // ControlPlaneEndpoint is the API server endpoint for the hosted cluster. + // +optional + ControlPlaneEndpoint hypershiftv1beta1.APIEndpoint `json:"controlPlaneEndpoint,omitempty"` + // Version is the running OpenShift version. + // +optional + Version string `json:"version,omitempty"` + // PlacementRef references the Placement that assigned this cluster to a management cluster. + // +optional + PlacementRef *PlacementReference `json:"placementRef,omitempty"` + // ObservedGeneration is the most recent generation observed by the controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} diff --git a/api/v1alpha1/public/filespec_types.go b/api/v1alpha1/public/filespec_types.go new file mode 100644 index 00000000..b131f342 --- /dev/null +++ b/api/v1alpha1/public/filespec_types.go @@ -0,0 +1,14 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +// FileSpec is the REST representation of FileSpec (visible fields only) +type FileSpec struct { + Path string `json:"path"` + // +kubebuilder:validation:MaxLength=262144 + 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/v1alpha1/public/hostedclusterspecpassthrough_types.go b/api/v1alpha1/public/hostedclusterspecpassthrough_types.go new file mode 100644 index 00000000..79e1f0af --- /dev/null +++ b/api/v1alpha1/public/hostedclusterspecpassthrough_types.go @@ -0,0 +1,57 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +import ( + hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" +) + +// 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=true + // +hyperfleet:write-mode=mutable + Release hypershiftv1beta1.Release `json:"release"` + // 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=true + // +hyperfleet:write-mode=mutable + Platform hypershiftv1beta1.PlatformSpec `json:"platform"` + // networking specifies network configuration for the hosted cluster. + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=mutable + Networking hypershiftv1beta1.ClusterNetworking `json:"networking"` + // autoNode specifies the configuration for automatic node provisioning and lifecycle management. + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=service-set + // +optional + AutoNode hypershiftv1beta1.AutoNode `json:"autoNode,omitzero"` + // etcd specifies configuration for the control plane etcd cluster. The + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=mutable + Etcd hypershiftv1beta1.EtcdSpec `json:"etcd"` + // configuration specifies configuration for individual OCP components in the + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=service-set + Configuration *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"` + // imageContentSources specifies image mirrors that can be used by cluster + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=mutable + // +kubebuilder:validation:MaxItems=50 + ImageContentSources []hypershiftv1beta1.ImageContentSource `json:"imageContentSources,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"` +} diff --git a/api/v1alpha1/public/kubeletconfig_types.go b/api/v1alpha1/public/kubeletconfig_types.go new file mode 100644 index 00000000..0d6ce497 --- /dev/null +++ b/api/v1alpha1/public/kubeletconfig_types.go @@ -0,0 +1,42 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// 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 + // +kubebuilder:validation:MaxProperties=32 + SystemReserved map[string]string `json:"systemReserved,omitempty"` + // +hyperfleet:write-mode=immutable + // +kubebuilder:validation:MaxProperties=32 + KubeReserved map[string]string `json:"kubeReserved,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"` + // +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"` +} diff --git a/api/v1alpha1/public/machineconfigspec_types.go b/api/v1alpha1/public/machineconfigspec_types.go new file mode 100644 index 00000000..f5026d0c --- /dev/null +++ b/api/v1alpha1/public/machineconfigspec_types.go @@ -0,0 +1,11 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +// MachineConfigSpec specifies machine-level configuration. +type MachineConfigSpec struct { + // +openshift:enable:FeatureGate=HyperFleetMachineConfig + // +hyperfleet:write-mode=immutable + // +kubebuilder:validation:MaxItems=128 + AllowedKernelArguments []string `json:"allowedKernelArguments,omitempty"` +} diff --git a/api/v1alpha1/public/managementcluster_types.go b/api/v1alpha1/public/managementcluster_types.go new file mode 100644 index 00000000..aed01023 --- /dev/null +++ b/api/v1alpha1/public/managementcluster_types.go @@ -0,0 +1,12 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +// ManagementCluster is the Schema for the managementclusters API. +// It represents a management cluster in the fleet. +type ManagementCluster struct { + // +required + Spec ManagementClusterSpec `json:"spec"` + // +optional + Status ManagementClusterStatus `json:"status,omitzero"` +} diff --git a/api/v1alpha1/public/managementclusterspec_types.go b/api/v1alpha1/public/managementclusterspec_types.go new file mode 100644 index 00000000..b8c9f861 --- /dev/null +++ b/api/v1alpha1/public/managementclusterspec_types.go @@ -0,0 +1,10 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +// ManagementClusterSpec defines the desired state of a ManagementCluster. +type ManagementClusterSpec struct { + // Region is the AWS region (e.g. us-east-1). + // +kubebuilder:validation:MinLength=1 + Region string `json:"region"` +} diff --git a/api/v1alpha1/public/managementclusterstatus_types.go b/api/v1alpha1/public/managementclusterstatus_types.go new file mode 100644 index 00000000..918355b0 --- /dev/null +++ b/api/v1alpha1/public/managementclusterstatus_types.go @@ -0,0 +1,16 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ManagementClusterStatus defines the observed state of a ManagementCluster. +type ManagementClusterStatus struct { + // Conditions represent the latest observations of the management cluster's state. + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} diff --git a/api/v1alpha1/public/manifest_types.go b/api/v1alpha1/public/manifest_types.go new file mode 100644 index 00000000..50baae79 --- /dev/null +++ b/api/v1alpha1/public/manifest_types.go @@ -0,0 +1,12 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +// Manifest is the Schema for the manifests API. +// It deploys arbitrary Kubernetes resources to a management cluster via DynamoDB desires. +type Manifest struct { + // +required + Spec ManifestSpec `json:"spec"` + // +optional + Status ManifestStatus `json:"status,omitzero"` +} diff --git a/api/v1alpha1/public/manifestspec_types.go b/api/v1alpha1/public/manifestspec_types.go new file mode 100644 index 00000000..4630dd57 --- /dev/null +++ b/api/v1alpha1/public/manifestspec_types.go @@ -0,0 +1,14 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +// ManifestSpec defines a set of Kubernetes resources to apply on a management cluster. +// +kubebuilder:validation:XValidation:rule="self.managementCluster == oldSelf.managementCluster",message="spec.managementCluster is immutable" +type ManifestSpec struct { + // ManagementCluster is the target MC ID (e.g. mc01). + // +kubebuilder:validation:MinLength=1 + ManagementCluster string `json:"managementCluster"` + // Resources is the list of Kubernetes resources to apply on the MC. + // +kubebuilder:validation:MinItems=1 + Resources []ResourceTemplate `json:"resources"` +} diff --git a/api/v1alpha1/public/manifeststatus_types.go b/api/v1alpha1/public/manifeststatus_types.go new file mode 100644 index 00000000..542564d4 --- /dev/null +++ b/api/v1alpha1/public/manifeststatus_types.go @@ -0,0 +1,30 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +import ( + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ManifestStatus defines the observed state of a Manifest. +type ManifestStatus struct { + // Conditions represent the latest observations of the manifest's state. + // Known condition types: Synced. + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + // Phase summarizes the manifest's lifecycle state. + // +optional + Phase v1alpha1.ManifestPhase `json:"phase,omitempty"` + // AppliedResources is the number of resources written as ApplyDesires. + // +optional + AppliedResources int32 `json:"appliedResources,omitempty"` + // ObservedGeneration is the most recent generation observed by the controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + // ResourceStatuses contains the mirrored live state of watched resources. + // +optional + ResourceStatuses []ResourceStatus `json:"resourceStatuses,omitempty"` +} diff --git a/api/v1alpha1/public/nodepool_types.go b/api/v1alpha1/public/nodepool_types.go new file mode 100644 index 00000000..bcd67107 --- /dev/null +++ b/api/v1alpha1/public/nodepool_types.go @@ -0,0 +1,13 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +// NodePool is the Schema for the nodepools API. +// It represents a set of worker nodes for a Cluster. +// The parent Cluster shares the same metadata.Namespace (cluster UUID). +type NodePool struct { + // +required + Spec NodePoolSpec `json:"spec"` + // +optional + Status NodePoolStatus `json:"status,omitzero"` +} diff --git a/api/v1alpha1/public/nodepoolspec_types.go b/api/v1alpha1/public/nodepoolspec_types.go new file mode 100644 index 00000000..6f132aa0 --- /dev/null +++ b/api/v1alpha1/public/nodepoolspec_types.go @@ -0,0 +1,26 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +// NodePoolSpec defines the desired state of a NodePool. +// The parent Cluster is identified by the shared metadata.Namespace (cluster UUID). +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 controls whether unhealthy nodes are automatically replaced. + // +hyperfleet:write-mode=mutable + // +optional + AutoRepair *bool `json:"autoRepair,omitempty"` + // Labels are customer-defined labels applied to nodes in this pool. + // +hyperfleet:write-mode=mutable + // +kubebuilder:validation:MaxProperties=100 + // +optional + Labels map[string]string `json:"labels,omitempty"` + // NodePool contains the upstream HyperShift fields, mirrored as + // passthrough types with per-field visibility and write-mode markers. + // +kubebuilder:validation:Required + NodePool NodePoolSpecPassthrough `json:"nodePool"` +} diff --git a/api/v1alpha1/public/nodepoolspecpassthrough_types.go b/api/v1alpha1/public/nodepoolspecpassthrough_types.go new file mode 100644 index 00000000..f17771ff --- /dev/null +++ b/api/v1alpha1/public/nodepoolspecpassthrough_types.go @@ -0,0 +1,19 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +import ( + hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" +) + +// NodePoolSpecPassthrough mirrors NodePoolSpec from upstream HyperShift +type NodePoolSpecPassthrough struct { + // platform specifies the underlying infrastructure provider for the NodePool + // +k8s:openapi-gen=true + // +hyperfleet:write-mode=mutable + 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=true + // +hyperfleet:write-mode=mutable + Replicas *int32 `json:"replicas,omitempty"` +} diff --git a/api/v1alpha1/public/nodepoolstatus_types.go b/api/v1alpha1/public/nodepoolstatus_types.go new file mode 100644 index 00000000..df9fd76a --- /dev/null +++ b/api/v1alpha1/public/nodepoolstatus_types.go @@ -0,0 +1,24 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +import ( + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NodePoolStatus defines the observed state of a NodePool. +type NodePoolStatus struct { + // Conditions represent the latest observations of the node pool's state. + // Known condition types: Synced, Ready. + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + // Phase summarizes the node pool's lifecycle state. + // +optional + Phase v1alpha1.NodePoolPhase `json:"phase,omitempty"` + // ObservedGeneration is the most recent generation observed by the controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} diff --git a/platform-api/openapi/openapi.yaml b/api/v1alpha1/public/openapi.yaml similarity index 89% rename from platform-api/openapi/openapi.yaml rename to api/v1alpha1/public/openapi.yaml index 8fdabf7e..49bea8cc 100644 --- a/platform-api/openapi/openapi.yaml +++ b/api/v1alpha1/public/openapi.yaml @@ -2257,42 +2257,62 @@ 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. + 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: - 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. + maxProperties: 100 + type: object + tags: + additionalProperties: + type: string + description: Tags are customer-defined labels for organizational purposes. + maxProperties: 100 + 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. + The parent Cluster is identified by the shared metadata.Namespace (cluster UUID). 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 controls whether unhealthy nodes are automatically replaced. + 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 in this pool. + maxProperties: 100 + type: object + nodePool: + $ref: '#/components/schemas/NodePoolSpecPassthrough' + required: + - nodePool + type: object Cluster: type: object description: A user cluster resource @@ -2942,6 +2962,162 @@ 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' + etcd: + description: etcd specifies configuration for the control plane etcd cluster. The + type: object + x-kubernetes-validations: + - message: Only managed configuration must be set when managementType is Managed + rule: 'self.managementType == ''Managed'' ? has(self.managed) : !has(self.managed)' + - message: Only unmanaged configuration must be set when managementType is Unmanaged + rule: 'self.managementType == ''Unmanaged'' ? has(self.unmanaged) : !has(self.unmanaged)' + 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 + maxItems: 50 + type: array + networking: + description: networking specifies network configuration for the hosted cluster. + type: object + x-kubernetes-validations: + - message: CIDR ranges in machineNetwork, clusterNetwork, and serviceNetwork must be unique and non-overlapping + rule: (!has(self.machineNetwork) && self.clusterNetwork.all(c, self.serviceNetwork.all(s, c.cidr != s.cidr)) || (has(self.machineNetwork) && (self.machineNetwork.all(m, self.clusterNetwork.all(c, m.cidr != c.cidr)) && self.machineNetwork.all(m, self.serviceNetwork.all(s, m.cidr != s.cidr)) && self.clusterNetwork.all(c, self.serviceNetwork.all(s, c.cidr != s.cidr))))) + - message: allocateNodeCIDRs can only be set to Enabled when networkType is 'Other' + rule: 'has(self.allocateNodeCIDRs) && self.allocateNodeCIDRs == ''Enabled'' ? self.networkType == ''Other'' : true' + 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 + platform: + description: platform specifies the underlying infrastructure provider for the cluster + type: object + release: + description: release specifies the desired OCP release payload for all the hosted cluster components. + type: object + required: + - etcd + - fips + - networking + - platform + - release + type: object + NodePoolSpecPassthrough: + additionalProperties: true + description: NodePoolSpecPassthrough mirrors NodePoolSpec from upstream HyperShift + properties: + platform: + description: platform specifies the underlying infrastructure provider for the NodePool + type: object + replicas: + description: replicas is the desired number of nodes the pool should maintain. If unset, the controller default value is 0. + format: int32 + type: integer + required: + - platform + 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 + maxProperties: 32 + 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 + maxProperties: 32 + type: object + type: object + MachineConfigSpec: + description: MachineConfigSpec specifies machine-level configuration. + properties: + allowedKernelArguments: + items: + type: string + maxItems: 128 + type: array + type: object responses: BadRequest: description: Bad request diff --git a/api/v1alpha1/public/placement_types.go b/api/v1alpha1/public/placement_types.go new file mode 100644 index 00000000..abf0b147 --- /dev/null +++ b/api/v1alpha1/public/placement_types.go @@ -0,0 +1,12 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +// Placement is the Schema for the placements API. +// It assigns a Cluster to a management cluster. +type Placement struct { + // +required + Spec PlacementSpec `json:"spec"` + // +optional + Status PlacementStatus `json:"status,omitzero"` +} diff --git a/api/v1alpha1/public/placementreference_types.go b/api/v1alpha1/public/placementreference_types.go new file mode 100644 index 00000000..c39345fb --- /dev/null +++ b/api/v1alpha1/public/placementreference_types.go @@ -0,0 +1,11 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +// PlacementReference identifies the management cluster assignment. +type PlacementReference struct { + // Name is the Placement CR name. + Name string `json:"name"` + // ManagementCluster is the target management cluster ID (e.g. mc01). + ManagementCluster string `json:"managementCluster"` +} diff --git a/api/v1alpha1/public/placementspec_types.go b/api/v1alpha1/public/placementspec_types.go new file mode 100644 index 00000000..510109e4 --- /dev/null +++ b/api/v1alpha1/public/placementspec_types.go @@ -0,0 +1,14 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +// PlacementSpec defines the desired placement of a Cluster on a management cluster. +// +kubebuilder:validation:XValidation:rule="self.clusterName == oldSelf.clusterName",message="spec.clusterName is immutable" +type PlacementSpec struct { + // ClusterName is the name of the Cluster CR this placement is for. + // +kubebuilder:validation:MinLength=1 + ClusterName string `json:"clusterName"` + // ManagementCluster is the target management cluster ID (e.g. mc01). + // +kubebuilder:validation:MinLength=1 + ManagementCluster string `json:"managementCluster"` +} diff --git a/api/v1alpha1/public/placementstatus_types.go b/api/v1alpha1/public/placementstatus_types.go new file mode 100644 index 00000000..6978cfe8 --- /dev/null +++ b/api/v1alpha1/public/placementstatus_types.go @@ -0,0 +1,24 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +import ( + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// PlacementStatus defines the observed state of a Placement. +type PlacementStatus struct { + // Conditions represent the latest observations of the placement's state. + // Known condition types: Bound. + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + // Phase summarizes the placement's state: Pending or Bound. + // +optional + Phase v1alpha1.PlacementPhase `json:"phase,omitempty"` + // ObservedGeneration is the most recent generation observed by the controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} diff --git a/api/v1alpha1/public/resourcestatus_types.go b/api/v1alpha1/public/resourcestatus_types.go new file mode 100644 index 00000000..2a40841a --- /dev/null +++ b/api/v1alpha1/public/resourcestatus_types.go @@ -0,0 +1,28 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// ResourceStatus holds the mirrored status of a watched resource from the MC. +type ResourceStatus struct { + // Resource is the plural resource name (e.g. "jobs"). + Resource string `json:"resource"` + // Name is metadata.name of the watched resource. + Name string `json:"name"` + // Namespace is metadata.namespace (empty for cluster-scoped resources). + // +optional + Namespace string `json:"namespace,omitempty"` + // Group is the API group (e.g. "batch", "" for core). + // +optional + Group string `json:"group,omitempty"` + // Version is the API version (e.g. "v1"). + // +optional + Version string `json:"version,omitempty"` + // Status is the .status sub-object of the live resource mirrored from the MC. + // Only the status is stored to avoid duplicating the full spec in etcd. + // +optional + Status runtime.RawExtension `json:"status,omitempty"` +} diff --git a/api/v1alpha1/public/resourcetemplate_types.go b/api/v1alpha1/public/resourcetemplate_types.go new file mode 100644 index 00000000..78fbb278 --- /dev/null +++ b/api/v1alpha1/public/resourcetemplate_types.go @@ -0,0 +1,27 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// ResourceTemplate describes a single Kubernetes resource to apply. +// The controller extracts apiVersion, metadata.name, and metadata.namespace +// from Content automatically. Only the plural resource name is required +// separately because kind-to-resource conversion needs a RESTMapper +// that the operator doesn't have for MC-side resources. kube-applier-aws +// uses the plural to build Kubernetes REST paths; if it derived the plural +// via discovery instead, this field could be dropped. +type ResourceTemplate struct { + // Resource is the plural resource name (e.g. "configmaps", "deployments"). + // +kubebuilder:validation:MinLength=1 + Resource string `json:"resource"` + // Content is the full Kubernetes resource manifest. + // Must include apiVersion, kind, and metadata.name at minimum. + Content runtime.RawExtension `json:"content"` + // Watch creates a ReadDesire for this resource, mirroring its live state + // from the MC back into status.resourceStatuses via kube-applier-aws. + // +optional + Watch bool `json:"watch,omitempty"` +} diff --git a/api/v1alpha1/public/systemddropin_types.go b/api/v1alpha1/public/systemddropin_types.go new file mode 100644 index 00000000..07f97558 --- /dev/null +++ b/api/v1alpha1/public/systemddropin_types.go @@ -0,0 +1,10 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +// SystemdDropin is the REST representation of SystemdDropin (visible fields only) +type SystemdDropin struct { + Name string `json:"name"` + // +kubebuilder:validation:MaxLength=32768 + Contents string `json:"contents,omitempty"` +} diff --git a/api/v1alpha1/public/systemdunit_types.go b/api/v1alpha1/public/systemdunit_types.go new file mode 100644 index 00000000..60261c2a --- /dev/null +++ b/api/v1alpha1/public/systemdunit_types.go @@ -0,0 +1,13 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package public + +// SystemdUnit is the REST representation of SystemdUnit (visible fields only) +type SystemdUnit struct { + Name string `json:"name"` + Enabled *bool `json:"enabled,omitempty"` + // +kubebuilder:validation:MaxLength=65536 + Contents string `json:"contents,omitempty"` + // +kubebuilder:validation:MaxItems=16 + Dropins []SystemdDropin `json:"dropins,omitempty"` +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..c1483257 --- /dev/null +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,1237 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +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(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.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 *ManagementCluster) DeepCopyInto(out *ManagementCluster) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagementCluster. +func (in *ManagementCluster) DeepCopy() *ManagementCluster { + if in == nil { + return nil + } + out := new(ManagementCluster) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ManagementCluster) 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 *ManagementClusterList) DeepCopyInto(out *ManagementClusterList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ManagementCluster, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagementClusterList. +func (in *ManagementClusterList) DeepCopy() *ManagementClusterList { + if in == nil { + return nil + } + out := new(ManagementClusterList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ManagementClusterList) 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 *ManagementClusterSpec) DeepCopyInto(out *ManagementClusterSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagementClusterSpec. +func (in *ManagementClusterSpec) DeepCopy() *ManagementClusterSpec { + if in == nil { + return nil + } + out := new(ManagementClusterSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagementClusterStatus) DeepCopyInto(out *ManagementClusterStatus) { + *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 ManagementClusterStatus. +func (in *ManagementClusterStatus) DeepCopy() *ManagementClusterStatus { + if in == nil { + return nil + } + out := new(ManagementClusterStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Manifest) DeepCopyInto(out *Manifest) { + *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 Manifest. +func (in *Manifest) DeepCopy() *Manifest { + if in == nil { + return nil + } + out := new(Manifest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Manifest) 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 *ManifestList) DeepCopyInto(out *ManifestList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Manifest, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManifestList. +func (in *ManifestList) DeepCopy() *ManifestList { + if in == nil { + return nil + } + out := new(ManifestList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ManifestList) 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 *ManifestSpec) DeepCopyInto(out *ManifestSpec) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]ResourceTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManifestSpec. +func (in *ManifestSpec) DeepCopy() *ManifestSpec { + if in == nil { + return nil + } + out := new(ManifestSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManifestStatus) DeepCopyInto(out *ManifestStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ResourceStatuses != nil { + in, out := &in.ResourceStatuses, &out.ResourceStatuses + *out = make([]ResourceStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManifestStatus. +func (in *ManifestStatus) DeepCopy() *ManifestStatus { + if in == nil { + return nil + } + out := new(ManifestStatus) + 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 *Placement) DeepCopyInto(out *Placement) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Placement. +func (in *Placement) DeepCopy() *Placement { + if in == nil { + return nil + } + out := new(Placement) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Placement) 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 *PlacementList) DeepCopyInto(out *PlacementList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Placement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlacementList. +func (in *PlacementList) DeepCopy() *PlacementList { + if in == nil { + return nil + } + out := new(PlacementList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PlacementList) 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 *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 *PlacementSpec) DeepCopyInto(out *PlacementSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlacementSpec. +func (in *PlacementSpec) DeepCopy() *PlacementSpec { + if in == nil { + return nil + } + out := new(PlacementSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlacementStatus) DeepCopyInto(out *PlacementStatus) { + *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 PlacementStatus. +func (in *PlacementStatus) DeepCopy() *PlacementStatus { + if in == nil { + return nil + } + out := new(PlacementStatus) + 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 *ResourceStatus) DeepCopyInto(out *ResourceStatus) { + *out = *in + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceStatus. +func (in *ResourceStatus) DeepCopy() *ResourceStatus { + if in == nil { + return nil + } + out := new(ResourceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceTemplate) DeepCopyInto(out *ResourceTemplate) { + *out = *in + in.Content.DeepCopyInto(&out.Content) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceTemplate. +func (in *ResourceTemplate) DeepCopy() *ResourceTemplate { + if in == nil { + return nil + } + out := new(ResourceTemplate) + 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/clientset/docs/architecture.md b/clientset/docs/architecture.md index 2837a4b1..cd02be00 100644 --- a/clientset/docs/architecture.md +++ b/clientset/docs/architecture.md @@ -19,7 +19,7 @@ It is built in two parts: ### Prerequisites: markers in the CRD types -`client-gen` generates a typed client for every type annotated with `// +genclient` in the operator API package (`hyperfleet-operator/api/v1alpha1`). Two types have this marker: +`client-gen` generates a typed client for every type annotated with `// +genclient` in the operator API package (`api/v1alpha1`). Two types have this marker: ```go // +genclient @@ -46,7 +46,7 @@ make verify-clientset # fail if generated output differs from committed file SDK_CLIENTSET ?= generated SDK_OUTPUT_DIR ?= $(abspath clientset) SDK_OUTPUT_PKG ?= $(SDK_MODULE)/clientset -WIRE_INPUT_DIR ?= $(abspath hyperfleet-operator/api/v1alpha1) +WIRE_INPUT_DIR ?= $(abspath api/v1alpha1) WIRE_OUTPUT_DIR ?= $(abspath clientset/transport) WRAPPERS_OUTPUT_DIR ?= $(abspath clientset/wrappers) ``` @@ -78,14 +78,14 @@ clientset/wrappers/ ### install package (hand-written, required by generated code) -The generated `scheme/register.go` imports `hyperfleet-operator/api/v1alpha1/install`, which does not exist in the operator module by default. It must be created manually: +The generated `scheme/register.go` imports `api/v1alpha1/install`, which does not exist in the api module by default. It must be created manually: ```go -// hyperfleet-operator/api/v1alpha1/install/install.go +// api/v1alpha1/install/install.go package install import ( - v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" "k8s.io/apimachinery/pkg/runtime" ) diff --git a/clientset/generated/fake/register.go b/clientset/generated/fake/register.go index eb1daa2a..4a2c8b37 100644 --- a/clientset/generated/fake/register.go +++ b/clientset/generated/fake/register.go @@ -19,7 +19,7 @@ limitations under the License. package fake import ( - v1alpha1internalversion "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + v1alpha1internalversion "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" diff --git a/clientset/generated/scheme/register.go b/clientset/generated/scheme/register.go index e0688c2b..9748eb16 100644 --- a/clientset/generated/scheme/register.go +++ b/clientset/generated/scheme/register.go @@ -19,7 +19,7 @@ limitations under the License. package scheme import ( - v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1/install" + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1/install" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" diff --git a/clientset/generated/typed/v1alpha1/internalversion/cluster.go b/clientset/generated/typed/v1alpha1/internalversion/cluster.go index 78678781..8ef28c47 100644 --- a/clientset/generated/typed/v1alpha1/internalversion/cluster.go +++ b/clientset/generated/typed/v1alpha1/internalversion/cluster.go @@ -21,8 +21,8 @@ package internalversion import ( context "context" + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" scheme "github.com/openshift-online/rosa-hyperfleet-api/clientset/generated/scheme" - v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" diff --git a/clientset/generated/typed/v1alpha1/internalversion/fake/fake_cluster.go b/clientset/generated/typed/v1alpha1/internalversion/fake/fake_cluster.go index 6d76b78d..1b879b39 100644 --- a/clientset/generated/typed/v1alpha1/internalversion/fake/fake_cluster.go +++ b/clientset/generated/typed/v1alpha1/internalversion/fake/fake_cluster.go @@ -19,8 +19,8 @@ limitations under the License. package fake import ( + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" internalversion "github.com/openshift-online/rosa-hyperfleet-api/clientset/generated/typed/v1alpha1/internalversion" - v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" gentype "k8s.io/client-go/gentype" ) diff --git a/clientset/generated/typed/v1alpha1/internalversion/fake/fake_nodepool.go b/clientset/generated/typed/v1alpha1/internalversion/fake/fake_nodepool.go index 92c8abd6..f802e06c 100644 --- a/clientset/generated/typed/v1alpha1/internalversion/fake/fake_nodepool.go +++ b/clientset/generated/typed/v1alpha1/internalversion/fake/fake_nodepool.go @@ -19,8 +19,8 @@ limitations under the License. package fake import ( + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" internalversion "github.com/openshift-online/rosa-hyperfleet-api/clientset/generated/typed/v1alpha1/internalversion" - v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" gentype "k8s.io/client-go/gentype" ) diff --git a/clientset/generated/typed/v1alpha1/internalversion/nodepool.go b/clientset/generated/typed/v1alpha1/internalversion/nodepool.go index ace6e1fe..8d21e839 100644 --- a/clientset/generated/typed/v1alpha1/internalversion/nodepool.go +++ b/clientset/generated/typed/v1alpha1/internalversion/nodepool.go @@ -21,8 +21,8 @@ package internalversion import ( context "context" + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" scheme "github.com/openshift-online/rosa-hyperfleet-api/clientset/generated/scheme" - v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" diff --git a/clientset/go.mod b/clientset/go.mod index 8e17c7d0..ee341744 100644 --- a/clientset/go.mod +++ b/clientset/go.mod @@ -5,7 +5,7 @@ go 1.26.3 require ( github.com/aws/aws-sdk-go-v2 v1.43.2 github.com/aws/aws-sdk-go-v2/credentials v1.19.30 - github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api v0.0.0 + github.com/openshift-online/rosa-hyperfleet-api/api v0.0.0 k8s.io/apimachinery v0.36.0 k8s.io/client-go v0.36.0 ) @@ -52,4 +52,4 @@ require ( sigs.k8s.io/yaml v1.6.0 // indirect ) -replace github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api => ../hyperfleet-operator/api +replace github.com/openshift-online/rosa-hyperfleet-api/api => ../api diff --git a/clientset/wrappers/wire_wrappers_generated.go b/clientset/wrappers/wire_wrappers_generated.go index 7d1777df..d9a1c9a3 100644 --- a/clientset/wrappers/wire_wrappers_generated.go +++ b/clientset/wrappers/wire_wrappers_generated.go @@ -29,7 +29,7 @@ import ( "k8s.io/client-go/rest" internalversion "github.com/openshift-online/rosa-hyperfleet-api/clientset/generated/typed/v1alpha1/internalversion" - v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" ) // ClusterInterface is the platform-scoped client for Cluster resources. diff --git a/clientset/wrappers/wrappers_test.go b/clientset/wrappers/wrappers_test.go index 0f34e33c..f76719ac 100644 --- a/clientset/wrappers/wrappers_test.go +++ b/clientset/wrappers/wrappers_test.go @@ -21,7 +21,7 @@ import ( "testing" "time" - v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" diff --git a/hack/api-codegen/README.md b/hack/api-codegen/README.md index 5c93c3a1..68d7a0c8 100644 --- a/hack/api-codegen/README.md +++ b/hack/api-codegen/README.md @@ -53,3 +53,51 @@ make build-api-codegen # Build all 7 generator binaries make test-api-codegen # Run tests make coverage-api-codegen # Generate coverage report ``` + +## Makefile codegen pipeline + +The top-level Makefile exposes the following targets for running the codegen pipeline: + +| Target | Description | +|--------|-------------| +| `codegen-passthrough` | Generate passthrough types from HyperShift into `api/v1alpha1/` | +| `codegen-registry` | Generate field metadata registry from `+hyperfleet:` markers | +| `codegen-conversion` | Generate REST types and Project/Unproject conversion functions | +| `codegen-verify` | Verify codegen outputs compile (`api` + `platform-api`) | +| `codegen` | Run full pipeline: passthrough + registry + verify | +| `verify-codegen` | Fail if codegen outputs are out of date (git diff check) | +| `verify-conversion` | Fail if conversion outputs are out of date | + +### Dependency chain + +``` +codegen + └─ codegen-verify + └─ codegen-registry + ├─ generate (controller-gen deepcopy on api/...) + └─ build-api-codegen (builds marker-scanner + conversion-gen + other tools) + └─ marker-scanner scans api/v1alpha1 → hack/api-codegen/pkg/registry/field_metadata.go + +codegen-conversion + └─ codegen-registry (needs field metadata to determine hidden/visible fields) + └─ build-api-codegen (builds conversion-gen) + └─ conversion-gen outputs: + api/v1alpha1/public/ (REST types, visible fields only, package "public") + platform-api/pkg/conversion/ + ├─ types.go (ServiceSetFields) + └─ v1alpha1/ + ├─ cluster.go (ProjectCluster, UnprojectCluster) + └─ nodepool.go (ProjectNodePool, UnprojectNodePool) +``` + +`codegen-passthrough` is intentionally **not** in the `codegen` chain yet — it needs to be run manually since `passthrough-gen` rewrites `api/v1alpha1/` files that are currently hand-curated (`hostedclusterspec.passthrough.go`). Once the passthrough codegen is fully wired in a future PR, it can be chained in. + +### Migration notes (from ROSAENG-61802 branch) + +The codegen targets were originally developed on the `ROSAENG-61802-field-validation-v2` branch targeting `api/public/v2alpha1/`. They were ported and refactored for the current type layout: + +- **Paths**: `api/public/v2alpha1/` → `api/v1alpha1/` +- **Package**: `v2alpha1` → `v1alpha1` +- **No `generate-public-deepcopy`**: Eliminated — the existing `generate` target already runs controller-gen on `api/...` which covers `v1alpha1` +- **`codegen-registry`** depends on `generate` instead of `generate-public-deepcopy` +- **`verify-codegen`** diffs `api/v1alpha1/` instead of `api/public/v2alpha1/` diff --git a/hack/api-codegen/cmd/conversion-gen/main.go b/hack/api-codegen/cmd/conversion-gen/main.go index a80991cd..ab263fd9 100644 --- a/hack/api-codegen/cmd/conversion-gen/main.go +++ b/hack/api-codegen/cmd/conversion-gen/main.go @@ -13,16 +13,22 @@ import ( func main() { var ( - apiVersion string - crdPackage string - inputDirs string - outputDir string + apiVersion string + crdPackage string + inputDirs string + outputDir string + outputPackage string + restOutputDir string + restPackage string ) flag.StringVar(&apiVersion, "api-version", "v1alpha1", "API version to generate for") flag.StringVar(&crdPackage, "crd-package", "", "Import path to CRD types (required)") flag.StringVar(&inputDirs, "input-dirs", "", "Comma-separated list of directories containing CRD source files (required)") flag.StringVar(&outputDir, "output-dir", "", "Output directory for generated code (required)") + flag.StringVar(&outputPackage, "output-package", "", "Import path for the parent package of the output directory (overrides default)") + flag.StringVar(&restOutputDir, "rest-output-dir", "", "Output directory for REST types (defaults to /rest)") + flag.StringVar(&restPackage, "rest-package", "", "Import path for the REST types package (defaults to //rest)") flag.Parse() if crdPackage == "" || inputDirs == "" || outputDir == "" { @@ -31,9 +37,10 @@ func main() { fmt.Fprintf(os.Stderr, "Example usage:\n") fmt.Fprintf(os.Stderr, " %s \\\n", os.Args[0]) fmt.Fprintf(os.Stderr, " --api-version=v1alpha1 \\\n") - fmt.Fprintf(os.Stderr, " --crd-package=github.com/openshift-online/rosa-hyperfleet-api/hack/api-codegen/api/v1alpha1 \\\n") + fmt.Fprintf(os.Stderr, " --crd-package=github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1 \\\n") fmt.Fprintf(os.Stderr, " --input-dirs=./api/v1alpha1 \\\n") - fmt.Fprintf(os.Stderr, " --output-dir=./pkg/conversion/v1alpha1\n") + fmt.Fprintf(os.Stderr, " --output-dir=./platform-api/pkg/conversion/v1alpha1 \\\n") + fmt.Fprintf(os.Stderr, " --output-package=github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/conversion\n") os.Exit(1) } @@ -50,12 +57,30 @@ func main() { // Create generator gen := conversion.NewGenerator(apiVersion, crdPackage, dirs, outputDir) + if outputPackage != "" { + gen.OutputPackage = outputPackage + } + if restOutputDir != "" { + gen.RESTOutputDir = restOutputDir + } + if restPackage != "" { + gen.RESTImportPath = restPackage + } log.Printf("Conversion code generator") log.Printf(" API Version: %s", apiVersion) log.Printf(" CRD Package: %s", crdPackage) log.Printf(" Input Dirs: %s", strings.Join(dirs, ", ")) log.Printf(" Output Dir: %s", outputDir) + if outputPackage != "" { + log.Printf(" Output Pkg: %s", outputPackage) + } + if restOutputDir != "" { + log.Printf(" REST Dir: %s", restOutputDir) + } + if restPackage != "" { + log.Printf(" REST Pkg: %s", restPackage) + } log.Println() // Generate @@ -63,8 +88,12 @@ func main() { log.Fatalf("Generation failed: %v", err) } + restTarget := gen.RESTOutputDir + if restTarget == "" { + restTarget = outputDir + "/rest" + } log.Println("✓ Successfully generated:") - log.Println(" - REST types (rest/)") + log.Printf(" - REST types (%s)", restTarget) log.Println(" - ServiceSetFields (../types.go)") log.Println(" - Conversion functions (cluster.go, nodepool.go)") } 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/cmd/openapi-merge/main.go b/hack/api-codegen/cmd/openapi-merge/main.go new file mode 100644 index 00000000..3d4689c9 --- /dev/null +++ b/hack/api-codegen/cmd/openapi-merge/main.go @@ -0,0 +1,279 @@ +package main + +import ( + "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 + for _, line := range strings.Split(string(yamlBytes), "\n") { + 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 { + lines := strings.Split(string(data), "\n") + if len(lines) > 0 && lines[len(lines)-1] == "" { + lines = lines[:len(lines)-1] + } + 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/conversion/generator.go b/hack/api-codegen/pkg/conversion/generator.go index 1c203310..95af338f 100644 --- a/hack/api-codegen/pkg/conversion/generator.go +++ b/hack/api-codegen/pkg/conversion/generator.go @@ -25,6 +25,13 @@ type Generator struct { OutputPackage string InputDirs []string + // RESTOutputDir overrides where REST type files are written. + // Defaults to OutputDir + "/rest" when empty. + RESTOutputDir string + // RESTImportPath is the Go import path for the REST types package. + // Defaults to OutputPackage + "//rest" when empty. + RESTImportPath string + knownTypes map[string]bool typeInfos map[string]*typeInfo emittedHelpers map[string]bool @@ -61,6 +68,25 @@ func NewGenerator(apiVersion, crdPackage string, inputDirs []string, outputDir s } } +func (g *Generator) restOutputDir() string { + if g.RESTOutputDir != "" { + return g.RESTOutputDir + } + return filepath.Join(g.OutputDir, "rest") +} + +func (g *Generator) restImportPath() string { + if g.RESTImportPath != "" { + return g.RESTImportPath + } + convPkgName := filepath.Base(g.OutputDir) + return g.outputImportPath() + "/" + convPkgName + "/rest" +} + +func (g *Generator) restPackageName() string { + return filepath.Base(g.restOutputDir()) +} + // Generate runs all three generation phases. func (g *Generator) Generate() error { if err := g.parseTypes(); err != nil { @@ -110,14 +136,15 @@ func (g *Generator) parseTypes() error { if !ok || !typeSpec.Name.IsExported() { continue } + + typeName := typeSpec.Name.Name + g.knownTypes[typeName] = true + structType, ok := typeSpec.Type.(*ast.StructType) if !ok { continue } - typeName := typeSpec.Name.Name - g.knownTypes[typeName] = true - ti := &typeInfo{ Name: typeName, StructType: structType, @@ -176,6 +203,14 @@ func (g *Generator) parseField(typeName string, field *ast.Field, name *ast.Iden } func (g *Generator) buildFieldPath(typeName, jsonName string) string { + switch typeName { + case "KubeletConfig": + return "kubelet." + jsonName + case "MachineConfigSpec": + return "machineConfig." + jsonName + case "ClusterConfiguration": + return jsonName + } switch { case strings.HasSuffix(typeName, "Spec"): return "spec." + jsonName @@ -243,6 +278,7 @@ type importInfo struct { NeedsV1alpha1 bool NeedsCorev1 bool NeedsConfigv1 bool + NeedsRuntime bool } // detectImports scans a list of Go type strings and determines which @@ -269,10 +305,13 @@ func (g *Generator) detectImports(goTypes []string) importInfo { if strings.Contains(base, "configv1.") { info.NeedsConfigv1 = true } + if strings.Contains(base, "runtime.") { + info.NeedsRuntime = true + } // Check if the unqualified type is from the CRD package if !strings.Contains(base, ".") && !g.isBuiltinType(base) { - if _, isCRDType := g.typeInfos[base]; isCRDType { + if g.knownTypes[base] { info.NeedsV1alpha1 = true } } @@ -297,8 +336,8 @@ func (g *Generator) qualifyType(goType string) string { return goType } - // If the type exists in our parsed CRD types, qualify with v1alpha1 - if _, ok := g.typeInfos[base]; ok { + // If the type exists in the CRD package, qualify with v1alpha1 + if g.knownTypes[base] { qualified := "v1alpha1." + base if isPointer { return "*" + qualified @@ -312,6 +351,27 @@ func (g *Generator) qualifyType(goType string) string { return goType } +// qualifyTypeForREST is like qualifyType but skips types that are also +// generated in the REST package (they're in the same package, no qualifier needed). +func (g *Generator) qualifyTypeForREST(goType string, restTypeSet map[string]bool) string { + base := goType + base = strings.TrimPrefix(base, "*") + base = strings.TrimPrefix(base, "[]") + + if strings.Contains(base, ".") { + return goType + } + if g.isBuiltinType(base) { + return goType + } + + if restTypeSet[base] { + return goType // Same REST package, no qualifier + } + + return g.qualifyType(goType) +} + func (g *Generator) isBuiltinType(t string) bool { switch t { case "string", "bool", "int", "int8", "int16", "int32", "int64", @@ -326,11 +386,12 @@ func (g *Generator) isBuiltinType(t string) bool { // restTypeData feeds the REST type template. type restTypeData struct { - TypeName string - DocComment string - Fields []restFieldData - Imports importInfo - CRDPackage string + PackageName string + TypeName string + DocComment string + Fields []restFieldData + Imports importInfo + CRDPackage string } type restFieldData struct { @@ -342,16 +403,25 @@ type restFieldData struct { var restTypeTmpl = template.Must(template.New("restType").Parse(`// Code generated by conversion-gen. DO NOT EDIT. -package rest +package {{ .PackageName }} -{{ if or .Imports.NeedsMetav1 .Imports.NeedsHyperShift .Imports.NeedsV1alpha1 -}} +{{ if or .Imports.NeedsMetav1 .Imports.NeedsHyperShift .Imports.NeedsV1alpha1 .Imports.NeedsCorev1 .Imports.NeedsConfigv1 .Imports.NeedsRuntime -}} import ( +{{- if .Imports.NeedsConfigv1 }} + configv1 "github.com/openshift/api/config/v1" +{{- end }} {{- if .Imports.NeedsHyperShift }} hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" {{- end }} +{{- if .Imports.NeedsCorev1 }} + corev1 "k8s.io/api/core/v1" +{{- end }} {{- if .Imports.NeedsMetav1 }} metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" {{- end }} +{{- if .Imports.NeedsRuntime }} + "k8s.io/apimachinery/pkg/runtime" +{{- end }} {{- if .Imports.NeedsV1alpha1 }} v1alpha1 "{{ .CRDPackage }}" {{- end }} @@ -369,7 +439,7 @@ type {{ .TypeName }} struct { `)) func (g *Generator) generateRESTTypes() error { - restDir := filepath.Join(g.OutputDir, "rest") + restDir := g.restOutputDir() if err := g.ensureDir(restDir); err != nil { return err } @@ -377,19 +447,30 @@ func (g *Generator) generateRESTTypes() error { // Discover resource types dynamically: types with both Spec and Status subtypes resourceTypes := g.discoverResourceTypes() + // Build the full set of types being generated as REST types (including passthrough) + restTypeSet := make(map[string]bool) + for _, t := range resourceTypes { + restTypeSet[t] = true + } + for typeName := range g.typeInfos { + if strings.Contains(typeName, "Passthrough") { + restTypeSet[typeName] = true + } + } + for _, typeName := range resourceTypes { ti, exists := g.typeInfos[typeName] if !exists { continue } - code, err := g.renderRESTType(ti) + code, err := g.renderRESTType(ti, restTypeSet) if err != nil { return fmt.Errorf("rendering REST type %s: %w", typeName, err) } filename := strings.ToLower(typeName) + "_types.go" - if err := g.writeFormattedFile(filepath.Join("rest", filename), code); err != nil { + if err := g.writeRESTFile(filename, code); err != nil { return fmt.Errorf("writing REST type %s: %w", typeName, err) } } @@ -398,12 +479,12 @@ func (g *Generator) generateRESTTypes() error { for typeName := range g.typeInfos { if strings.Contains(typeName, "Passthrough") { ti := g.typeInfos[typeName] - code, err := g.renderRESTType(ti) + code, err := g.renderRESTType(ti, restTypeSet) if err != nil { return fmt.Errorf("rendering REST type %s: %w", typeName, err) } filename := strings.ToLower(typeName) + "_types.go" - if err := g.writeFormattedFile(filepath.Join("rest", filename), code); err != nil { + if err := g.writeRESTFile(filename, code); err != nil { return fmt.Errorf("writing REST type %s: %w", typeName, err) } } @@ -467,7 +548,7 @@ func (g *Generator) addReferencedTypes(typeSet map[string]bool) { } } -func (g *Generator) renderRESTType(ti *typeInfo) (string, error) { +func (g *Generator) renderRESTType(ti *typeInfo, restTypeSet map[string]bool) (string, error) { var visibleFields []restFieldData var goTypes []string @@ -475,8 +556,13 @@ func (g *Generator) renderRESTType(ti *typeInfo) (string, error) { if fi.Hidden { continue } - qualifiedType := g.qualifyType(fi.GoType) - goTypes = append(goTypes, qualifiedType) + qualifiedType := g.qualifyTypeForREST(fi.GoType, restTypeSet) + // Only track types for import detection that aren't REST-local + base := strings.TrimPrefix(fi.GoType, "*") + base = strings.TrimPrefix(base, "[]") + if !restTypeSet[base] { + goTypes = append(goTypes, qualifiedType) + } jsonTag := fi.JSONName if fi.Field.Tag != nil { @@ -530,11 +616,12 @@ func (g *Generator) renderRESTType(ti *typeInfo) (string, error) { } data := restTypeData{ - TypeName: ti.Name, - DocComment: docComment, - Fields: visibleFields, - Imports: g.detectImports(goTypes), - CRDPackage: g.CRDPackage, + PackageName: g.restPackageName(), + TypeName: ti.Name, + DocComment: docComment, + Fields: visibleFields, + Imports: g.detectImports(goTypes), + CRDPackage: g.CRDPackage, } var buf bytes.Buffer @@ -729,7 +816,7 @@ import ( v1alpha1 "{{ .CRDPackage }}" "{{ .ParentPkg }}" - "{{ .RESTPkg }}" + rest "{{ .RESTPkg }}" ) // Project{{ .Resource }} converts CRD {{ .Resource }} to REST (visible fields only). @@ -784,17 +871,12 @@ func Unproject{{ .Resource }}(spec *rest.{{ .SpecType }}, enrichment *conversion {{- end }} if enrichment != nil { - enrichCRD(&crdSpec, enrichment) + ssData, _ := json.Marshal(enrichment) + _ = json.Unmarshal(ssData, &crdSpec) } return &crdSpec } - -// enrichCRD applies service-set fields from the enrichment struct onto the CRD spec. -func enrichCRD(crdSpec *v1alpha1.{{ .SpecType }}, enrichment *conversion.ServiceSetFields) { - data, _ := json.Marshal(enrichment) - _ = json.Unmarshal(data, crdSpec) -} {{ range .MirrorTypes }} // Convert{{ .BaseType }}_v1beta1_to_v1alpha1 converts upstream HyperShift to HyperFleet mirror type via JSON roundtrip. func Convert{{ .BaseType }}_v1beta1_to_v1alpha1(in interface{}) interface{} { @@ -860,9 +942,8 @@ func (g *Generator) renderConversionFunctions(resource string) (string, error) { specType := resource + "Spec" statusType := resource + "Status" - convPkgName := filepath.Base(g.OutputDir) parentPkg := g.outputImportPath() - restPkg := parentPkg + "/" + convPkgName + "/rest" + restPkg := g.restImportPath() specTI := g.typeInfos[specType] var specFields []convFieldData @@ -907,7 +988,7 @@ func (g *Generator) renderConversionFunctions(resource string) (string, error) { } data := conversionData{ - PackageName: convPkgName, + PackageName: filepath.Base(g.OutputDir), CRDPackage: g.CRDPackage, ParentPkg: parentPkg, RESTPkg: restPkg, @@ -940,6 +1021,20 @@ func (g *Generator) ensureDir(dir string) error { return os.MkdirAll(dir, 0755) } +func (g *Generator) writeRESTFile(filename, content string) error { + fullPath := filepath.Join(g.restOutputDir(), filename) + if err := g.ensureDir(filepath.Dir(fullPath)); err != nil { + return err + } + + formatted, err := format.Source([]byte(content)) + if err != nil { + formatted = []byte(content) + } + + return os.WriteFile(fullPath, formatted, 0644) +} + func (g *Generator) writeFormattedFile(relativePath, content string) error { fullPath := filepath.Join(g.OutputDir, relativePath) if err := g.ensureDir(filepath.Dir(fullPath)); 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 023f6fb4..10096dd8 100644 --- a/hack/api-codegen/pkg/markers/scanner.go +++ b/hack/api-codegen/pkg/markers/scanner.go @@ -19,21 +19,31 @@ var ( ) // NewScanner creates a new marker scanner -func NewScanner(inputDirs []string) *MarkerScanner { +func NewScanner(inputDirs []string, verbose bool) *MarkerScanner { return &MarkerScanner{ InputDirs: inputDirs, Registry: make(FieldRegistry), typeCache: make(map[string]*ast.StructType), + verbose: verbose, + } +} + +// logf writes a formatted log line to stderr when verbose mode is enabled. +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 } @@ -77,26 +87,49 @@ 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 - s.processStruct(typeName, structType, "", visited) + prefix := rootTypePrefix(typeName) + s.logf(" root type: %s (prefix=%q)", typeName, prefix) + s.processStruct(typeName, structType, prefix, visited) } } return nil } -// isRootType returns true for top-level CRD types (not Spec/Status/Passthrough types) +// 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 { - // 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" } @@ -126,6 +159,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 249c48f2..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) } @@ -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}, false) + 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 @@ -144,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..effce590 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 + verbose bool } diff --git a/hack/api-codegen/pkg/openapi/generator.go b/hack/api-codegen/pkg/openapi/generator.go index fe1e4afa..1f3c1482 100644 --- a/hack/api-codegen/pkg/openapi/generator.go +++ b/hack/api-codegen/pkg/openapi/generator.go @@ -46,7 +46,14 @@ func (g *Generator) Generate() error { } // Filter hidden fields using the registry - filterHiddenFields(definitions) + if err := filterHiddenFields(definitions); err != nil { + return fmt.Errorf("filtering hidden fields: %w", err) + } + + // 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", @@ -145,10 +152,28 @@ 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). -func filterHiddenFields(definitions map[string]apiextensionsv1.JSONSchemaProps) { +// +// Every definition that carries hidden fields must have an entry in +// typeToRegistryPrefix; an unmapped definition whose properties match +// hidden registry entries is an error (the mapping was forgotten). +func filterHiddenFields(definitions map[string]apiextensionsv1.JSONSchemaProps) error { hiddenPaths := make(map[string]bool) for path, meta := range registry.FieldRegistry { if meta.Hidden { @@ -157,13 +182,35 @@ func filterHiddenFields(definitions map[string]apiextensionsv1.JSONSchemaProps) } if len(hiddenPaths) == 0 { - return + return nil } for typeName, schema := range definitions { - pruned := pruneHiddenProperties(&schema, typeName, hiddenPaths) + prefix, ok := typeToRegistryPrefix[typeName] + if !ok { + if leaked := leakedHiddenProps(schema, hiddenPaths); len(leaked) > 0 { + return fmt.Errorf("definition %q has hidden fields %v but no entry in typeToRegistryPrefix", typeName, leaked) + } + continue + } + pruned := pruneHiddenProperties(&schema, prefix, hiddenPaths) definitions[typeName] = *pruned } + return nil +} + +// leakedHiddenProps returns property names from schema whose bare name +// matches a hidden entry in the registry. This catches definitions that +// should be mapped but aren't. +func leakedHiddenProps(schema apiextensionsv1.JSONSchemaProps, hidden map[string]bool) []string { + var leaked []string + for propName := range schema.Properties { + if hidden[propName] { + leaked = append(leaked, propName) + } + } + sort.Strings(leaked) + return leaked } // pruneHiddenProperties recursively removes properties whose registry path @@ -174,7 +221,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 +249,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..7e0c1c66 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 v1alpha1 package relative to this test file's location + // (hack/api-codegen/pkg/openapi/) → ../../../../api/v1alpha1 + v2alpha1Dir := "../../../../api/v1alpha1" + if _, err := os.Stat(v2alpha1Dir); err != nil { + t.Skipf("v1alpha1 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..d683f465 100644 --- a/hack/api-codegen/pkg/registry/field_metadata.go +++ b/hack/api-codegen/pkg/registry/field_metadata.go @@ -232,10 +232,6 @@ var FieldRegistry = map[string]FieldMeta{ WriteMode: ServiceSet, Hidden: true, }, - "machineConfig.fips": { - FieldPath: "machineConfig.fips", - WriteMode: Immutable, - }, "machineConfig.kernelArguments": { FieldPath: "machineConfig.kernelArguments", WriteMode: ServiceSet, @@ -362,6 +358,194 @@ 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.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, @@ -379,8 +563,7 @@ var FieldRegistry = map[string]FieldMeta{ }, "spec.hostedCluster.etcd": { FieldPath: "spec.hostedCluster.etcd", - WriteMode: ServiceSet, - Hidden: true, + WriteMode: Mutable, }, "spec.hostedCluster.fips": { FieldPath: "spec.hostedCluster.fips", @@ -388,8 +571,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", @@ -418,8 +600,7 @@ var FieldRegistry = map[string]FieldMeta{ }, "spec.hostedCluster.networking": { FieldPath: "spec.hostedCluster.networking", - WriteMode: ServiceSet, - Hidden: true, + WriteMode: Mutable, }, "spec.hostedCluster.nodeSelector": { FieldPath: "spec.hostedCluster.nodeSelector", @@ -441,8 +622,7 @@ var FieldRegistry = map[string]FieldMeta{ }, "spec.hostedCluster.platform": { FieldPath: "spec.hostedCluster.platform", - WriteMode: ServiceSet, - Hidden: true, + WriteMode: Mutable, }, "spec.hostedCluster.pullSecret": { FieldPath: "spec.hostedCluster.pullSecret", @@ -451,8 +631,7 @@ var FieldRegistry = map[string]FieldMeta{ }, "spec.hostedCluster.release": { FieldPath: "spec.hostedCluster.release", - WriteMode: ServiceSet, - Hidden: true, + WriteMode: Mutable, }, "spec.hostedCluster.secretEncryption": { FieldPath: "spec.hostedCluster.secretEncryption", @@ -538,6 +717,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, @@ -545,8 +729,7 @@ var FieldRegistry = map[string]FieldMeta{ }, "spec.nodePool.platform": { FieldPath: "spec.nodePool.platform", - WriteMode: ServiceSet, - Hidden: true, + WriteMode: Mutable, }, "spec.nodePool.release": { FieldPath: "spec.nodePool.release", @@ -555,8 +738,7 @@ var FieldRegistry = map[string]FieldMeta{ }, "spec.nodePool.replicas": { FieldPath: "spec.nodePool.replicas", - WriteMode: ServiceSet, - Hidden: true, + WriteMode: Mutable, }, "spec.nodePool.taints": { FieldPath: "spec.nodePool.taints", diff --git a/hack/api-codegen/pkg/registry/field_metadata.json b/hack/api-codegen/pkg/registry/field_metadata.json index 167ecff8..ab33abf6 100644 --- a/hack/api-codegen/pkg/registry/field_metadata.json +++ b/hack/api-codegen/pkg/registry/field_metadata.json @@ -211,10 +211,6 @@ "writeMode": "service-set", "hidden": true }, - { - "fieldPath": "machineConfig.fips", - "writeMode": "immutable" - }, { "fieldPath": "machineConfig.kernelArguments", "writeMode": "service-set", @@ -341,6 +337,194 @@ "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.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", @@ -358,8 +542,7 @@ }, { "fieldPath": "spec.hostedCluster.etcd", - "writeMode": "service-set", - "hidden": true + "writeMode": "mutable" }, { "fieldPath": "spec.hostedCluster.fips", @@ -367,8 +550,7 @@ }, { "fieldPath": "spec.hostedCluster.imageContentSources", - "writeMode": "service-set", - "hidden": true + "writeMode": "mutable" }, { "fieldPath": "spec.hostedCluster.infraID", @@ -397,8 +579,7 @@ }, { "fieldPath": "spec.hostedCluster.networking", - "writeMode": "service-set", - "hidden": true + "writeMode": "mutable" }, { "fieldPath": "spec.hostedCluster.nodeSelector", @@ -420,8 +601,7 @@ }, { "fieldPath": "spec.hostedCluster.platform", - "writeMode": "service-set", - "hidden": true + "writeMode": "mutable" }, { "fieldPath": "spec.hostedCluster.pullSecret", @@ -430,8 +610,7 @@ }, { "fieldPath": "spec.hostedCluster.release", - "writeMode": "service-set", - "hidden": true + "writeMode": "mutable" }, { "fieldPath": "spec.hostedCluster.secretEncryption", @@ -518,15 +697,19 @@ "hidden": true }, { - "fieldPath": "spec.nodePool.pausedUntil", + "fieldPath": "spec.nodePool.osImageStream", "writeMode": "service-set", "hidden": true }, { - "fieldPath": "spec.nodePool.platform", + "fieldPath": "spec.nodePool.pausedUntil", "writeMode": "service-set", "hidden": true }, + { + "fieldPath": "spec.nodePool.platform", + "writeMode": "mutable" + }, { "fieldPath": "spec.nodePool.release", "writeMode": "service-set", @@ -534,8 +717,7 @@ }, { "fieldPath": "spec.nodePool.replicas", - "writeMode": "service-set", - "hidden": true + "writeMode": "mutable" }, { "fieldPath": "spec.nodePool.taints", diff --git a/hyperfleet-operator/Containerfile b/hyperfleet-operator/Containerfile index f668c385..886884df 100644 --- a/hyperfleet-operator/Containerfile +++ b/hyperfleet-operator/Containerfile @@ -7,12 +7,14 @@ RUN mkdir -p /workspace && chown 1001:0 /workspace USER 1001 WORKDIR /workspace +ENV GOCACHE=/workspace/.cache/go-build 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 api/go.mod api/go.sum ./api/ COPY hyperfleet-operator/go.mod hyperfleet-operator/go.sum ./hyperfleet-operator/ RUN cd hyperfleet-operator && go mod download +COPY api/ ./api/ COPY hyperfleet-db/ ./hyperfleet-db/ COPY hyperfleet-operator/ ./hyperfleet-operator/ diff --git a/hyperfleet-operator/PROJECT b/hyperfleet-operator/PROJECT index 54059540..f9bc7075 100644 --- a/hyperfleet-operator/PROJECT +++ b/hyperfleet-operator/PROJECT @@ -15,7 +15,7 @@ resources: controller: true domain: hyperfleet.io kind: Cluster - path: github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1 + path: github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1 version: v1alpha1 - api: crdVersion: v1 @@ -23,7 +23,7 @@ resources: controller: true domain: hyperfleet.io kind: NodePool - path: github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1 + path: github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1 version: v1alpha1 - api: crdVersion: v1 @@ -31,6 +31,6 @@ resources: controller: true domain: hyperfleet.io kind: Placement - path: github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1 + path: github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1 version: v1alpha1 version: "3" diff --git a/hyperfleet-operator/api/v1alpha1/zz_generated.deepcopy.go b/hyperfleet-operator/api/v1alpha1/zz_generated.deepcopy.go deleted file mode 100644 index 7807487d..00000000 --- a/hyperfleet-operator/api/v1alpha1/zz_generated.deepcopy.go +++ /dev/null @@ -1,563 +0,0 @@ -//go:build !ignore_autogenerated - -// Code generated by controller-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - "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 *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 *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.ExpirationTimestamp != nil { - in, out := &in.ExpirationTimestamp, &out.ExpirationTimestamp - *out = (*in).DeepCopy() - } - 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 *ManagementCluster) DeepCopyInto(out *ManagementCluster) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagementCluster. -func (in *ManagementCluster) DeepCopy() *ManagementCluster { - if in == nil { - return nil - } - out := new(ManagementCluster) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ManagementCluster) 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 *ManagementClusterList) DeepCopyInto(out *ManagementClusterList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]ManagementCluster, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagementClusterList. -func (in *ManagementClusterList) DeepCopy() *ManagementClusterList { - if in == nil { - return nil - } - out := new(ManagementClusterList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ManagementClusterList) 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 *ManagementClusterSpec) DeepCopyInto(out *ManagementClusterSpec) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagementClusterSpec. -func (in *ManagementClusterSpec) DeepCopy() *ManagementClusterSpec { - if in == nil { - return nil - } - out := new(ManagementClusterSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ManagementClusterStatus) DeepCopyInto(out *ManagementClusterStatus) { - *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 ManagementClusterStatus. -func (in *ManagementClusterStatus) DeepCopy() *ManagementClusterStatus { - if in == nil { - return nil - } - out := new(ManagementClusterStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Manifest) DeepCopyInto(out *Manifest) { - *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 Manifest. -func (in *Manifest) DeepCopy() *Manifest { - if in == nil { - return nil - } - out := new(Manifest) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Manifest) 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 *ManifestList) DeepCopyInto(out *ManifestList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Manifest, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManifestList. -func (in *ManifestList) DeepCopy() *ManifestList { - if in == nil { - return nil - } - out := new(ManifestList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ManifestList) 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 *ManifestSpec) DeepCopyInto(out *ManifestSpec) { - *out = *in - if in.Resources != nil { - in, out := &in.Resources, &out.Resources - *out = make([]ResourceTemplate, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManifestSpec. -func (in *ManifestSpec) DeepCopy() *ManifestSpec { - if in == nil { - return nil - } - out := new(ManifestSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ManifestStatus) DeepCopyInto(out *ManifestStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.ResourceStatuses != nil { - in, out := &in.ResourceStatuses, &out.ResourceStatuses - *out = make([]ResourceStatus, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManifestStatus. -func (in *ManifestStatus) DeepCopy() *ManifestStatus { - if in == nil { - return nil - } - out := new(ManifestStatus) - 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 - 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 *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 *Placement) DeepCopyInto(out *Placement) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Placement. -func (in *Placement) DeepCopy() *Placement { - if in == nil { - return nil - } - out := new(Placement) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Placement) 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 *PlacementList) DeepCopyInto(out *PlacementList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Placement, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlacementList. -func (in *PlacementList) DeepCopy() *PlacementList { - if in == nil { - return nil - } - out := new(PlacementList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *PlacementList) 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 *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 *PlacementSpec) DeepCopyInto(out *PlacementSpec) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlacementSpec. -func (in *PlacementSpec) DeepCopy() *PlacementSpec { - if in == nil { - return nil - } - out := new(PlacementSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PlacementStatus) DeepCopyInto(out *PlacementStatus) { - *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 PlacementStatus. -func (in *PlacementStatus) DeepCopy() *PlacementStatus { - if in == nil { - return nil - } - out := new(PlacementStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ResourceStatus) DeepCopyInto(out *ResourceStatus) { - *out = *in - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceStatus. -func (in *ResourceStatus) DeepCopy() *ResourceStatus { - if in == nil { - return nil - } - out := new(ResourceStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ResourceTemplate) DeepCopyInto(out *ResourceTemplate) { - *out = *in - in.Content.DeepCopyInto(&out.Content) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceTemplate. -func (in *ResourceTemplate) DeepCopy() *ResourceTemplate { - if in == nil { - return nil - } - out := new(ResourceTemplate) - in.DeepCopyInto(out) - return out -} diff --git a/hyperfleet-operator/cmd/manager/main.go b/hyperfleet-operator/cmd/manager/main.go index 2acb2c8e..6e6215f1 100644 --- a/hyperfleet-operator/cmd/manager/main.go +++ b/hyperfleet-operator/cmd/manager/main.go @@ -38,7 +38,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" - v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/controller" "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/dynamo" "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/dynamo/statusstream" diff --git a/hyperfleet-operator/config/crd/bases/hyperfleet.io_clusters.yaml b/hyperfleet-operator/config/crd/bases/hyperfleet.io_clusters.yaml index 20750447..caf029db 100644 --- a/hyperfleet-operator/config/crd/bases/hyperfleet.io_clusters.yaml +++ b/hyperfleet-operator/config/crd/bases/hyperfleet.io_clusters.yaml @@ -66,31 +66,34 @@ 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 that owns this cluster. + type: string creatorARN: - description: |- - CreatorARN is the IAM ARN of the user who created this cluster. - Used to bootstrap the initial cluster-admin RBAC mapping. + description: CreatorARN is the IAM ARN of the user who created this + cluster. pattern: '^arn:aws:' type: string + 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 is the time after which the cluster will be - automatically deleted. If nil, the cluster has no expiration. + description: ExpirationTimestamp marks when this cluster should be + automatically deleted. format: date-time type: string hostedCluster: description: |- - HostedCluster is the full HyperShift HostedClusterSpec. The customer provides - the fields they care about; the operator overrides platform-managed fields - (InfraID, DNS, PullSecret, Services, etc.) at render time. + HostedCluster contains the upstream HyperShift fields, mirrored as + passthrough types with per-field visibility and write-mode markers. properties: additionalTrustBundle: - description: |- - additionalTrustBundle is a local reference to a ConfigMap that must have a "ca-bundle.crt" key - whose content must be a PEM-encoded X.509 certificate bundle that will be added to the hosted controlplane and nodes - If the reference is set but none of the above requirements are met, the HostedCluster will enter a degraded state. - This will be part of every payload generated by the controllers for any NodePool of the HostedCluster. - Changing this value will trigger a rollout for all existing NodePools in the cluster. + description: additionalTrustBundle is a local reference to a ConfigMap + that must have a "ca-bundle.crt" key properties: name: default: "" @@ -104,13 +107,8 @@ spec: type: object x-kubernetes-map-type: atomic auditWebhook: - description: |- - auditWebhook contains metadata for configuring an audit webhook endpoint - for a cluster to process cluster audit events. It references a secret that - contains the webhook information for the audit webhook endpoint. It is a - secret because if the endpoint has mTLS the kubeconfig will contain client - keys. The kubeconfig needs to be stored in the secret with a secret key - name that corresponds to the constant AuditWebhookKubeconfigKey. + description: auditWebhook contains metadata for configuring an + audit webhook endpoint properties: name: default: "" @@ -124,9 +122,8 @@ spec: type: object x-kubernetes-map-type: atomic autoNode: - description: |- - autoNode specifies the configuration for automatic node provisioning and lifecycle management. - When set, the provisioner(e.g. Karpenter) will be used to provision nodes for targeted workloads. + description: autoNode specifies the configuration for automatic + node provisioning and lifecycle management. properties: provisionerConfig: description: provisionerConfig specifies the provisioner used @@ -262,9 +259,8 @@ spec: - provisionerConfig type: object autoscaling: - description: |- - autoscaling specifies auto-scaling behavior that applies to all NodePools - associated with this HostedCluster. + description: autoscaling specifies auto-scaling behavior that + applies to all NodePools properties: balancingIgnoredLabels: description: |- @@ -382,2846 +378,317 @@ spec: When set to 0, this means scale down evaluation will resume immediately after node deletion, without any delay. When omitted, the autoscaler defaults to 0s. format: int32 - maximum: 86400 - minimum: 0 - type: integer - delayAfterFailureSeconds: - description: |- - delayAfterFailureSeconds sets how long after a scale down failure, scale down evaluation resumes. - It must be between 0 and 86400 (24 hours). - When set to 0, this means scale down evaluation will resume immediately after a scale down failure, without any delay. - When omitted, the autoscaler defaults to 180s (3 minutes). - format: int32 - maximum: 86400 - minimum: 0 - type: integer - unneededDurationSeconds: - description: |- - unneededDurationSeconds establishes how long a node should be unneeded before it is eligible for scale down in seconds. - It must be between 0 and 86400 (24 hours). - When omitted, the autoscaler defaults to 600s (10 minutes). - format: int32 - maximum: 86400 - minimum: 0 - type: integer - utilizationThresholdPercent: - description: |- - utilizationThresholdPercent determines the node utilization level, defined as sum of requested resources divided by capacity, below which a node can be considered for scale down. - The value represents a percentage from 0 to 100. - When set to 0, this means nodes will only be considered for scale down if they are completely idle (0% utilization). - When set to 100, this means nodes will be considered for scale down regardless of their utilization level. - A value between 0 and 100 represents the utilization threshold below which a node can be considered for scale down. - When omitted, the autoscaler defaults to 50%. - format: int32 - maximum: 100 - minimum: 0 - type: integer - type: object - scaling: - default: ScaleUpAndScaleDown - description: |- - scaling defines the scaling behavior for the cluster autoscaler. - ScaleUpOnly means the autoscaler will only scale up nodes, never scale down. - ScaleUpAndScaleDown means the autoscaler will both scale up and scale down nodes. - When set to ScaleUpAndScaleDown, the scaleDown field can be used to configure scale down behavior. - - Note: This field is only supported in OpenShift versions 4.19 and above. - enum: - - ScaleUpOnly - - ScaleUpAndScaleDown - type: string - type: object - x-kubernetes-validations: - - message: scaleDown can only be set when scaling is ScaleUpAndScaleDown - rule: 'self.scaling == ''ScaleUpAndScaleDown'' ? true : !has(self.scaleDown)' - capabilities: - default: {} - description: |- - capabilities allows for disabling optional components at cluster install time. - This field is optional and once set cannot be changed. - properties: - disabled: - description: |- - disabled when specified, explicitly disables the specified capabilitíes on the hosted cluster. - Once set, this field cannot be changed. - - Note: Disabling 'openshift-samples','Insights', 'Console', 'NodeTuning', 'Ingress' are only supported in OpenShift versions 4.20 and above. - items: - enum: - - ImageRegistry - - openshift-samples - - Insights - - baremetal - - Console - - NodeTuning - - Ingress - type: string - maxItems: 25 - type: array - x-kubernetes-list-type: atomic - x-kubernetes-validations: - - message: Disabled is immutable. Changes might result in - unpredictable and disruptive behavior. - rule: self == oldSelf - - message: Ingress capability can only be disabled if Console - capability is also disabled - rule: '!self.exists(cap, cap == ''Ingress'') || self.exists(cap, - cap == ''Console'')' - enabled: - description: |- - enabled when specified, explicitly enables the specified capabilitíes on the hosted cluster. - Once set, this field cannot be changed. - items: - enum: - - ImageRegistry - - openshift-samples - - Insights - - baremetal - - Console - - NodeTuning - - Ingress - type: string - maxItems: 25 - type: array - x-kubernetes-list-type: atomic - x-kubernetes-validations: - - message: Enabled is immutable. Changes might result in unpredictable - and disruptive behavior. - rule: self == oldSelf - type: object - x-kubernetes-validations: - - message: Capabilities is immutable. Changes might result in - unpredictable and disruptive behavior. - rule: self == oldSelf - - message: Capabilities can not be both enabled and disabled at - once. - rule: 'has(self.enabled) && has(self.disabled) ? self.enabled.all(e, - !(e in self.disabled)) : true' - channel: - description: |- - channel is an identifier for explicitly requesting that a non-default set of updates be applied to this cluster. - If omitted no particular upgrades are suggested. - maxLength: 100 - minLength: 1 - type: string - clusterID: - description: |- - clusterID uniquely identifies this cluster. This is expected to be an RFC4122 UUID value (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx in hexadecimal digits). - As with a Kubernetes metadata.uid, this ID uniquely identifies this cluster in space and time. - This value identifies the cluster in metrics pushed to telemetry and metrics produced by the control plane operators. - If a value is not specified, a random clusterID will be generated and set by the controller. - Once set, this value is immutable. - maxLength: 36 - minLength: 36 - type: string - x-kubernetes-validations: - - message: clusterID must be an RFC4122 UUID value (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - in hexadecimal digits) - rule: self.matches('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}') - - message: clusterID is immutable - rule: oldSelf == "" || self == oldSelf - configuration: - description: |- - configuration specifies configuration for individual OCP components in the - cluster, represented as embedded resources that correspond to the openshift - configuration API. - properties: - apiServer: - description: |- - apiServer holds configuration (like serving certificates, client CA and CORS domains) - shared by all API servers in the system, among them especially kube-apiserver - and openshift-apiserver. - properties: - additionalCORSAllowedOrigins: - description: |- - additionalCORSAllowedOrigins lists additional, user-defined regular expressions describing hosts for which the - API server allows access using the CORS headers. This may be needed to access the API and the integrated OAuth - server from JavaScript applications. - The values are regular expressions that correspond to the Golang regular expression language. - items: - type: string - type: array - x-kubernetes-list-type: atomic - audit: - default: - profile: Default - description: |- - audit specifies the settings for audit configuration to be applied to all OpenShift-provided - API servers in the cluster. - properties: - customRules: - description: |- - customRules specify profiles per group. These profile take precedence over the - top-level profile field if they apply. They are evaluation from top to bottom and - the first one that matches, applies. - items: - description: |- - AuditCustomRule describes a custom rule for an audit profile that takes precedence over - the top-level profile. - properties: - group: - description: group is a name of group a request - user must be member of in order to this profile - to apply. - minLength: 1 - type: string - profile: - description: |- - profile specifies the name of the desired audit policy configuration to be deployed to - all OpenShift-provided API servers in the cluster. - - The following profiles are provided: - - Default: the existing default policy. - - WriteRequestBodies: like 'Default', but logs request and response HTTP payloads for - write requests (create, update, patch). - - AllRequestBodies: like 'WriteRequestBodies', but also logs request and response - HTTP payloads for read requests (get, list). - - None: no requests are logged at all, not even oauthaccesstokens and oauthauthorizetokens. - - If unset, the 'Default' profile is used as the default. - enum: - - Default - - WriteRequestBodies - - AllRequestBodies - - None - type: string - required: - - group - - profile - type: object - type: array - x-kubernetes-list-map-keys: - - group - x-kubernetes-list-type: map - profile: - default: Default - description: |- - profile specifies the name of the desired top-level audit profile to be applied to all requests - sent to any of the OpenShift-provided API servers in the cluster (kube-apiserver, - openshift-apiserver and oauth-apiserver), with the exception of those requests that match - one or more of the customRules. - - The following profiles are provided: - - Default: default policy which means MetaData level logging with the exception of events - (not logged at all), oauthaccesstokens and oauthauthorizetokens (both logged at RequestBody - level). - - WriteRequestBodies: like 'Default', but logs request and response HTTP payloads for - write requests (create, update, patch). - - AllRequestBodies: like 'WriteRequestBodies', but also logs request and response - HTTP payloads for read requests (get, list). - - None: no requests are logged at all, not even oauthaccesstokens and oauthauthorizetokens. - - Warning: It is not recommended to disable audit logging by using the `None` profile unless you - are fully aware of the risks of not logging data that can be beneficial when troubleshooting issues. - If you disable audit logging and a support situation arises, you might need to enable audit logging - and reproduce the issue in order to troubleshoot properly. - - If unset, the 'Default' profile is used as the default. - enum: - - Default - - WriteRequestBodies - - AllRequestBodies - - None - type: string - type: object - clientCA: - description: |- - clientCA references a ConfigMap containing a certificate bundle for the signers that will be recognized for - incoming client certificates in addition to the operator managed signers. If this is empty, then only operator managed signers are valid. - You usually only have to set this if you have your own PKI you wish to honor client certificates from. - The ConfigMap must exist in the openshift-config namespace and contain the following required fields: - - ConfigMap.Data["ca-bundle.crt"] - CA bundle. - properties: - name: - description: name is the metadata.name of the referenced - config map - type: string - required: - - name - type: object - encryption: - description: encryption allows the configuration of encryption - of resources at the datastore layer. - properties: - kms: - description: |- - kms defines the configuration for the external KMS instance that manages the encryption keys, - when KMS encryption is enabled sensitive resources will be encrypted using keys managed by an - externally configured KMS instance. - - The Key Management Service (KMS) instance provides symmetric encryption and is responsible for - managing the lifecyle of the encryption keys outside of the control plane. - This allows integration with an external provider to manage the data encryption keys securely. - properties: - aws: - description: |- - aws defines the key config for using an AWS KMS instance - for the encryption. The AWS KMS instance is managed - by the user outside the purview of the control plane. - properties: - keyARN: - description: |- - keyARN specifies the Amazon Resource Name (ARN) of the AWS KMS key used for encryption. - The value must adhere to the format `arn:aws:kms:::key/`, where: - - `` is the AWS region consisting of lowercase letters and hyphens followed by a number. - - `` is a 12-digit numeric identifier for the AWS account. - - `` is a unique identifier for the KMS key, consisting of lowercase hexadecimal characters and hyphens. - maxLength: 128 - minLength: 1 - type: string - x-kubernetes-validations: - - message: keyARN must follow the format `arn:aws:kms:::key/`. - The account ID must be a 12 digit number - and the region and key ID should consist - only of lowercase hexadecimal characters - and hyphens (-). - rule: self.matches('^arn:aws:kms:[a-z0-9-]+:[0-9]{12}:key/[a-f0-9-]+$') - region: - description: |- - region specifies the AWS region where the KMS instance exists, and follows the format - `--`, e.g.: `us-east-1`. - Only lowercase letters and hyphens followed by numbers are allowed. - maxLength: 64 - minLength: 1 - type: string - x-kubernetes-validations: - - message: region must be a valid AWS region, - consisting of lowercase characters, digits - and hyphens (-) only. - rule: self.matches('^[a-z0-9]+(-[a-z0-9]+)*$') - required: - - keyARN - - region - type: object - type: - description: |- - type defines the kind of platform for the KMS provider. - Available provider types are AWS only. - enum: - - AWS - type: string - required: - - type - type: object - x-kubernetes-validations: - - message: aws config is required when kms provider - type is AWS, and forbidden otherwise - rule: 'has(self.type) && self.type == ''AWS'' ? has(self.aws) - : !has(self.aws)' - type: - description: |- - type defines what encryption type should be used to encrypt resources at the datastore layer. - When this field is unset (i.e. when it is set to the empty string), identity is implied. - The behavior of unset can and will change over time. Even if encryption is enabled by default, - the meaning of unset may change to a different encryption type based on changes in best practices. - - When encryption is enabled, all sensitive resources shipped with the platform are encrypted. - This list of sensitive resources can and will change over time. The current authoritative list is: - - 1. secrets - 2. configmaps - 3. routes.route.openshift.io - 4. oauthaccesstokens.oauth.openshift.io - 5. oauthauthorizetokens.oauth.openshift.io - type: string - type: object - servingCerts: - description: |- - servingCert is the TLS cert info for serving secure traffic. If not specified, operator managed certificates - will be used for serving secure traffic. - properties: - namedCertificates: - description: |- - namedCertificates references secrets containing the TLS cert info for serving secure traffic to specific hostnames. - If no named certificates are provided, or no named certificates match the server name as understood by a client, - the defaultServingCertificate will be used. - items: - description: APIServerNamedServingCert maps a server - DNS name, as understood by a client, to a certificate. - properties: - names: - description: |- - names is a optional list of explicit DNS names (leading wildcards allowed) that should use this certificate to - serve secure traffic. If no names are provided, the implicit names will be extracted from the certificates. - Exact names trump over wildcard names. Explicit names defined here trump over extracted implicit names. - items: - type: string - maxItems: 64 - type: array - x-kubernetes-list-type: atomic - servingCertificate: - description: |- - servingCertificate references a kubernetes.io/tls type secret containing the TLS cert info for serving secure traffic. - The secret must exist in the openshift-config namespace and contain the following required fields: - - Secret.Data["tls.key"] - TLS private key. - - Secret.Data["tls.crt"] - TLS certificate. - properties: - name: - description: name is the metadata.name of - the referenced secret - type: string - required: - - name - type: object - type: object - maxItems: 32 - type: array - x-kubernetes-list-type: atomic - type: object - tlsAdherence: - description: |- - tlsAdherence controls if components in the cluster adhere to the TLS security profile - configured on this APIServer resource. - - Valid values are "LegacyAdheringComponentsOnly" and "StrictAllComponents". - - When set to "LegacyAdheringComponentsOnly", components that already honor the - cluster-wide TLS profile continue to do so. Components that do not already honor - it continue to use their individual TLS configurations. - - When set to "StrictAllComponents", all components must honor the configured TLS - profile unless they have a component-specific TLS configuration that overrides - it. This mode is recommended for security-conscious deployments and is required - for certain compliance frameworks. - - Note: Some components such as Kubelet and IngressController have their own - dedicated TLS configuration mechanisms via KubeletConfig and IngressController - CRs respectively. When these component-specific TLS configurations are set, - they take precedence over the cluster-wide tlsSecurityProfile. When not set, - these components fall back to the cluster-wide default. - - Components that encounter an unknown value for tlsAdherence should treat it - as "StrictAllComponents" and log a warning to ensure forward compatibility - while defaulting to the more secure behavior. - - This field is optional. - When omitted, this means the user has no opinion and the platform is left - to choose reasonable defaults. These defaults are subject to change over time. - The current default is LegacyAdheringComponentsOnly. - - Once set, this field may be changed to a different value, but may not be removed. - enum: - - LegacyAdheringComponentsOnly - - StrictAllComponents - type: string - tlsSecurityProfile: - description: |- - tlsSecurityProfile specifies settings for TLS connections for externally exposed servers. - - When omitted, this means no opinion and the platform is left to choose a reasonable default, which is subject to change over time. - The current default is the Intermediate profile. - properties: - custom: - description: |- - custom is a user-defined TLS security profile. Be extremely careful using a custom - profile as invalid configurations can be catastrophic. An example custom profile - looks like this: - - minTLSVersion: VersionTLS11 - ciphers: - - ECDHE-ECDSA-CHACHA20-POLY1305 - - ECDHE-RSA-CHACHA20-POLY1305 - - ECDHE-RSA-AES128-GCM-SHA256 - - ECDHE-ECDSA-AES128-GCM-SHA256 - nullable: true - properties: - ciphers: - description: |- - ciphers is used to specify the cipher algorithms that are negotiated - during the TLS handshake. Operators may remove entries that their operands - do not support. For example, to use only ECDHE-RSA-AES128-GCM-SHA256 (yaml): - - ciphers: - - ECDHE-RSA-AES128-GCM-SHA256 - - TLS 1.3 cipher suites (e.g. TLS_AES_128_GCM_SHA256) are not configurable - and are always enabled when TLS 1.3 is negotiated. - items: - type: string - type: array - x-kubernetes-list-type: atomic - minTLSVersion: - description: |- - minTLSVersion is used to specify the minimal version of the TLS protocol - that is negotiated during the TLS handshake. For example, to use TLS - versions 1.1, 1.2 and 1.3 (yaml): - - minTLSVersion: VersionTLS11 - enum: - - VersionTLS10 - - VersionTLS11 - - VersionTLS12 - - VersionTLS13 - type: string - type: object - intermediate: - description: |- - intermediate is a TLS profile for use when you do not need compatibility with - legacy clients and want to remain highly secure while being compatible with - most clients currently in use. - - This profile is equivalent to a Custom profile specified as: - minTLSVersion: VersionTLS12 - ciphers: - - TLS_AES_128_GCM_SHA256 - - TLS_AES_256_GCM_SHA384 - - TLS_CHACHA20_POLY1305_SHA256 - - ECDHE-ECDSA-AES128-GCM-SHA256 - - ECDHE-RSA-AES128-GCM-SHA256 - - ECDHE-ECDSA-AES256-GCM-SHA384 - - ECDHE-RSA-AES256-GCM-SHA384 - - ECDHE-ECDSA-CHACHA20-POLY1305 - - ECDHE-RSA-CHACHA20-POLY1305 - nullable: true - type: object - modern: - description: |- - modern is a TLS security profile for use with clients that support TLS 1.3 and - do not need backward compatibility for older clients. - - This profile is equivalent to a Custom profile specified as: - minTLSVersion: VersionTLS13 - ciphers: - - TLS_AES_128_GCM_SHA256 - - TLS_AES_256_GCM_SHA384 - - TLS_CHACHA20_POLY1305_SHA256 - nullable: true - type: object - old: - description: |- - old is a TLS profile for use when services need to be accessed by very old - clients or libraries and should be used only as a last resort. - - This profile is equivalent to a Custom profile specified as: - minTLSVersion: VersionTLS10 - ciphers: - - TLS_AES_128_GCM_SHA256 - - TLS_AES_256_GCM_SHA384 - - TLS_CHACHA20_POLY1305_SHA256 - - ECDHE-ECDSA-AES128-GCM-SHA256 - - ECDHE-RSA-AES128-GCM-SHA256 - - ECDHE-ECDSA-AES256-GCM-SHA384 - - ECDHE-RSA-AES256-GCM-SHA384 - - ECDHE-ECDSA-CHACHA20-POLY1305 - - ECDHE-RSA-CHACHA20-POLY1305 - - ECDHE-ECDSA-AES128-SHA256 - - ECDHE-RSA-AES128-SHA256 - - ECDHE-ECDSA-AES128-SHA - - ECDHE-RSA-AES128-SHA - - ECDHE-ECDSA-AES256-SHA - - ECDHE-RSA-AES256-SHA - - AES128-GCM-SHA256 - - AES256-GCM-SHA384 - - AES128-SHA256 - - AES128-SHA - - AES256-SHA - - DES-CBC3-SHA - nullable: true - type: object - type: - description: |- - type is one of Old, Intermediate, Modern or Custom. Custom provides the - ability to specify individual TLS security profile parameters. - - The profiles are based on version 5.7 of the Mozilla Server Side TLS - configuration guidelines. The cipher lists consist of the configuration's - "ciphersuites" followed by the Go-specific "ciphers" from the guidelines. - See: https://ssl-config.mozilla.org/guidelines/5.7.json - - The profiles are intent based, so they may change over time as new ciphers are - developed and existing ciphers are found to be insecure. Depending on - precisely which ciphers are available to a process, the list may be reduced. - enum: - - Old - - Intermediate - - Modern - - Custom - type: string - type: object - type: object - authentication: - description: |- - authentication specifies cluster-wide settings for authentication (like OAuth and - webhook token authenticators). - properties: - oauthMetadata: - description: |- - oauthMetadata contains the discovery endpoint data for OAuth 2.0 - Authorization Server Metadata for an external OAuth server. - This discovery document can be viewed from its served location: - oc get --raw '/.well-known/oauth-authorization-server' - For further details, see the IETF Draft: - https://tools.ietf.org/html/draft-ietf-oauth-discovery-04#section-2 - If oauthMetadata.name is non-empty, this value has precedence - over any metadata reference stored in status. - The key "oauthMetadata" is used to locate the data. - If specified and the config map or expected key is not found, no metadata is served. - If the specified metadata is not valid, no metadata is served. - The namespace for this config map is openshift-config. - properties: - name: - description: name is the metadata.name of the referenced - config map - type: string - required: - - name - type: object - oidcProviders: - description: |- - oidcProviders are OIDC identity providers that can issue tokens for this cluster - Can only be set if "Type" is set to "OIDC". - - At most one provider can be configured. - items: - properties: - claimMappings: - description: claimMappings is a required field that - configures the rules to be used by the Kubernetes - API server for translating claims in a JWT token, - issued by the identity provider, to a cluster - identity. - properties: - extra: - description: |- - extra is an optional field for configuring the mappings used to construct the extra attribute for the cluster identity. - When omitted, no extra attributes will be present on the cluster identity. - - key values for extra mappings must be unique. - A maximum of 32 extra attribute mappings may be provided. - items: - description: |- - ExtraMapping allows specifying a key and CEL expression to evaluate the keys' value. - It is used to create additional mappings and attributes added to a cluster identity from a provided authentication token. - properties: - key: - description: |- - key is a required field that specifies the string to use as the extra attribute key. - - key must be a domain-prefix path (e.g 'example.org/foo'). - key must not exceed 510 characters in length. - key must contain the '/' character, separating the domain and path characters. - key must not be empty. - - The domain portion of the key (string of characters prior to the '/') must be a valid RFC1123 subdomain. - It must not exceed 253 characters in length. - It must start and end with an alphanumeric character. - It must only contain lower case alphanumeric characters and '-' or '.'. - It must not use the reserved domains, or be subdomains of, "kubernetes.io", "k8s.io", and "openshift.io". - - The path portion of the key (string of characters after the '/') must not be empty and must consist of at least one alphanumeric character, percent-encoded octets, '-', '.', '_', '~', '!', '$', '&', ''', '(', ')', '*', '+', ',', ';', '=', and ':'. - It must not exceed 256 characters in length. - maxLength: 510 - minLength: 1 - type: string - x-kubernetes-validations: - - message: key must contain the '/' character - rule: self.contains('/') - - message: the domain of the key must - consist of only lower case alphanumeric - characters, '-' or '.', and must start - and end with an alphanumeric character - rule: self.split('/', 2)[0].matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") - - message: the domain of the key must - not exceed 253 characters in length - rule: self.split('/', 2)[0].size() <= - 253 - - message: the domain 'kubernetes.io' - is reserved for Kubernetes use - rule: self.split('/', 2)[0] != 'kubernetes.io' - - message: the subdomains '*.kubernetes.io' - are reserved for Kubernetes use - rule: '!self.split(''/'', 2)[0].endsWith(''.kubernetes.io'')' - - message: the domain 'k8s.io' is reserved - for Kubernetes use - rule: self.split('/', 2)[0] != 'k8s.io' - - message: the subdomains '*.k8s.io' are - reserved for Kubernetes use - rule: '!self.split(''/'', 2)[0].endsWith(''.k8s.io'')' - - message: the domain 'openshift.io' is - reserved for OpenShift use - rule: self.split('/', 2)[0] != 'openshift.io' - - message: the subdomains '*.openshift.io' - are reserved for OpenShift use - rule: '!self.split(''/'', 2)[0].endsWith(''.openshift.io'')' - - message: the path of the key must not - be empty and must consist of at least - one alphanumeric character, percent-encoded - octets, apostrophe, '-', '.', '_', - '~', '!', '$', '&', '(', ')', '*', - '+', ',', ';', '=', and ':' - rule: self.split('/', 2)[1].matches('[A-Za-z0-9/\\-._~%!$&\'()*+;=:]+') - - message: the path of the key must not - exceed 256 characters in length - rule: self.split('/', 2)[1].size() <= - 256 - valueExpression: - description: |- - valueExpression is a required field to specify the CEL expression to extract the extra attribute value from a JWT token's claims. - valueExpression must produce a string or string array value. - "", [], and null are treated as the extra mapping not being present. - Empty string values within an array are filtered out. - - CEL expressions have access to the token claims through a CEL variable, 'claims'. - 'claims' is a map of claim names to claim values. - For example, the 'sub' claim value can be accessed as 'claims.sub'. - Nested claims can be accessed using dot notation ('claims.foo.bar'). - - valueExpression must not exceed 1024 characters in length. - valueExpression must not be empty. - maxLength: 1024 - minLength: 1 - type: string - required: - - key - - valueExpression - type: object - maxItems: 32 - type: array - x-kubernetes-list-map-keys: - - key - x-kubernetes-list-type: map - groups: - description: |- - groups is an optional field that configures how the groups of a cluster identity should be constructed from the claims in a JWT token issued by the identity provider. - - When referencing a claim, if the claim is present in the JWT token, its value must be a list of groups separated by a comma (','). - - For example - '"example"' and '"exampleOne", "exampleTwo", "exampleThree"' are valid claim values. - properties: - claim: - description: |- - claim is an optional field for specifying the JWT token claim that is used in the mapping. - The value of this claim will be assigned to the field in which this mapping is associated. - claim must not exceed 256 characters in length. - When set to the empty string `""`, this means that no named claim should be used for the group mapping. - claim is required when the ExternalOIDCWithUpstreamParity feature gate is not enabled. - maxLength: 256 - type: string - expression: - description: |- - expression is an optional CEL expression used to derive - group values from JWT claims. - - CEL expressions have access to the token claims through a CEL variable, 'claims'. - - expression must be at least 1 character and must not exceed 1024 characters in length . - - When specified, claim must not be set or be explicitly set to the empty string (`""`). - maxLength: 1024 - minLength: 1 - type: string - prefix: - description: |- - prefix is an optional field that configures the prefix that will be applied to the cluster identity attribute during the process of mapping JWT claims to cluster identity attributes. - - When omitted or set to an empty string (""), no prefix is applied to the cluster identity attribute. - Must not be set to a non-empty value when expression is set. - - Example: if `prefix` is set to "myoidc:" and the `claim` in JWT contains an array of strings "a", "b" and "c", the mapping will result in an array of string "myoidc:a", "myoidc:b" and "myoidc:c". - type: string - type: object - uid: - description: |- - uid is an optional field for configuring the claim mapping used to construct the uid for the cluster identity. - - When using uid.claim to specify the claim it must be a single string value. - When using uid.expression the expression must result in a single string value. - - When omitted, this means the user has no opinion and the platform is left to choose a default, which is subject to change over time. - - The current default is to use the 'sub' claim. - properties: - claim: - description: |- - claim is an optional field for specifying the JWT token claim that is used in the mapping. - The value of this claim will be assigned to the field in which this mapping is associated. - - Precisely one of claim or expression must be set. - claim must not be specified when expression is set. - When specified, claim must be at least 1 character in length and must not exceed 256 characters in length. - maxLength: 256 - minLength: 1 - type: string - expression: - description: |- - expression is an optional field for specifying a CEL expression that produces a string value from JWT token claims. - - CEL expressions have access to the token claims through a CEL variable, 'claims'. - 'claims' is a map of claim names to claim values. - For example, the 'sub' claim value can be accessed as 'claims.sub'. - Nested claims can be accessed using dot notation ('claims.foo.bar'). - - Precisely one of claim or expression must be set. - expression must not be specified when claim is set. - When specified, expression must be at least 1 character in length and must not exceed 1024 characters in length. - maxLength: 1024 - minLength: 1 - type: string - type: object - x-kubernetes-validations: - - message: precisely one of claim or expression - must be set - rule: 'has(self.claim) ? !has(self.expression) - : has(self.expression)' - username: - description: username is a required field that - configures how the username of a cluster identity - should be constructed from the claims in a - JWT token issued by the identity provider. - properties: - claim: - description: |- - claim is an optional field that configures the JWT token claim whose value is assigned to the cluster identity field associated with this mapping. - claim is required when the ExternalOIDCWithUpstreamParity feature gate is not enabled. - When the ExternalOIDCWithUpstreamParity feature gate is enabled, claim must not be set when expression is set. - - claim must not be an empty string ("") and must not exceed 256 characters. - maxLength: 256 - minLength: 1 - type: string - expression: - description: |- - expression is an optional CEL expression used to derive - the username from JWT claims. - - CEL expressions have access to the token claims - through a CEL variable, 'claims'. - - expression must be at least 1 character and must not exceed 1024 characters in length. - expression must not be set when claim is set. - maxLength: 1024 - minLength: 1 - type: string - prefix: - description: |- - prefix configures the prefix that should be prepended to the value of the JWT claim. - - prefix must be set when prefixPolicy is set to 'Prefix' and must be unset otherwise. - properties: - prefixString: - description: |- - prefixString is a required field that configures the prefix that will be applied to cluster identity username attribute during the process of mapping JWT claims to cluster identity attributes. - - prefixString must not be an empty string (""). - minLength: 1 - type: string - required: - - prefixString - type: object - prefixPolicy: - description: |- - prefixPolicy is an optional field that configures how a prefix should be applied to the value of the JWT claim specified in the 'claim' field. - - Allowed values are 'Prefix', 'NoPrefix', and omitted (not provided or an empty string). - - When set to 'Prefix', the value specified in the prefix field will be prepended to the value of the JWT claim. - The prefix field must be set when prefixPolicy is 'Prefix'. - Must not be set to 'Prefix' when expression is set. - When set to 'NoPrefix', no prefix will be prepended to the value of the JWT claim. - When omitted, this means no opinion and the platform is left to choose any prefixes that are applied which is subject to change over time. - Currently, the platform prepends `{issuerURL}#` to the value of the JWT claim when the claim is not 'email'. - - As an example, consider the following scenario: - - `prefix` is unset, `issuerURL` is set to `https://myoidc.tld`, - the JWT claims include "username":"userA" and "email":"userA@myoidc.tld", - and `claim` is set to: - - "username": the mapped value will be "https://myoidc.tld#userA" - - "email": the mapped value will be "userA@myoidc.tld" - enum: - - "" - - NoPrefix - - Prefix - type: string - type: object - x-kubernetes-validations: - - message: prefix must be set if prefixPolicy - is 'Prefix', but must remain unset otherwise - rule: 'has(self.prefixPolicy) && self.prefixPolicy - == ''Prefix'' ? (has(self.prefix) && size(self.prefix.prefixString) - > 0) : !has(self.prefix)' - required: - - username - type: object - claimValidationRules: - description: |- - claimValidationRules is an optional field that configures the rules to be used by the Kubernetes API server for validating the claims in a JWT token issued by the identity provider. - - Validation rules are joined via an AND operation. - items: - description: |- - TokenClaimValidationRule represents a validation rule based on token claims. - If type is RequiredClaim, requiredClaim must be set. - If Type is CEL, CEL must be set and RequiredClaim must be omitted. - properties: - cel: - description: |- - cel holds the CEL expression and message for validation. - Must be set when Type is "CEL", and forbidden otherwise. - properties: - expression: - description: |- - expression is a CEL expression evaluated against token claims. - expression is required, must be at least 1 character in length and must not exceed 1024 characters. - The expression must return a boolean value where 'true' signals a valid token and 'false' an invalid one. - maxLength: 1024 - minLength: 1 - type: string - message: - description: |- - message is a required human-readable message to be logged by the Kubernetes API server if the CEL expression defined in 'expression' fails. - message must be at least 1 character in length and must not exceed 256 characters. - maxLength: 256 - minLength: 1 - type: string - required: - - expression - - message - type: object - requiredClaim: - description: |- - requiredClaim allows configuring a required claim name and its expected value. - This field is required when `type` is set to RequiredClaim, and must be omitted when `type` is set to any other value. - The Kubernetes API server uses this field to validate if an incoming JWT is valid for this identity provider. - properties: - claim: - description: |- - claim is a required field that configures the name of the required claim. - When taken from the JWT claims, claim must be a string value. - - claim must not be an empty string (""). - minLength: 1 - type: string - requiredValue: - description: |- - requiredValue is a required field that configures the value that 'claim' must have when taken from the incoming JWT claims. - If the value in the JWT claims does not match, the token will be rejected for authentication. - - requiredValue must not be an empty string (""). - minLength: 1 - type: string - required: - - claim - - requiredValue - type: object - type: - description: |- - type is an optional field that configures the type of the validation rule. - - Allowed values are "RequiredClaim" and "CEL". - - When set to 'RequiredClaim', the Kubernetes API server will be configured to validate that the incoming JWT contains the required claim and that its value matches the required value. - - When set to 'CEL', the Kubernetes API server will be configured to validate the incoming JWT against the configured CEL expression. - type: string - required: - - type - type: object - x-kubernetes-validations: - - message: requiredClaim must be set when type - is 'RequiredClaim', and forbidden otherwise - rule: 'has(self.type) && self.type == ''RequiredClaim'' - ? has(self.requiredClaim) : !has(self.requiredClaim)' - type: array - x-kubernetes-list-type: atomic - issuer: - description: issuer is a required field that configures - how the platform interacts with the identity provider - and how tokens issued from the identity provider - are evaluated by the Kubernetes API server. - properties: - audiences: - description: |- - audiences is a required field that configures the acceptable audiences the JWT token, issued by the identity provider, must be issued to. - At least one of the entries must match the 'aud' claim in the JWT token. - - audiences must contain at least one entry and must not exceed ten entries. - items: - minLength: 1 - type: string - maxItems: 10 - minItems: 1 - type: array - x-kubernetes-list-type: set - discoveryURL: - description: |- - discoveryURL is an optional field that, if specified, overrides the default discovery endpoint used to retrieve OIDC configuration metadata. - By default, the discovery URL is derived from `issuerURL` as "{issuerURL}/.well-known/openid-configuration". - - The discoveryURL must be a valid absolute HTTPS URL. - It must not contain query parameters, user information, or fragments. - Additionally, it must differ from the value of `issuerURL` (ignoring trailing slashes). - The discoveryURL value must be at least 1 character long and no longer than 2048 characters. - maxLength: 2048 - minLength: 1 - type: string - x-kubernetes-validations: - - message: discoveryURL must be a valid URL - rule: isURL(self) - - message: discoveryURL must be a valid https - URL - rule: url(self).getScheme() == 'https' - - message: discoveryURL must not contain query - parameters - rule: url(self).getQuery().size() == 0 - - message: discoveryURL must not contain fragments - rule: self.matches('^[^#]*$') - - message: discoveryURL must not contain user - info - rule: '!self.matches(''^https://.+:.+@.+/.*$'')' - issuerCertificateAuthority: - description: |- - issuerCertificateAuthority is an optional field that configures the certificate authority, used by the Kubernetes API server, to validate the connection to the identity provider when fetching discovery information. - - When not specified, the system trust is used. - - When specified, it must reference a ConfigMap in the openshift-config namespace containing the PEM-encoded CA certificates under the 'ca-bundle.crt' key in the data field of the ConfigMap. - properties: - name: - description: name is the metadata.name of - the referenced config map - type: string - required: - - name - type: object - issuerURL: - description: |- - issuerURL is a required field that configures the URL used to issue tokens by the identity provider. - The Kubernetes API server determines how authentication tokens should be handled by matching the 'iss' claim in the JWT to the issuerURL of configured identity providers. - - Must be at least 1 character and must not exceed 512 characters in length. - Must be a valid URL that uses the 'https' scheme and does not contain a query, fragment or user. - maxLength: 512 - minLength: 1 - type: string - x-kubernetes-validations: - - message: must be a valid URL - rule: isURL(self) - - message: must use the 'https' scheme - rule: isURL(self) && url(self).getScheme() - == 'https' - - message: must not have a query - rule: isURL(self) && url(self).getQuery() - == {} - - message: must not have a fragment - rule: self.find('#(.+)$') == '' - - message: must not have user info - rule: self.find('@') == '' - required: - - audiences - - issuerURL - type: object - name: - description: |- - name is a required field that configures the unique human-readable identifier associated with the identity provider. - It is used to distinguish between multiple identity providers and has no impact on token validation or authentication mechanics. - - name must not be an empty string (""). - minLength: 1 - type: string - oidcClients: - description: |- - oidcClients is an optional field that configures how on-cluster, platform clients should request tokens from the identity provider. - oidcClients must not exceed 20 entries and entries must have unique namespace/name pairs. - items: - description: OIDCClientConfig configures how platform - clients interact with identity providers as - an authentication method. - properties: - clientID: - description: |- - clientID is a required field that configures the client identifier, from the identity provider, that the platform component uses for authentication requests made to the identity provider. - The identity provider must accept this identifier for platform components to be able to use the identity provider as an authentication mode. - - clientID must not be an empty string (""). - minLength: 1 - type: string - clientSecret: - description: |- - clientSecret is an optional field that configures the client secret used by the platform component when making authentication requests to the identity provider. - - When not specified, no client secret will be used when making authentication requests to the identity provider. - - When specified, clientSecret references a Secret in the 'openshift-config' namespace that contains the client secret in the 'clientSecret' key of the '.data' field. - - The client secret will be used when making authentication requests to the identity provider. - - Public clients do not require a client secret but private clients do require a client secret to work with the identity provider. - properties: - name: - description: name is the metadata.name - of the referenced secret - type: string - required: - - name - type: object - componentName: - description: |- - componentName is a required field that specifies the name of the platform component being configured to use the identity provider as an authentication mode. - - It is used in combination with componentNamespace as a unique identifier. - - componentName must not be an empty string ("") and must not exceed 256 characters in length. - maxLength: 256 - minLength: 1 - type: string - componentNamespace: - description: |- - componentNamespace is a required field that specifies the namespace in which the platform component being configured to use the identity provider as an authentication mode is running. - - It is used in combination with componentName as a unique identifier. - - componentNamespace must not be an empty string ("") and must not exceed 63 characters in length. - maxLength: 63 - minLength: 1 - type: string - extraScopes: - description: |- - extraScopes is an optional field that configures the extra scopes that should be requested by the platform component when making authentication requests to the identity provider. - This is useful if you have configured claim mappings that requires specific scopes to be requested beyond the standard OIDC scopes. - - When omitted, no additional scopes are requested. - items: - type: string - type: array - x-kubernetes-list-type: set - required: - - clientID - - componentName - - componentNamespace - type: object - maxItems: 20 - type: array - x-kubernetes-list-map-keys: - - componentNamespace - - componentName - x-kubernetes-list-type: map - userValidationRules: - description: |- - userValidationRules is an optional field that configures the set of rules used to validate the cluster user identity that was constructed via mapping token claims to user identity attributes. - Rules are CEL expressions that must evaluate to 'true' for authentication to succeed. - If any rule in the chain of rules evaluates to 'false', authentication will fail. - When specified, at least one rule must be specified and no more than 64 rules may be specified. - items: - description: |- - TokenUserValidationRule provides a CEL-based rule used to validate a token subject. - Each rule contains a CEL expression that is evaluated against the token’s claims. - properties: - expression: - description: |- - expression is a required CEL expression that performs a validation on cluster user identity attributes like username, groups, etc. - - The expression must evaluate to a boolean value. - When the expression evaluates to 'true', the cluster user identity is considered valid. - When the expression evaluates to 'false', the cluster user identity is not considered valid. - expression must be at least 1 character in length and must not exceed 1024 characters. - maxLength: 1024 - minLength: 1 - type: string - message: - description: |- - message is a required human-readable message to be logged by the Kubernetes API server if the CEL expression defined in 'expression' fails. - message must be at least 1 character in length and must not exceed 256 characters. - maxLength: 256 - minLength: 1 - type: string - required: - - expression - - message - type: object - maxItems: 64 - minItems: 1 - type: array - x-kubernetes-list-map-keys: - - expression - x-kubernetes-list-type: map - required: - - claimMappings - - issuer - - name - type: object - maxItems: 1 - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - serviceAccountIssuer: - description: |- - serviceAccountIssuer is the identifier of the bound service account token - issuer. - The default is https://kubernetes.default.svc - WARNING: Updating this field will not result in immediate invalidation of all bound tokens with the - previous issuer value. Instead, the tokens issued by previous service account issuer will continue to - be trusted for a time period chosen by the platform (currently set to 24h). - This time period is subject to change over time. - This allows internal components to transition to use new service account issuer without service distruption. - type: string - type: - description: |- - type identifies the cluster managed, user facing authentication mode in use. - Specifically, it manages the component that responds to login attempts. - The default is IntegratedOAuth. - type: string - webhookTokenAuthenticator: - description: |- - webhookTokenAuthenticator configures a remote token reviewer. - These remote authentication webhooks can be used to verify bearer tokens - via the tokenreviews.authentication.k8s.io REST API. This is required to - honor bearer tokens that are provisioned by an external authentication service. - - Can only be set if "Type" is set to "None". - properties: - kubeConfig: - description: |- - kubeConfig references a secret that contains kube config file data which - describes how to access the remote webhook service. - The namespace for the referenced secret is openshift-config. - - For further details, see: - - https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication - - The key "kubeConfig" is used to locate the data. - If the secret or expected key is not found, the webhook is not honored. - If the specified kube config data is not valid, the webhook is not honored. - properties: - name: - description: name is the metadata.name of the - referenced secret - type: string - required: - - name - type: object - required: - - kubeConfig - type: object - webhookTokenAuthenticators: - description: webhookTokenAuthenticators is DEPRECATED, - setting it has no effect. - items: - description: |- - deprecatedWebhookTokenAuthenticator holds the necessary configuration options for a remote token authenticator. - It's the same as WebhookTokenAuthenticator but it's missing the 'required' validation on KubeConfig field. - properties: - kubeConfig: - description: |- - kubeConfig contains kube config file data which describes how to access the remote webhook service. - For further details, see: - https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication - The key "kubeConfig" is used to locate the data. - If the secret or expected key is not found, the webhook is not honored. - If the specified kube config data is not valid, the webhook is not honored. - The namespace for this secret is determined by the point of use. - properties: - name: - description: name is the metadata.name of the - referenced secret - type: string - required: - - name - type: object - type: object - type: array - x-kubernetes-list-type: atomic - type: object - featureGate: - description: featureGate holds cluster-wide information about - feature gates. - properties: - customNoUpgrade: - description: |- - customNoUpgrade allows the enabling or disabling of any feature. Turning this feature set on IS NOT SUPPORTED, CANNOT BE UNDONE, and PREVENTS UPGRADES. - Because of its nature, this setting cannot be validated. If you have any typos or accidentally apply invalid combinations - your cluster may fail in an unrecoverable way. featureSet must equal "CustomNoUpgrade" must be set to use this field. - nullable: true - properties: - disabled: - description: disabled is a list of all feature gates - that you want to force off - items: - description: FeatureGateName is a string to enforce - patterns on the name of a FeatureGate - pattern: ^([A-Za-z0-9-]+\.)*[A-Za-z0-9-]+\.?$ - type: string - type: array - enabled: - description: enabled is a list of all feature gates - that you want to force on - items: - description: FeatureGateName is a string to enforce - patterns on the name of a FeatureGate - pattern: ^([A-Za-z0-9-]+\.)*[A-Za-z0-9-]+\.?$ - type: string - type: array - type: object - featureSet: - description: |- - featureSet changes the list of features in the cluster. The default is empty. Be very careful adjusting this setting. - Turning on or off features may cause irreversible changes in your cluster which cannot be undone. - enum: - - CustomNoUpgrade - - DevPreviewNoUpgrade - - TechPreviewNoUpgrade - - OKD - - "" - type: string - x-kubernetes-validations: - - message: CustomNoUpgrade may not be changed - rule: 'oldSelf == ''CustomNoUpgrade'' ? self == ''CustomNoUpgrade'' - : true' - - message: TechPreviewNoUpgrade may not be changed - rule: 'oldSelf == ''TechPreviewNoUpgrade'' ? self == - ''TechPreviewNoUpgrade'' : true' - - message: DevPreviewNoUpgrade may not be changed - rule: 'oldSelf == ''DevPreviewNoUpgrade'' ? self == - ''DevPreviewNoUpgrade'' : true' - - message: OKD cannot transition to Default - rule: 'oldSelf == ''OKD'' ? self != '''' : true' - type: object - image: - description: |- - image governs policies related to imagestream imports and runtime configuration - for external registries. It allows cluster admins to configure which registries - OpenShift is allowed to import images from, extra CA trust bundles for external - registries, and policies to block or allow registry hostnames. - When exposing OpenShift's image registry to the public, this also lets cluster - admins specify the external hostname. - This input will be part of every payload generated by the controllers for any NodePool of the HostedCluster. - Changing this value will trigger a rollout for all existing NodePools in the cluster. - properties: - additionalTrustedCA: - description: |- - additionalTrustedCA is a reference to a ConfigMap containing additional CAs that - should be trusted during imagestream import, pod image pull, build image pull, and - imageregistry pullthrough. - The namespace for this config map is openshift-config. - properties: - name: - description: name is the metadata.name of the referenced - config map - type: string - required: - - name - type: object - allowedRegistriesForImport: - description: |- - allowedRegistriesForImport limits the container image registries that normal users may import - images from. Set this list to the registries that you trust to contain valid Docker - images and that you want applications to be able to import from. Users with - permission to create Images or ImageStreamMappings via the API are not affected by - this policy - typically only administrators or system integrations will have those - permissions. - items: - description: |- - RegistryLocation contains a location of the registry specified by the registry domain - name. The domain name might include wildcards, like '*' or '??'. - properties: - domainName: - description: |- - domainName specifies a domain name for the registry - In case the registry use non-standard (80 or 443) port, the port should be included - in the domain name as well. - type: string - insecure: - description: |- - insecure indicates whether the registry is secure (https) or insecure (http) - By default (if not specified) the registry is assumed as secure. - type: boolean - type: object - type: array - x-kubernetes-list-type: atomic - externalRegistryHostnames: - description: |- - externalRegistryHostnames provides the hostnames for the default external image - registry. The external hostname should be set only when the image registry - is exposed externally. The first value is used in 'publicDockerImageRepository' - field in ImageStreams. The value must be in "hostname[:port]" format. - items: - type: string - type: array - x-kubernetes-list-type: atomic - imageStreamImportMode: - description: |- - imageStreamImportMode controls the import mode behaviour of imagestreams. - It can be set to `Legacy` or `PreserveOriginal` or the empty string. If this value - is specified, this setting is applied to all newly created imagestreams which do not have the - value set. `Legacy` indicates that the legacy behaviour should be used. - For manifest lists, the legacy behaviour will discard the manifest list and import a single - sub-manifest. In this case, the platform is chosen in the following order of priority: - 1. tag annotations; 2. control plane arch/os; 3. linux/amd64; 4. the first manifest in the list. - `PreserveOriginal` indicates that the original manifest will be preserved. For manifest lists, - the manifest list and all its sub-manifests will be imported. When empty, the behaviour will be - decided based on the payload type advertised by the ClusterVersion status, i.e single arch payload - implies the import mode is Legacy and multi payload implies PreserveOriginal. - enum: - - "" - - Legacy - - PreserveOriginal - type: string - registrySources: - description: |- - registrySources contains configuration that determines how the container runtime - should treat individual registries when accessing images for builds+pods. (e.g. - whether or not to allow insecure access). It does not contain configuration for the - internal cluster registry. - properties: - allowedRegistries: - description: |- - allowedRegistries are the only registries permitted for image pull and push actions. All other registries are denied. - - Only one of BlockedRegistries or AllowedRegistries may be set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - blockedRegistries: - description: |- - blockedRegistries cannot be used for image pull and push actions. All other registries are permitted. - - Only one of BlockedRegistries or AllowedRegistries may be set. - items: - type: string - type: array - x-kubernetes-list-type: atomic - containerRuntimeSearchRegistries: - description: |- - containerRuntimeSearchRegistries are registries that will be searched when pulling images that do not have fully qualified - domains in their pull specs. Registries will be searched in the order provided in the list. - Note: this search list only works with the container runtime, i.e CRI-O. Will NOT work with builds or imagestream imports. - format: hostname - items: - type: string - minItems: 1 - type: array - x-kubernetes-list-type: set - insecureRegistries: - description: insecureRegistries are registries which - do not have a valid TLS certificates or only support - HTTP connections. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - x-kubernetes-validations: - - message: Only one of blockedRegistries or allowedRegistries - may be set - rule: 'has(self.blockedRegistries) ? !has(self.allowedRegistries) - : true' - type: object - ingress: - description: |- - ingress holds cluster-wide information about ingress, including the default ingress domain - used for routes. - properties: - appsDomain: - description: |- - appsDomain is an optional domain to use instead of the one specified - in the domain field when a Route is created without specifying an explicit - host. If appsDomain is nonempty, this value is used to generate default - host values for Route. Unlike domain, appsDomain may be modified after - installation. - This assumes a new ingresscontroller has been setup with a wildcard - certificate. - type: string - componentRoutes: - description: |- - componentRoutes is an optional list of routes that are managed by OpenShift components - that a cluster-admin is able to configure the hostname and serving certificate for. - The namespace and name of each route in this list should match an existing entry in the - status.componentRoutes list. - - To determine the set of configurable Routes, look at namespace and name of entries in the - .status.componentRoutes list, where participating operators write the status of - configurable routes. - items: - description: ComponentRouteSpec allows for configuration - of a route's hostname and serving certificate. - properties: - hostname: - description: hostname is the hostname that should - be used by the route. - pattern: ^([a-zA-Z0-9\p{S}\p{L}]((-?[a-zA-Z0-9\p{S}\p{L}]{0,62})?)|([a-zA-Z0-9\p{S}\p{L}](([a-zA-Z0-9-\p{S}\p{L}]{0,61}[a-zA-Z0-9\p{S}\p{L}])?)(\.)){1,}([a-zA-Z\p{L}]){2,63})$|^(([a-z0-9][-a-z0-9]{0,61}[a-z0-9]|[a-z0-9]{1,63})[\.]){0,}([a-z0-9][-a-z0-9]{0,61}[a-z0-9]|[a-z0-9]{1,63})$ - type: string - name: - description: |- - name is the logical name of the route to customize. - - The namespace and name of this componentRoute must match a corresponding - entry in the list of status.componentRoutes if the route is to be customized. - maxLength: 256 - minLength: 1 - type: string - namespace: - description: |- - namespace is the namespace of the route to customize. - - The namespace and name of this componentRoute must match a corresponding - entry in the list of status.componentRoutes if the route is to be customized. - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - servingCertKeyPairSecret: - description: |- - servingCertKeyPairSecret is a reference to a secret of type `kubernetes.io/tls` in the openshift-config namespace. - The serving cert/key pair must match and will be used by the operator to fulfill the intent of serving with this name. - If the custom hostname uses the default routing suffix of the cluster, - the Secret specification for a serving certificate will not be needed. - properties: - name: - description: name is the metadata.name of the - referenced secret - type: string - required: - - name - type: object - required: - - hostname - - name - - namespace - type: object - type: array - x-kubernetes-list-map-keys: - - namespace - - name - x-kubernetes-list-type: map - domain: - description: |- - domain is used to generate a default host name for a route when the - route's host name is empty. The generated host name will follow this - pattern: "..". - - It is also used as the default wildcard domain suffix for ingress. The - default ingresscontroller domain will follow this pattern: "*.". - - Once set, changing domain is not currently supported. - type: string - x-kubernetes-validations: - - message: domain is immutable once set - rule: self == oldSelf - loadBalancer: - description: |- - loadBalancer contains the load balancer details in general which are not only specific to the underlying infrastructure - provider of the current cluster and are required for Ingress Controller to work on OpenShift. - properties: - platform: - description: |- - platform holds configuration specific to the underlying - infrastructure provider for the ingress load balancers. - When omitted, this means the user has no opinion and the platform is left - to choose reasonable defaults. These defaults are subject to change over time. - properties: - aws: - description: aws contains settings specific to - the Amazon Web Services infrastructure provider. - properties: - type: - description: |- - type allows user to set a load balancer type. - When this field is set the default ingresscontroller will get created using the specified LBType. - If this field is not set then the default ingress controller of LBType Classic will be created. - Valid values are: - - * "Classic": A Classic Load Balancer that makes routing decisions at either - the transport layer (TCP/SSL) or the application layer (HTTP/HTTPS). See - the following for additional details: - - https://docs.aws.amazon.com/AmazonECS/latest/developerguide/load-balancer-types.html#clb - - * "NLB": A Network Load Balancer that makes routing decisions at the - transport layer (TCP/SSL). See the following for additional details: - - https://docs.aws.amazon.com/AmazonECS/latest/developerguide/load-balancer-types.html#nlb - enum: - - NLB - - Classic - type: string - required: - - type - type: object - type: - description: |- - type is the underlying infrastructure provider for the cluster. - Allowed values are "AWS", "Azure", "BareMetal", "GCP", "Libvirt", - "OpenStack", "VSphere", "oVirt", "KubeVirt", "EquinixMetal", "PowerVS", - "AlibabaCloud", "Nutanix" and "None". Individual components may not support all platforms, - and must handle unrecognized platforms as None if they do not support that platform. - enum: - - "" - - AWS - - Azure - - BareMetal - - GCP - - Libvirt - - OpenStack - - None - - VSphere - - oVirt - - IBMCloud - - KubeVirt - - EquinixMetal - - PowerVS - - AlibabaCloud - - Nutanix - - External - type: string - type: object - type: object - requiredHSTSPolicies: - description: |- - requiredHSTSPolicies specifies HSTS policies that are required to be set on newly created or updated routes - matching the domainPattern/s and namespaceSelector/s that are specified in the policy. - Each requiredHSTSPolicy must have at least a domainPattern and a maxAge to validate a route HSTS Policy route - annotation, and affect route admission. - - A candidate route is checked for HSTS Policies if it has the HSTS Policy route annotation: - "haproxy.router.openshift.io/hsts_header" - E.g. haproxy.router.openshift.io/hsts_header: max-age=31536000;preload;includeSubDomains - - - For each candidate route, if it matches a requiredHSTSPolicy domainPattern and optional namespaceSelector, - then the maxAge, preloadPolicy, and includeSubdomainsPolicy must be valid to be admitted. Otherwise, the route - is rejected. - - The first match, by domainPattern and optional namespaceSelector, in the ordering of the RequiredHSTSPolicies - determines the route's admission status. - - If the candidate route doesn't match any requiredHSTSPolicy domainPattern and optional namespaceSelector, - then it may use any HSTS Policy annotation. - - The HSTS policy configuration may be changed after routes have already been created. An update to a previously - admitted route may then fail if the updated route does not conform to the updated HSTS policy configuration. - However, changing the HSTS policy configuration will not cause a route that is already admitted to stop working. - - Note that if there are no RequiredHSTSPolicies, any HSTS Policy annotation on the route is valid. - items: - properties: - domainPatterns: - description: |- - domainPatterns is a list of domains for which the desired HSTS annotations are required. - If domainPatterns is specified and a route is created with a spec.host matching one of the domains, - the route must specify the HSTS Policy components described in the matching RequiredHSTSPolicy. - - The use of wildcards is allowed like this: *.foo.com matches everything under foo.com. - foo.com only matches foo.com, so to cover foo.com and everything under it, you must specify *both*. - items: - type: string - minItems: 1 - type: array - includeSubDomainsPolicy: - description: |- - includeSubDomainsPolicy means the HSTS Policy should apply to any subdomains of the host's - domain name. Thus, for the host bar.foo.com, if includeSubDomainsPolicy was set to RequireIncludeSubDomains: - - the host app.bar.foo.com would inherit the HSTS Policy of bar.foo.com - - the host bar.foo.com would inherit the HSTS Policy of bar.foo.com - - the host foo.com would NOT inherit the HSTS Policy of bar.foo.com - - the host def.foo.com would NOT inherit the HSTS Policy of bar.foo.com - enum: - - RequireIncludeSubDomains - - RequireNoIncludeSubDomains - - NoOpinion - type: string - maxAge: - description: |- - maxAge is the delta time range in seconds during which hosts are regarded as HSTS hosts. - If set to 0, it negates the effect, and hosts are removed as HSTS hosts. - If set to 0 and includeSubdomains is specified, all subdomains of the host are also removed as HSTS hosts. - maxAge is a time-to-live value, and if this policy is not refreshed on a client, the HSTS - policy will eventually expire on that client. - properties: - largestMaxAge: - description: |- - The largest allowed value (in seconds) of the RequiredHSTSPolicy max-age - This value can be left unspecified, in which case no upper limit is enforced. - format: int32 - maximum: 2147483647 - minimum: 0 - type: integer - smallestMaxAge: - description: |- - The smallest allowed value (in seconds) of the RequiredHSTSPolicy max-age - Setting max-age=0 allows the deletion of an existing HSTS header from a host. This is a necessary - tool for administrators to quickly correct mistakes. - This value can be left unspecified, in which case no lower limit is enforced. - format: int32 - maximum: 2147483647 - minimum: 0 - type: integer - type: object - namespaceSelector: - description: |- - namespaceSelector specifies a label selector such that the policy applies only to those routes that - are in namespaces with labels that match the selector, and are in one of the DomainPatterns. - Defaults to the empty LabelSelector, which matches everything. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - preloadPolicy: - description: |- - preloadPolicy directs the client to include hosts in its host preload list so that - it never needs to do an initial load to get the HSTS header (note that this is not defined - in RFC 6797 and is therefore client implementation-dependent). - enum: - - RequirePreload - - RequireNoPreload - - NoOpinion - type: string - required: - - domainPatterns - type: object - type: array - type: object - network: - description: |- - network holds cluster-wide information about the network. It is used to configure the desired network configuration, such as: IP address pools for services/pod IPs, network plugin, etc. - Please view network.spec for an explanation on what applies when configuring this resource. - properties: - clusterNetwork: - description: |- - IP address pool to use for pod IPs. - This field is immutable after installation. - items: - description: |- - ClusterNetworkEntry is a contiguous block of IP addresses from which pod IPs - are allocated. - properties: - cidr: - description: The complete block for pod IPs. - type: string - hostPrefix: - description: |- - The size (prefix) of block to allocate to each node. If this - field is not used by the plugin, it can be left unset. - format: int32 - minimum: 0 - type: integer - type: object - type: array - x-kubernetes-list-type: atomic - externalIP: - description: |- - externalIP defines configuration for controllers that - affect Service.ExternalIP. If nil, then ExternalIP is - not allowed to be set. - properties: - autoAssignCIDRs: - description: |- - autoAssignCIDRs is a list of CIDRs from which to automatically assign - Service.ExternalIP. These are assigned when the service is of type - LoadBalancer. In general, this is only useful for bare-metal clusters. - In Openshift 3.x, this was misleadingly called "IngressIPs". - Automatically assigned External IPs are not affected by any - ExternalIPPolicy rules. - Currently, only one entry may be provided. - items: - type: string - type: array - x-kubernetes-list-type: atomic - policy: - description: |- - policy is a set of restrictions applied to the ExternalIP field. - If nil or empty, then ExternalIP is not allowed to be set. - properties: - allowedCIDRs: - description: allowedCIDRs is the list of allowed - CIDRs. - items: - type: string - type: array - x-kubernetes-list-type: atomic - rejectedCIDRs: - description: |- - rejectedCIDRs is the list of disallowed CIDRs. These take precedence - over allowedCIDRs. - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - type: object - networkDiagnostics: - description: |- - networkDiagnostics defines network diagnostics configuration. - - Takes precedence over spec.disableNetworkDiagnostics in network.operator.openshift.io. - If networkDiagnostics is not specified or is empty, - and the spec.disableNetworkDiagnostics flag in network.operator.openshift.io is set to true, - the network diagnostics feature will be disabled. - properties: - mode: - description: |- - mode controls the network diagnostics mode - - When omitted, this means the user has no opinion and the platform is left - to choose reasonable defaults. These defaults are subject to change over time. - The current default is All. - enum: - - "" - - All - - Disabled - type: string - sourcePlacement: - description: |- - sourcePlacement controls the scheduling of network diagnostics source deployment - - See NetworkDiagnosticsSourcePlacement for more details about default values. - properties: - nodeSelector: - additionalProperties: - type: string - description: |- - nodeSelector is the node selector applied to network diagnostics components - - When omitted, this means the user has no opinion and the platform is left - to choose reasonable defaults. These defaults are subject to change over time. - The current default is `kubernetes.io/os: linux`. - type: object - tolerations: - description: |- - tolerations is a list of tolerations applied to network diagnostics components - - When omitted, this means the user has no opinion and the platform is left - to choose reasonable defaults. These defaults are subject to change over time. - The current default is an empty list. - items: - description: |- - The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . - properties: - effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. - type: string - key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. - type: string - operator: - description: |- - Operator represents a key's relationship to the value. - Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. - Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). - type: string - tolerationSeconds: - description: |- - TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). Zero and - negative values will be treated as 0 (evict immediately) by the system. - format: int64 - type: integer - value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. - type: string - type: object - type: array - x-kubernetes-list-type: atomic - type: object - targetPlacement: - description: |- - targetPlacement controls the scheduling of network diagnostics target daemonset - - See NetworkDiagnosticsTargetPlacement for more details about default values. - properties: - nodeSelector: - additionalProperties: - type: string - description: |- - nodeSelector is the node selector applied to network diagnostics components - - When omitted, this means the user has no opinion and the platform is left - to choose reasonable defaults. These defaults are subject to change over time. - The current default is `kubernetes.io/os: linux`. - type: object - tolerations: - description: |- - tolerations is a list of tolerations applied to network diagnostics components - - When omitted, this means the user has no opinion and the platform is left - to choose reasonable defaults. These defaults are subject to change over time. - The current default is `- operator: "Exists"` which means that all taints are tolerated. - items: - description: |- - The pod this Toleration is attached to tolerates any taint that matches - the triple using the matching operator . - properties: - effect: - description: |- - Effect indicates the taint effect to match. Empty means match all taint effects. - When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. - type: string - key: - description: |- - Key is the taint key that the toleration applies to. Empty means match all taint keys. - If the key is empty, operator must be Exists; this combination means to match all values and all keys. - type: string - operator: - description: |- - Operator represents a key's relationship to the value. - Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod can - tolerate all taints of a particular category. - Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). - type: string - tolerationSeconds: - description: |- - TolerationSeconds represents the period of time the toleration (which must be - of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, - it is not set, which means tolerate the taint forever (do not evict). Zero and - negative values will be treated as 0 (evict immediately) by the system. - format: int64 - type: integer - value: - description: |- - Value is the taint value the toleration matches to. - If the operator is Exists, the value should be empty, otherwise just a regular string. - type: string - type: object - type: array - x-kubernetes-list-type: atomic - type: object - type: object - networkType: - description: |- - networkType is the plugin that is to be deployed (e.g. OVNKubernetes). - This should match a value that the cluster-network-operator understands, - or else no networking will be installed. - Currently supported values are: - - OVNKubernetes - This field is immutable after installation. - type: string - serviceNetwork: - description: |- - IP address pool for services. - Currently, we only support a single entry here. - This field is immutable after installation. - items: - type: string - type: array - x-kubernetes-list-type: atomic - serviceNodePortRange: - description: |- - The port range allowed for Services of type NodePort. - If not specified, the default of 30000-32767 will be used. - Such Services without a NodePort specified will have one - automatically allocated from this range. - This parameter can be updated after the cluster is - installed. - pattern: ^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])-([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$ - type: string - type: object - x-kubernetes-validations: - - message: cannot set networkDiagnostics.sourcePlacement and - networkDiagnostics.targetPlacement when networkDiagnostics.mode - is Disabled - rule: '!has(self.networkDiagnostics) || !has(self.networkDiagnostics.mode) - || self.networkDiagnostics.mode!=''Disabled'' || !has(self.networkDiagnostics.sourcePlacement) - && !has(self.networkDiagnostics.targetPlacement)' - oauth: - description: |- - oauth holds cluster-wide information about OAuth. - It is used to configure the integrated OAuth server. - This configuration is only honored when the top level Authentication config has type set to IntegratedOAuth. - properties: - identityProviders: - description: |- - identityProviders is an ordered list of ways for a user to identify themselves. - When this list is empty, no identities are provisioned for users. - items: - description: IdentityProvider provides identities for - users authenticating using credentials - properties: - basicAuth: - description: basicAuth contains configuration options - for the BasicAuth IdP - properties: - ca: - description: |- - ca is an optional reference to a config map by name containing the PEM-encoded CA bundle. - It is used as a trust anchor to validate the TLS certificate presented by the remote server. - The key "ca.crt" is used to locate the data. - If specified and the config map or expected key is not found, the identity provider is not honored. - If the specified ca data is not valid, the identity provider is not honored. - If empty, the default system roots are used. - The namespace for this config map is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced config map - type: string - required: - - name - type: object - tlsClientCert: - description: |- - tlsClientCert is an optional reference to a secret by name that contains the - PEM-encoded TLS client certificate to present when connecting to the server. - The key "tls.crt" is used to locate the data. - If specified and the secret or expected key is not found, the identity provider is not honored. - If the specified certificate data is not valid, the identity provider is not honored. - The namespace for this secret is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced secret - type: string - required: - - name - type: object - tlsClientKey: - description: |- - tlsClientKey is an optional reference to a secret by name that contains the - PEM-encoded TLS private key for the client certificate referenced in tlsClientCert. - The key "tls.key" is used to locate the data. - If specified and the secret or expected key is not found, the identity provider is not honored. - If the specified certificate data is not valid, the identity provider is not honored. - The namespace for this secret is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced secret - type: string - required: - - name - type: object - url: - description: url is the remote URL to connect - to - type: string - type: object - github: - description: github enables user authentication - using GitHub credentials - properties: - ca: - description: |- - ca is an optional reference to a config map by name containing the PEM-encoded CA bundle. - It is used as a trust anchor to validate the TLS certificate presented by the remote server. - The key "ca.crt" is used to locate the data. - If specified and the config map or expected key is not found, the identity provider is not honored. - If the specified ca data is not valid, the identity provider is not honored. - If empty, the default system roots are used. - This can only be configured when hostname is set to a non-empty value. - The namespace for this config map is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced config map - type: string - required: - - name - type: object - clientID: - description: clientID is the oauth client ID - type: string - clientSecret: - description: |- - clientSecret is a required reference to the secret by name containing the oauth client secret. - The key "clientSecret" is used to locate the data. - If the secret or expected key is not found, the identity provider is not honored. - The namespace for this secret is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced secret - type: string - required: - - name - type: object - hostname: - description: |- - hostname is the optional domain (e.g. "mycompany.com") for use with a hosted instance of - GitHub Enterprise. - It must match the GitHub Enterprise settings value configured at /setup/settings#hostname. - type: string - organizations: - description: organizations optionally restricts - which organizations are allowed to log in - items: - type: string - type: array - teams: - description: teams optionally restricts which - teams are allowed to log in. Format is /. - items: - type: string - type: array - type: object - gitlab: - description: gitlab enables user authentication - using GitLab credentials - properties: - ca: - description: |- - ca is an optional reference to a config map by name containing the PEM-encoded CA bundle. - It is used as a trust anchor to validate the TLS certificate presented by the remote server. - The key "ca.crt" is used to locate the data. - If specified and the config map or expected key is not found, the identity provider is not honored. - If the specified ca data is not valid, the identity provider is not honored. - If empty, the default system roots are used. - The namespace for this config map is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced config map - type: string - required: - - name - type: object - clientID: - description: clientID is the oauth client ID - type: string - clientSecret: - description: |- - clientSecret is a required reference to the secret by name containing the oauth client secret. - The key "clientSecret" is used to locate the data. - If the secret or expected key is not found, the identity provider is not honored. - The namespace for this secret is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced secret - type: string - required: - - name - type: object - url: - description: url is the oauth server base URL - type: string - type: object - google: - description: google enables user authentication - using Google credentials - properties: - clientID: - description: clientID is the oauth client ID - type: string - clientSecret: - description: |- - clientSecret is a required reference to the secret by name containing the oauth client secret. - The key "clientSecret" is used to locate the data. - If the secret or expected key is not found, the identity provider is not honored. - The namespace for this secret is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced secret - type: string - required: - - name - type: object - hostedDomain: - description: hostedDomain is the optional Google - App domain (e.g. "mycompany.com") to restrict - logins to - type: string - type: object - htpasswd: - description: htpasswd enables user authentication - using an HTPasswd file to validate credentials - properties: - fileData: - description: |- - fileData is a required reference to a secret by name containing the data to use as the htpasswd file. - The key "htpasswd" is used to locate the data. - If the secret or expected key is not found, the identity provider is not honored. - If the specified htpasswd data is not valid, the identity provider is not honored. - The namespace for this secret is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced secret - type: string - required: - - name - type: object - type: object - keystone: - description: keystone enables user authentication - using keystone password credentials - properties: - ca: - description: |- - ca is an optional reference to a config map by name containing the PEM-encoded CA bundle. - It is used as a trust anchor to validate the TLS certificate presented by the remote server. - The key "ca.crt" is used to locate the data. - If specified and the config map or expected key is not found, the identity provider is not honored. - If the specified ca data is not valid, the identity provider is not honored. - If empty, the default system roots are used. - The namespace for this config map is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced config map - type: string - required: - - name - type: object - domainName: - description: domainName is required for keystone - v3 - type: string - tlsClientCert: - description: |- - tlsClientCert is an optional reference to a secret by name that contains the - PEM-encoded TLS client certificate to present when connecting to the server. - The key "tls.crt" is used to locate the data. - If specified and the secret or expected key is not found, the identity provider is not honored. - If the specified certificate data is not valid, the identity provider is not honored. - The namespace for this secret is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced secret - type: string - required: - - name - type: object - tlsClientKey: - description: |- - tlsClientKey is an optional reference to a secret by name that contains the - PEM-encoded TLS private key for the client certificate referenced in tlsClientCert. - The key "tls.key" is used to locate the data. - If specified and the secret or expected key is not found, the identity provider is not honored. - If the specified certificate data is not valid, the identity provider is not honored. - The namespace for this secret is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced secret - type: string - required: - - name - type: object - url: - description: url is the remote URL to connect - to - type: string - type: object - ldap: - description: ldap enables user authentication using - LDAP credentials - properties: - attributes: - description: attributes maps LDAP attributes - to identities - properties: - email: - description: |- - email is the list of attributes whose values should be used as the email address. Optional. - If unspecified, no email is set for the identity - items: - type: string - type: array - id: - description: |- - id is the list of attributes whose values should be used as the user ID. Required. - First non-empty attribute is used. At least one attribute is required. If none of the listed - attribute have a value, authentication fails. - LDAP standard identity attribute is "dn" - items: - type: string - type: array - name: - description: |- - name is the list of attributes whose values should be used as the display name. Optional. - If unspecified, no display name is set for the identity - LDAP standard display name attribute is "cn" - items: - type: string - type: array - preferredUsername: - description: |- - preferredUsername is the list of attributes whose values should be used as the preferred username. - LDAP standard login attribute is "uid" - items: - type: string - type: array - type: object - bindDN: - description: bindDN is an optional DN to bind - with during the search phase. - type: string - bindPassword: - description: |- - bindPassword is an optional reference to a secret by name - containing a password to bind with during the search phase. - The key "bindPassword" is used to locate the data. - If specified and the secret or expected key is not found, the identity provider is not honored. - The namespace for this secret is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced secret - type: string - required: - - name - type: object - ca: - description: |- - ca is an optional reference to a config map by name containing the PEM-encoded CA bundle. - It is used as a trust anchor to validate the TLS certificate presented by the remote server. - The key "ca.crt" is used to locate the data. - If specified and the config map or expected key is not found, the identity provider is not honored. - If the specified ca data is not valid, the identity provider is not honored. - If empty, the default system roots are used. - The namespace for this config map is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced config map - type: string - required: - - name - type: object - insecure: - description: |- - insecure, if true, indicates the connection should not use TLS - WARNING: Should not be set to `true` with the URL scheme "ldaps://" as "ldaps://" URLs always - attempt to connect using TLS, even when `insecure` is set to `true` - When `true`, "ldap://" URLS connect insecurely. When `false`, "ldap://" URLs are upgraded to - a TLS connection using StartTLS as specified in https://tools.ietf.org/html/rfc2830. - type: boolean - url: - description: |- - url is an RFC 2255 URL which specifies the LDAP search parameters to use. - The syntax of the URL is: - ldap://host:port/basedn?attribute?scope?filter - type: string - type: object - mappingMethod: - description: |- - mappingMethod determines how identities from this provider are mapped to users - Defaults to "claim" - type: string - name: - description: |- - name is used to qualify the identities returned by this provider. - - It MUST be unique and not shared by any other identity provider used - - It MUST be a valid path segment: name cannot equal "." or ".." or contain "/" or "%" or ":" - Ref: https://godoc.org/github.com/openshift/origin/pkg/user/apis/user/validation#ValidateIdentityProviderName - type: string - openID: - description: openID enables user authentication - using OpenID credentials - properties: - ca: - description: |- - ca is an optional reference to a config map by name containing the PEM-encoded CA bundle. - It is used as a trust anchor to validate the TLS certificate presented by the remote server. - The key "ca.crt" is used to locate the data. - If specified and the config map or expected key is not found, the identity provider is not honored. - If the specified ca data is not valid, the identity provider is not honored. - If empty, the default system roots are used. - The namespace for this config map is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced config map - type: string - required: - - name - type: object - claims: - description: claims mappings - properties: - email: - description: |- - email is the list of claims whose values should be used as the email address. Optional. - If unspecified, no email is set for the identity - items: - type: string - type: array - x-kubernetes-list-type: atomic - groups: - description: |- - groups is the list of claims value of which should be used to synchronize groups - from the OIDC provider to OpenShift for the user. - If multiple claims are specified, the first one with a non-empty value is used. - items: - description: |- - OpenIDClaim represents a claim retrieved from an OpenID provider's tokens or userInfo - responses - minLength: 1 - type: string - type: array - x-kubernetes-list-type: atomic - name: - description: |- - name is the list of claims whose values should be used as the display name. Optional. - If unspecified, no display name is set for the identity - items: - type: string - type: array - x-kubernetes-list-type: atomic - preferredUsername: - description: |- - preferredUsername is the list of claims whose values should be used as the preferred username. - If unspecified, the preferred username is determined from the value of the sub claim - items: - type: string - type: array - x-kubernetes-list-type: atomic - type: object - clientID: - description: clientID is the oauth client ID - type: string - clientSecret: - description: |- - clientSecret is a required reference to the secret by name containing the oauth client secret. - The key "clientSecret" is used to locate the data. - If the secret or expected key is not found, the identity provider is not honored. - The namespace for this secret is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced secret - type: string - required: - - name - type: object - extraAuthorizeParameters: - additionalProperties: - type: string - description: extraAuthorizeParameters are any - custom parameters to add to the authorize - request. - type: object - extraScopes: - description: extraScopes are any scopes to request - in addition to the standard "openid" scope. - items: - type: string - type: array - issuer: - description: |- - issuer is the URL that the OpenID Provider asserts as its Issuer Identifier. - It must use the https scheme with no query or fragment component. - type: string - type: object - requestHeader: - description: requestHeader enables user authentication - using request header credentials - properties: - ca: - description: |- - ca is a required reference to a config map by name containing the PEM-encoded CA bundle. - It is used as a trust anchor to validate the TLS certificate presented by the remote server. - Specifically, it allows verification of incoming requests to prevent header spoofing. - The key "ca.crt" is used to locate the data. - If the config map or expected key is not found, the identity provider is not honored. - If the specified ca data is not valid, the identity provider is not honored. - The namespace for this config map is openshift-config. - properties: - name: - description: name is the metadata.name of - the referenced config map - type: string - required: - - name - type: object - challengeURL: - description: |- - challengeURL is a URL to redirect unauthenticated /authorize requests to - Unauthenticated requests from OAuth clients which expect WWW-Authenticate challenges will be - redirected here. - ${url} is replaced with the current URL, escaped to be safe in a query parameter - https://www.example.com/sso-login?then=${url} - ${query} is replaced with the current query string - https://www.example.com/auth-proxy/oauth/authorize?${query} - Required when challenge is set to true. - type: string - clientCommonNames: - description: |- - clientCommonNames is an optional list of common names to require a match from. If empty, any - client certificate validated against the clientCA bundle is considered authoritative. - items: - type: string - type: array - emailHeaders: - description: emailHeaders is the set of headers - to check for the email address - items: - type: string - type: array - headers: - description: headers is the set of headers to - check for identity information - items: - type: string - type: array - loginURL: - description: |- - loginURL is a URL to redirect unauthenticated /authorize requests to - Unauthenticated requests from OAuth clients which expect interactive logins will be redirected here - ${url} is replaced with the current URL, escaped to be safe in a query parameter - https://www.example.com/sso-login?then=${url} - ${query} is replaced with the current query string - https://www.example.com/auth-proxy/oauth/authorize?${query} - Required when login is set to true. - type: string - nameHeaders: - description: nameHeaders is the set of headers - to check for the display name - items: - type: string - type: array - preferredUsernameHeaders: - description: preferredUsernameHeaders is the - set of headers to check for the preferred - username - items: - type: string - type: array - type: object - type: - description: type identifies the identity provider - type for this entry. - type: string - type: object - type: array - x-kubernetes-list-type: atomic - templates: - description: templates allow you to customize pages like - the login page. - properties: - error: - description: |- - error is the name of a secret that specifies a go template to use to render error pages - during the authentication or grant flow. - The key "errors.html" is used to locate the template data. - If specified and the secret or expected key is not found, the default error page is used. - If the specified template is not valid, the default error page is used. - If unspecified, the default error page is used. - The namespace for this secret is openshift-config. - properties: - name: - description: name is the metadata.name of the - referenced secret - type: string - required: - - name - type: object - login: - description: |- - login is the name of a secret that specifies a go template to use to render the login page. - The key "login.html" is used to locate the template data. - If specified and the secret or expected key is not found, the default login page is used. - If the specified template is not valid, the default login page is used. - If unspecified, the default login page is used. - The namespace for this secret is openshift-config. - properties: - name: - description: name is the metadata.name of the - referenced secret - type: string - required: - - name - type: object - providerSelection: - description: |- - providerSelection is the name of a secret that specifies a go template to use to render - the provider selection page. - The key "providers.html" is used to locate the template data. - If specified and the secret or expected key is not found, the default provider selection page is used. - If the specified template is not valid, the default provider selection page is used. - If unspecified, the default provider selection page is used. - The namespace for this secret is openshift-config. - properties: - name: - description: name is the metadata.name of the - referenced secret - type: string - required: - - name - type: object - type: object - tokenConfig: - description: tokenConfig contains options for authorization - and access tokens - properties: - accessTokenInactivityTimeout: - description: |- - accessTokenInactivityTimeout defines the token inactivity timeout - for tokens granted by any client. - The value represents the maximum amount of time that can occur between - consecutive uses of the token. Tokens become invalid if they are not - used within this temporal window. The user will need to acquire a new - token to regain access once a token times out. Takes valid time - duration string such as "5m", "1.5h" or "2h45m". The minimum allowed - value for duration is 300s (5 minutes). If the timeout is configured - per client, then that value takes precedence. If the timeout value is - not specified and the client does not override the value, then tokens - are valid until their lifetime. - - WARNING: existing tokens' timeout will not be affected (lowered) by changing this value - type: string - accessTokenInactivityTimeoutSeconds: - description: 'accessTokenInactivityTimeoutSeconds - - DEPRECATED: setting this field has no effect.' - format: int32 - type: integer - accessTokenMaxAgeSeconds: - description: accessTokenMaxAgeSeconds defines the - maximum age of access tokens - format: int32 - type: integer - type: object + maximum: 86400 + minimum: 0 + type: integer + delayAfterFailureSeconds: + description: |- + delayAfterFailureSeconds sets how long after a scale down failure, scale down evaluation resumes. + It must be between 0 and 86400 (24 hours). + When set to 0, this means scale down evaluation will resume immediately after a scale down failure, without any delay. + When omitted, the autoscaler defaults to 180s (3 minutes). + format: int32 + maximum: 86400 + minimum: 0 + type: integer + unneededDurationSeconds: + description: |- + unneededDurationSeconds establishes how long a node should be unneeded before it is eligible for scale down in seconds. + It must be between 0 and 86400 (24 hours). + When omitted, the autoscaler defaults to 600s (10 minutes). + format: int32 + maximum: 86400 + minimum: 0 + type: integer + utilizationThresholdPercent: + description: |- + utilizationThresholdPercent determines the node utilization level, defined as sum of requested resources divided by capacity, below which a node can be considered for scale down. + The value represents a percentage from 0 to 100. + When set to 0, this means nodes will only be considered for scale down if they are completely idle (0% utilization). + When set to 100, this means nodes will be considered for scale down regardless of their utilization level. + A value between 0 and 100 represents the utilization threshold below which a node can be considered for scale down. + When omitted, the autoscaler defaults to 50%. + format: int32 + maximum: 100 + minimum: 0 + type: integer type: object + scaling: + default: ScaleUpAndScaleDown + description: |- + scaling defines the scaling behavior for the cluster autoscaler. + ScaleUpOnly means the autoscaler will only scale up nodes, never scale down. + ScaleUpAndScaleDown means the autoscaler will both scale up and scale down nodes. + When set to ScaleUpAndScaleDown, the scaleDown field can be used to configure scale down behavior. + + Note: This field is only supported in OpenShift versions 4.19 and above. + enum: + - ScaleUpOnly + - ScaleUpAndScaleDown + type: string + type: object + x-kubernetes-validations: + - message: scaleDown can only be set when scaling is ScaleUpAndScaleDown + rule: 'self.scaling == ''ScaleUpAndScaleDown'' ? true : !has(self.scaleDown)' + capabilities: + description: capabilities allows for disabling optional components + at cluster install time. + properties: + disabled: + description: |- + disabled when specified, explicitly disables the specified capabilitíes on the hosted cluster. + Once set, this field cannot be changed. + + Note: Disabling 'openshift-samples','Insights', 'Console', 'NodeTuning', 'Ingress' are only supported in OpenShift versions 4.20 and above. + items: + enum: + - ImageRegistry + - openshift-samples + - Insights + - baremetal + - Console + - NodeTuning + - Ingress + type: string + maxItems: 25 + type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - - message: spec.configuration.oauth.tokenConfig.accessTokenInactivityTimeout - minimum acceptable token timeout value is 300 seconds - rule: '!has(self.tokenConfig) || !has(self.tokenConfig.accessTokenInactivityTimeout) - || duration(self.tokenConfig.accessTokenInactivityTimeout).getSeconds() - >= 300' - operatorhub: + - message: Disabled is immutable. Changes might result in + unpredictable and disruptive behavior. + rule: self == oldSelf + - message: Ingress capability can only be disabled if Console + capability is also disabled + rule: '!self.exists(cap, cap == ''Ingress'') || self.exists(cap, + cap == ''Console'')' + enabled: description: |- - operatorhub specifies the configuration for the Operator Lifecycle Manager in the HostedCluster. This is only configured at deployment time but the controller are not reconcilling over it. - The OperatorHub configuration will be constantly reconciled if catalog placement is management, but only on cluster creation otherwise. + enabled when specified, explicitly enables the specified capabilitíes on the hosted cluster. + Once set, this field cannot be changed. + items: + enum: + - ImageRegistry + - openshift-samples + - Insights + - baremetal + - Console + - NodeTuning + - Ingress + type: string + maxItems: 25 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: Enabled is immutable. Changes might result in unpredictable + and disruptive behavior. + rule: self == oldSelf + type: object + x-kubernetes-validations: + - message: Capabilities can not be both enabled and disabled at + once. + rule: 'has(self.enabled) && has(self.disabled) ? self.enabled.all(e, + !(e in self.disabled)) : true' + channel: + description: channel is an identifier for explicitly requesting + that a non-default set of updates be applied to this cluster. + type: string + clusterID: + description: clusterID uniquely identifies this cluster. This + is expected to be an RFC4122 UUID value (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + in hexadecimal digits). + type: string + configuration: + description: configuration specifies configuration for individual + OCP components in the + properties: + apiServer: + description: apiServer contains advanced network settings + for the API server. + type: object + authentication: + description: authentication contains configuration for the + cluster authentication. + type: object + featureGate: + description: featureGate contains the desired configuration + for feature gates. + type: object + image: + description: image contains the configuration for internal + registry. + type: object + ingress: + description: ingress contains the configuration for ingress. + type: object + kubelet: + description: kubelet contains the configuration for kubelet + on nodes. properties: - disableAllDefaultSources: - description: |- - disableAllDefaultSources allows you to disable all the default hub - sources. If this is true, a specific entry in sources can be used to - enable a default source. If this is false, a specific entry in - sources can be used to disable or enable a default source. + allowedUnsafeSysctls: + items: + type: string + maxItems: 256 + type: array + containerLogMaxFiles: + format: int32 + type: integer + containerLogMaxSize: + type: string + cpuManagerPolicy: + type: string + cpuManagerPolicyOptions: + additionalProperties: + type: string + maxProperties: 32 + type: object + cpuManagerReconcilePeriod: + type: string + evictionHard: + additionalProperties: + type: string + maxProperties: 32 + type: object + evictionSoft: + additionalProperties: + type: string + maxProperties: 32 + type: object + evictionSoftGracePeriod: + additionalProperties: + type: string + maxProperties: 32 + type: object + imageGCHighThresholdPercent: + format: int32 + type: integer + imageGCLowThresholdPercent: + format: int32 + type: integer + imageMinimumGCAge: + type: string + kubeReserved: + additionalProperties: + type: string + maxProperties: 32 + type: object + maxPods: + format: int32 + type: integer + memoryThrottlingFactor: + type: number + podPidsLimit: + format: int64 + type: integer + registryBurst: + format: int32 + type: integer + registryPullQPS: + format: int32 + type: integer + serializeImagePulls: type: boolean - sources: - description: |- - sources is the list of default hub sources and their configuration. - If the list is empty, it implies that the default hub sources are - enabled on the cluster unless disableAllDefaultSources is true. - If disableAllDefaultSources is true and sources is not empty, - the configuration present in sources will take precedence. The list of - default hub sources and their current state will always be reflected in - the status block. + streamingConnectionIdleTimeout: + type: string + systemReserved: + additionalProperties: + type: string + maxProperties: 32 + type: object + topologyManagerPolicy: + type: string + topologyManagerScope: + type: string + type: object + machineConfig: + description: machineConfig contains the configuration for + machine-level settings. + properties: + allowedKernelArguments: + items: + type: string + maxItems: 128 + type: array + extensions: + items: + type: string + maxItems: 64 + type: array + files: items: - description: HubSource is used to specify the hub source - and its configuration properties: - disabled: - description: disabled is used to disable a default - hub source on cluster + contents: + maxLength: 262144 + type: string + group: + type: string + mode: + format: int32 + type: integer + overwrite: type: boolean - name: - description: name is the name of one of the default - hub sources - maxLength: 253 - minLength: 1 + path: + type: string + user: type: string + required: + - path type: object + maxItems: 256 type: array - type: object - proxy: - description: |- - proxy holds cluster-wide information on how to configure default proxies for the cluster. - This affects traffic flowing from the hosted cluster data plane. - The controllers will generate a machineConfig with the proxy config for the cluster. - This MachineConfig will be part of every payload generated by the controllers for any NodePool of the HostedCluster. - Changing this value will trigger a rollout for all existing NodePools in the cluster. - properties: - httpProxy: - description: httpProxy is the URL of the proxy for HTTP - requests. Empty means unset and will not result in - an env var. - type: string - httpsProxy: - description: httpsProxy is the URL of the proxy for HTTPS - requests. Empty means unset and will not result in - an env var. - type: string - noProxy: - description: |- - noProxy is a comma-separated list of hostnames and/or CIDRs and/or IPs for which the proxy should not be used. - Empty means unset and will not result in an env var. - type: string - readinessEndpoints: - description: readinessEndpoints is a list of endpoints - used to verify readiness of the proxy. + kernelArguments: items: type: string + maxItems: 128 + type: array + kernelType: + type: string + systemdUnits: + items: + properties: + contents: + maxLength: 65536 + type: string + dropins: + items: + properties: + contents: + maxLength: 32768 + type: string + name: + type: string + required: + - name + type: object + maxItems: 16 + type: array + enabled: + type: boolean + name: + type: string + required: + - name + type: object + maxItems: 64 type: array - trustedCA: - description: |- - trustedCA is a reference to a ConfigMap containing a CA certificate bundle. - The trustedCA field should only be consumed by a proxy validator. The - validator is responsible for reading the certificate bundle from the required - key "ca-bundle.crt", merging it with the system default trust bundle, - and writing the merged trust bundle to a ConfigMap named "trusted-ca-bundle" - in the "openshift-config-managed" namespace. Clients that expect to make - proxy connections must use the trusted-ca-bundle for all HTTPS requests to - the proxy, and may use the trusted-ca-bundle for non-proxy HTTPS requests as - well. - - The namespace for the ConfigMap referenced by trustedCA is - "openshift-config". Here is an example ConfigMap (in yaml): - - apiVersion: v1 - kind: ConfigMap - metadata: - name: user-ca-bundle - namespace: openshift-config - data: - ca-bundle.crt: | - -----BEGIN CERTIFICATE----- - Custom CA certificate bundle. - -----END CERTIFICATE----- - properties: - name: - description: name is the metadata.name of the referenced - config map - type: string - required: - - name - type: object + type: object + network: + description: network contains the configuration for cluster + networking. + type: object + oauth: + description: oauth contains the configuration for OAuth. + type: object + proxy: + description: proxy contains the configuration for the cluster-wide + proxy. type: object scheduler: - description: |- - scheduler holds cluster-wide config information to run the Kubernetes Scheduler - and influence its placement decisions. The canonical name for this config is `cluster`. - properties: - defaultNodeSelector: - description: |- - defaultNodeSelector helps set the cluster-wide default node selector to - restrict pod placement to specific nodes. This is applied to the pods - created in all namespaces and creates an intersection with any existing - nodeSelectors already set on a pod, additionally constraining that pod's selector. - For example, - defaultNodeSelector: "type=user-node,region=east" would set nodeSelector - field in pod spec to "type=user-node,region=east" to all pods created - in all namespaces. Namespaces having project-wide node selectors won't be - impacted even if this field is set. This adds an annotation section to - the namespace. - For example, if a new namespace is created with - node-selector='type=user-node,region=east', - the annotation openshift.io/node-selector: type=user-node,region=east - gets added to the project. When the openshift.io/node-selector annotation - is set on the project the value is used in preference to the value we are setting - for defaultNodeSelector field. - For instance, - openshift.io/node-selector: "type=user-node,region=west" means - that the default of "type=user-node,region=east" set in defaultNodeSelector - would not be applied. - type: string - mastersSchedulable: - description: |- - mastersSchedulable allows masters nodes to be schedulable. When this flag is - turned on, all the master nodes in the cluster will be made schedulable, - so that workload pods can run on them. The default value for this field is false, - meaning none of the master nodes are schedulable. - Important Note: Once the workload pods start running on the master nodes, - extreme care must be taken to ensure that cluster-critical control plane components - are not impacted. - Please turn on this field after doing due diligence. - type: boolean - policy: - description: |- - DEPRECATED: the scheduler Policy API has been deprecated and will be removed in a future release. - policy is a reference to a ConfigMap containing scheduler policy which has - user specified predicates and priorities. If this ConfigMap is not available - scheduler will default to use DefaultAlgorithmProvider. - The namespace for this configmap is openshift-config. - properties: - name: - description: name is the metadata.name of the referenced - config map - type: string - required: - - name - type: object - profile: - description: |- - profile sets which scheduling profile should be set in order to configure scheduling - decisions for new pods. - - Valid values are "LowNodeUtilization", "HighNodeUtilization", "NoScoring" - Defaults to "LowNodeUtilization" - enum: - - "" - - LowNodeUtilization - - HighNodeUtilization - - NoScoring - type: string - profileCustomizations: - description: |- - profileCustomizations contains configuration for modifying the default behavior of existing scheduler profiles. - Deprecated: no longer needed, since DRA is GA starting with 4.21, and - is enabled by' default in the cluster, this field will be removed in 4.24. - properties: - dynamicResourceAllocation: - description: |- - dynamicResourceAllocation allows to enable or disable dynamic resource allocation within the scheduler. - Dynamic resource allocation is an API for requesting and sharing resources between pods and containers inside a pod. - Third-party resource drivers are responsible for tracking and allocating resources. - Different kinds of resources support arbitrary parameters for defining requirements and initialization. - Valid values are Enabled, Disabled and omitted. - When omitted, this means no opinion and the platform is left to choose a reasonable default, - which is subject to change over time. - The current default is Disabled. - enum: - - "" - - Enabled - - Disabled - type: string - type: object + description: scheduler contains the configuration for scheduler. type: object type: object controlPlaneRelease: - description: |- - controlPlaneRelease is like spec.release but only for the components running on the management cluster. - This excludes any operand which will land in the hosted cluster data plane. - It is useful when you need to apply patch management side like a CVE, transparently for the hosted cluster. - Version input for this field is free, no validation is performed against spec.release or maximum and minimum is performed. - If defined, it will dicate the version of the components running management side, while spec.release will dictate the version of the components landing in the hosted cluster data plane. - If not defined, spec.release is used for both. - Changing this field will trigger a rollout of the control plane. - The behavior of the rollout will be driven by the ControllerAvailabilityPolicy and InfrastructureAvailabilityPolicy for PDBs and maxUnavailable and surce policies. + description: controlPlaneRelease is like spec.release but only + for the components running on the management cluster. properties: image: description: |- @@ -3238,18 +705,13 @@ spec: - image type: object controllerAvailabilityPolicy: - default: HighlyAvailable - description: |- - controllerAvailabilityPolicy specifies the availability policy applied to critical control plane components like the Kube API Server. - Possible values are HighlyAvailable and SingleReplica. The default value is HighlyAvailable. - This field is immutable. + description: controllerAvailabilityPolicy specifies the availability + policy applied to critical control plane components like the + Kube API Server. enum: - HighlyAvailable - SingleReplica type: string - x-kubernetes-validations: - - message: ControllerAvailabilityPolicy is immutable - rule: self == oldSelf dns: description: dns specifies the DNS configuration for the hosted cluster ingress. @@ -3322,17 +784,8 @@ spec: - baseDomain type: object etcd: - default: - managed: - storage: - persistentVolume: - size: 8Gi - type: PersistentVolume - managementType: Managed - description: |- - etcd specifies configuration for the control plane etcd cluster. The - default managementType is Managed. Once set, the managementType cannot be - changed. + description: etcd specifies configuration for the control plane + etcd cluster. The properties: managed: description: managed specifies the behavior of an etcd cluster @@ -3548,21 +1001,12 @@ spec: rule: 'self.managementType == ''Unmanaged'' ? has(self.unmanaged) : !has(self.unmanaged)' fips: - description: |- - fips indicates whether this cluster's nodes will be running in FIPS mode. - If set to true, the control plane's ignition server will be configured to - expect that nodes joining the cluster will be FIPS-enabled. + description: fips indicates whether this cluster's nodes will + be running in FIPS mode. type: boolean - x-kubernetes-validations: - - message: fips is immutable - rule: self == oldSelf imageContentSources: - description: |- - imageContentSources specifies image mirrors that can be used by cluster - nodes to pull content. - When imageContentSources is set, the controllers will generate a machineConfig. - This MachineConfig will be part of every payload generated by the controllers for any NodePool of the HostedCluster. - Changing this value will trigger a rollout for all existing NodePools in the cluster. + description: imageContentSources specifies image mirrors that + can be used by cluster items: description: |- ImageContentSource specifies image mirrors that can be used by cluster nodes @@ -3588,93 +1032,38 @@ spec: required: - source type: object - maxItems: 255 + maxItems: 50 type: array infraID: - description: |- - infraID is a globally unique identifier for the cluster. - It must consist of lowercase alphanumeric characters and hyphens ('-') only, and start and end with an alphanumeric character. - It must be no more than 253 characters in length. - This identifier will be used to associate various cloud resources with the HostedCluster and its associated NodePools. - infraID is used to compute and tag created resources with "kubernetes.io/cluster/"+hcluster.Spec.InfraID which has contractual meaning for the cloud provider implementations. - If a value is not specified, a random infraID will be generated and set by the controller. - Once set, this value is immutable. - maxLength: 253 - minLength: 1 + description: infraID is a globally unique identifier for the cluster. type: string - x-kubernetes-validations: - - message: infraID must consist of lowercase alphanumeric characters - or '-', start and end with an alphanumeric character, and - be between 1 and 253 characters - rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$') - - message: infraID is immutable - rule: oldSelf == "" || self == oldSelf infrastructureAvailabilityPolicy: - default: SingleReplica - description: |- - 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. - Possible values are HighlyAvailable and SingleReplica. The default value is SingleReplica. + description: 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. enum: - HighlyAvailable - SingleReplica type: string issuerURL: - default: https://kubernetes.default.svc - description: |- - issuerURL is an OIDC issuer URL which will be used as the issuer in all - ServiceAccount tokens generated by the control plane API server via --service-account-issuer kube api server flag. - https://k8s-docs.netlify.app/en/docs/reference/command-line-tools-reference/kube-apiserver/ - https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#serviceaccount-token-volume-projection - The default value is kubernetes.default.svc, which only works for in-cluster - validation. - If the platform is AWS and this value is set, the controller will update an s3 object with the appropriate OIDC documents (using the serviceAccountSigningKey info) into that issuerURL. - The expectation is for this s3 url to be backed by an OIDC provider in the AWS IAM. - maxLength: 255 + description: issuerURL is an OIDC issuer URL which will be used + as the issuer in all type: string - x-kubernetes-validations: - - message: issuerURL is immutable - rule: self == oldSelf - - message: issuerURL must be a valid absolute URL - rule: isURL(self) kubeAPIServerDNSName: - description: |- - kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. - When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. - If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. - The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. - This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable - access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates - for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. - This API endpoint only works in OCP version 4.19 or later. Older versions will result in a no-op. - maxLength: 253 + description: kubeAPIServerDNSName specifies a desired DNS name + to resolve to the KAS. type: string - x-kubernetes-validations: - - message: kubeAPIServerDNSName must be a valid URL name (e.g., - api.example.com) - rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') labels: additionalProperties: type: string - description: |- - labels when specified, define what custom labels are added to the hcp pods. - Changing this day 2 will cause a rollout of all hcp pods. - Duplicate keys are not supported. If duplicate keys are defined, only the last key/value pair is preserved. - Valid values are those in https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set - - -kubebuilder:validation:XValidation:rule=`self.all(key, size(key) <= 317 && key.matches('^(([A-Za-z0-9]+(\\.[A-Za-z0-9]+)?)*[A-Za-z0-9]\\/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$'))`, message="label key must have two segments: an optional prefix and name, separated by a slash (/). The name segment is required and must be 63 characters or less, beginning and ending with an alphanumeric character ([a-z0-9A-Z]) with dashes (-), underscores (_), dots (.), and alphanumerics between. The prefix is optional. If specified, the prefix must be a DNS subdomain: a series of DNS labels separated by dots (.), not longer than 253 characters in total, followed by a slash (/)" - -kubebuilder:validation:XValidation:rule=`self.all(key, size(self[key]) <= 63 && self[key].matches('^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$'))`, message="label value must be 63 characters or less (can be empty), consist of alphanumeric characters, dashes (-), underscores (_) or dots (.), and begin and end with an alphanumeric character" - maxProperties: 20 + description: labels when specified, define what custom labels + are added to the hcp pods. + maxProperties: 100 type: object networking: - default: - clusterNetwork: - - cidr: 10.132.0.0/14 - networkType: OVNKubernetes - serviceNetwork: - - cidr: 172.31.0.0/16 - description: |- - networking specifies network configuration for the hosted cluster. - Defaults to OVNKubernetes with a cluster network of cidr: "10.132.0.0/14" and a service network of cidr: "172.31.0.0/16". + description: networking specifies network configuration for the + hosted cluster. properties: allocateNodeCIDRs: description: |- @@ -3874,28 +1263,18 @@ spec: nodeSelector: additionalProperties: type: string - description: |- - nodeSelector when specified, is propagated to all control plane Deployments and Stateful sets running management side. - It must be satisfied by the management Nodes for the pods to be scheduled. Otherwise the HostedCluster will enter a degraded state. - Changes to this field will propagate to existing Deployments and StatefulSets. + description: nodeSelector when specified, is propagated to all + control plane Deployments and Stateful sets running management + side. + maxProperties: 100 type: object - x-kubernetes-validations: - - message: nodeSelector map can have at most 20 entries - rule: size(self) <= 20 olmCatalogPlacement: - default: management - description: |- - olmCatalogPlacement specifies the placement of OLM catalog components. By default, - this is set to management and OLM catalog components are deployed onto the management - cluster. If set to guest, the OLM catalog components will be deployed onto the guest - cluster. + description: olmCatalogPlacement specifies the placement of OLM + catalog components. By default, enum: - management - guest type: string - x-kubernetes-validations: - - message: OLMCatalogPlacement is immutable - rule: self == oldSelf operatorConfiguration: description: operatorConfiguration specifies configuration for individual OCP operators in the cluster. @@ -4826,23 +2205,13 @@ spec: type: object 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. - Either a date can be provided in RFC3339 format or a boolean as in 'true', 'false', 'True', 'False'. If a date is - provided: reconciliation is paused on the resource until that date. If the boolean true is - provided: reconciliation is paused on the resource until the field is removed. - maxLength: 35 - minLength: 4 + 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 - x-kubernetes-validations: - - message: PausedUntil must be a date in RFC3339 format or 'True', - 'true', 'False' or 'false' - rule: self.matches('^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.*$') - || self in ['true', 'false', 'True', 'False'] platform: - description: |- - platform specifies the underlying infrastructure provider for the cluster - and is used to configure platform specific behavior. + description: platform specifies the underlying infrastructure + provider for the cluster properties: agent: description: agent specifies configuration for agent-based @@ -7828,14 +5197,9 @@ spec: - type type: object pullSecret: - description: |- - pullSecret is a local reference to a Secret that must have a ".dockerconfigjson" key whose content must be a valid Openshift pull secret JSON. - If the reference is set but none of the above requirements are met, the HostedCluster will enter a degraded state. - This pull secret is included in NodePool ignition/bootstrap payloads and applied to the container runtime when nodes provision. - Changing this value will trigger a rollout for all existing NodePools in the cluster (for both replace and inplace upgrade types). - Updating the referenced Secret's data in place (without changing this reference) does not trigger that rollout. - In AWS and Azure NodePools using the Replace upgrade strategy, the Secret's data in place changes - will still propagate the updated credentials down to the guest cluster and kubelet config. + description: pullSecret is a local reference to a Secret that + must have a ".dockerconfigjson" key whose content must be a + valid Openshift pull secret JSON. properties: name: default: "" @@ -7849,14 +5213,8 @@ spec: type: object x-kubernetes-map-type: atomic release: - description: |- - release specifies the desired OCP release payload for all the hosted cluster components. - This includes those components running management side like the Kube API Server and the CVO but also the operands which land in the hosted cluster data plane like the ingress controller, ovn agents, etc. - The maximum and minimum supported release versions are determined by the running Hypersfhit Operator. - Attempting to use an unsupported version will result in the HostedCluster being degraded and the validateReleaseImage condition being false. - Attempting to use a release with a skew against a NodePool release bigger than N-2 for the y-stream will result in leaving the NodePool in an unsupported state. - Changing this field will trigger a rollout of the control plane components. - The behavior of the rollout will be driven by the ControllerAvailabilityPolicy and InfrastructureAvailabilityPolicy for PDBs and maxUnavailable and surce policies. + description: release specifies the desired OCP release payload + for all the hosted cluster components. properties: image: description: |- @@ -7873,9 +5231,8 @@ spec: - image type: object secretEncryption: - description: |- - secretEncryption specifies a Kubernetes secret encryption strategy for the - control plane. + description: secretEncryption specifies a Kubernetes secret encryption + strategy for the properties: aescbc: description: aescbc defines metadata about the AESCBC secret @@ -8285,17 +5642,9 @@ spec: - type type: object serviceAccountSigningKey: - description: |- - serviceAccountSigningKey is a local reference to a secret that must have a "key" key whose content must be the private key - used by the service account token issuer. - If not specified, a service account signing key will - be generated automatically for the cluster. - When specifying a service account signing key, an IssuerURL must also be specified. - If the reference is set but none of the above requirements are met, the HostedCluster will enter a degraded state. - - For key rotation, the secret may optionally contain an "old-key.pub" key whose content is the PEM-encoded - public key of the previous signing key. When present, the kube-apiserver will accept tokens signed by - both the current and previous keys, allowing for graceful key rotation without invalidating existing tokens. + description: serviceAccountSigningKey is a local reference to + a secret that must have a "key" key whose content must be the + private key properties: name: default: "" @@ -8309,16 +5658,8 @@ spec: type: object x-kubernetes-map-type: atomic services: - description: |- - services specifies how individual control plane services endpoints are published for consumption. - This requires APIServer;OAuthServer;Konnectivity;Ignition. - This field is immutable for all platforms but IBMCloud. - Max is 6 to account for OIDC;OVNSbDb for backward compatibility though they are no-op. - - -kubebuilder:validation:XValidation:rule="self.all(s, !(s.service == 'APIServer' && s.servicePublishingStrategy.type == 'Route') || has(s.servicePublishingStrategy.route.hostname))",message="If serviceType is 'APIServer' and publishing strategy is 'Route', then hostname must be set" - -kubebuilder:validation:XValidation:rule="self.platform.type == 'IBMCloud' ? ['APIServer', 'OAuthServer', 'Konnectivity'].all(requiredType, self.exists(s, s.service == requiredType))",message="Services list must contain at least 'APIServer', 'OAuthServer', and 'Konnectivity' service types" : ['APIServer', 'OAuthServer', 'Konnectivity', 'Ignition'].all(requiredType, self.exists(s, s.service == requiredType))",message="Services list must contain at least 'APIServer', 'OAuthServer', 'Konnectivity', and 'Ignition' service types" - -kubebuilder:validation:XValidation:rule="self.filter(s, s.servicePublishingStrategy.type == 'Route' && has(s.servicePublishingStrategy.route) && has(s.servicePublishingStrategy.route.hostname)).all(x, self.filter(y, y.servicePublishingStrategy.type == 'Route' && (has(y.servicePublishingStrategy.route) && has(y.servicePublishingStrategy.route.hostname) && y.servicePublishingStrategy.route.hostname == x.servicePublishingStrategy.route.hostname)).size() <= 1)",message="Each route publishingStrategy 'hostname' must be unique within the Services list." - -kubebuilder:validation:XValidation:rule="self.filter(s, s.servicePublishingStrategy.type == 'NodePort' && has(s.servicePublishingStrategy.nodePort) && has(s.servicePublishingStrategy.nodePort.address) && has(s.servicePublishingStrategy.nodePort.port)).all(x, self.filter(y, y.servicePublishingStrategy.type == 'NodePort' && (has(y.servicePublishingStrategy.nodePort) && has(y.servicePublishingStrategy.nodePort.address) && y.servicePublishingStrategy.nodePort.address == x.servicePublishingStrategy.nodePort.address && has(y.servicePublishingStrategy.nodePort.port) && y.servicePublishingStrategy.nodePort.port == x.servicePublishingStrategy.nodePort.port )).size() <= 1)",message="Each nodePort publishingStrategy 'nodePort' and 'hostname' must be unique within the Services list." + description: services specifies how individual control plane services + endpoints are published for consumption. items: description: |- ServicePublishingStrategyMapping specifies how individual control plane services endpoints are published for consumption. @@ -8440,15 +5781,12 @@ spec: - service - servicePublishingStrategy type: object - maxItems: 6 + maxItems: 10 type: array sshKey: - description: |- - 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. - If the reference is set but none of the above requirements are met, the HostedCluster will enter a degraded state. - When sshKey is set, the controllers will generate a machineConfig with the sshAuthorizedKeys https://coreos.github.io/ignition/configuration-v3_2/ populated with this value. - This MachineConfig will be part of every payload generated by the controllers for any NodePool of the HostedCluster. - Changing this value will trigger a rollout for all existing NodePools in the cluster. + description: 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. properties: name: default: "" @@ -8501,77 +5839,38 @@ spec: If the operator is Exists, the value should be empty, otherwise just a regular string. type: string type: object - maxItems: 25 + maxItems: 50 type: array updateService: - description: |- - updateService may be used to specify the preferred upstream update service. - If omitted we will use the appropriate update service for the cluster and region. - This is used by the control plane operator to determine and signal the appropriate available upgrades in the hostedCluster.status. + description: updateService may be used to specify the preferred + upstream update service. type: string - x-kubernetes-validations: - - message: updateService must be a valid absolute URL - rule: isURL(self) required: - etcd + - fips - networking - platform - pullSecret - release - services + - sshKey + type: object + internalId: + description: InternalID is a platform-assigned unique identifier. + type: string + properties: + additionalProperties: + type: string + description: Properties are arbitrary key-value pairs for customer + metadata. + maxProperties: 100 + type: object + tags: + additionalProperties: + type: string + description: Tags are customer-defined labels for organizational purposes. + maxProperties: 100 type: object - x-kubernetes-validations: - - message: spec.services in body should have at least 4 items or 3 - for IBMCloud - rule: 'self.platform.type == ''IBMCloud'' ? size(self.services) - >= 3 : size(self.services) >= 4' - - message: Services is immutable. Changes might result in unpredictable - and disruptive behavior. - rule: 'self.platform.type != "IBMCloud" ? self.services == oldSelf.services - : true' - - message: Azure managed platform (ARO HCP) requires OAuthServer to - use Route - rule: self.platform.type != "Azure" || self.platform.?azure.azureAuthenticationConfig.azureAuthenticationConfigType.orValue("") - == "WorkloadIdentities" || self.services.exists(s, s.service == - "OAuthServer" && s.servicePublishingStrategy.type == "Route") - - message: Self-managed Azure requires OAuthServer to use Route or - LoadBalancer - rule: self.platform.type != "Azure" || self.platform.?azure.azureAuthenticationConfig.azureAuthenticationConfigType.orValue("") - != "WorkloadIdentities" || self.services.exists(s, s.service == - "OAuthServer" && (s.servicePublishingStrategy.type == "Route" - || s.servicePublishingStrategy.type == "LoadBalancer")) - - message: Azure platform requires Konnectivity to use Route service - publishing strategy - rule: 'self.platform.type == "Azure" ? self.services.exists(s, s.service - == "Konnectivity" && s.servicePublishingStrategy.type == "Route") - : true' - - message: Azure platform requires Ignition to use Route service publishing - strategy - rule: 'self.platform.type == "Azure" ? self.services.exists(s, s.service - == "Ignition" && s.servicePublishingStrategy.type == "Route") - : true' - - message: If serviceAccountSigningKey is set, issuerURL must be set - rule: has(self.issuerURL) || !has(self.serviceAccountSigningKey) - - message: APIServer loadBalancer hostname cannot be in ClusterConfiguration.apiserver.servingCerts.namedCertificates[] - rule: '!self.services.exists(s, s.service == ''APIServer'' && has(s.servicePublishingStrategy.loadBalancer) - && s.servicePublishingStrategy.loadBalancer.hostname != "" && - has(self.configuration) && has(self.configuration.apiServer) && - has(self.configuration.apiServer.servingCerts) && has(self.configuration.apiServer.servingCerts.namedCertificates) - && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert, - has(cert.names) && cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))' - - message: disableMultiNetwork can only be set to true when networkType - is 'Other' - rule: '!has(self.operatorConfiguration) || !has(self.operatorConfiguration.clusterNetworkOperator) - || !has(self.operatorConfiguration.clusterNetworkOperator.disableMultiNetwork) - || !self.operatorConfiguration.clusterNetworkOperator.disableMultiNetwork - || self.networking.networkType == ''Other''' - - message: ovnKubernetesConfig is forbidden when networkType is not - OVNKubernetes - rule: self.networking.networkType == 'OVNKubernetes' || !has(self.operatorConfiguration) - || !has(self.operatorConfiguration.clusterNetworkOperator) || - !has(self.operatorConfiguration.clusterNetworkOperator.ovnKubernetesConfig) - - message: secretEncryption cannot be removed once configured - rule: '!has(oldSelf.secretEncryption) || has(self.secretEncryption)' 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..660b6505 100644 --- a/hyperfleet-operator/config/crd/bases/hyperfleet.io_nodepools.yaml +++ b/hyperfleet-operator/config/crd/bases/hyperfleet.io_nodepools.yaml @@ -53,30 +53,40 @@ 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 that owns this node pool. + type: string + autoRepair: + description: AutoRepair controls whether unhealthy nodes are automatically + replaced. + type: boolean + displayName: + description: DisplayName is a human-readable name for the node pool. + maxLength: 256 + type: string + internalPoolId: + description: InternalPoolID is a platform-assigned unique identifier. + type: string + labels: + additionalProperties: + type: string + description: Labels are customer-defined labels applied to nodes in + this pool. + maxProperties: 100 + type: object nodePool: description: |- - 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. + NodePool contains the upstream HyperShift fields, mirrored as + passthrough types with per-field visibility and write-mode markers. properties: arch: - default: amd64 - description: "arch is the preferred processor architecture for + description: arch is the preferred processor architecture for the NodePool. Different platforms might have different supported - architectures.\n\thttps://github.com/kubernetes/kubernetes/issues/108768#issuecomment-1253912215" - enum: - - arm64 - - amd64 - - ppc64le - - s390x + architectures. type: string - x-kubernetes-validations: - - message: Arch is immutable - rule: self == oldSelf autoScaling: - description: |- - autoScaling specifies auto-scaling behavior for the NodePool. - autoScaling is mutually exclusive with replicas. If replicas is set, this field must be omitted. + description: autoScaling specifies auto-scaling behavior for the + NodePool. properties: max: description: max is the maximum number of nodes allowed in @@ -100,39 +110,12 @@ spec: - message: max must be equal or greater than min rule: self.max >= self.min clusterName: - description: |- - clusterName is the name of the HostedCluster this NodePool belongs to. - If a HostedCluster with this name doesn't exist, the controller will no-op until it exists. - maxLength: 253 - minLength: 1 + description: clusterName is the name of the HostedCluster this + NodePool belongs to. type: string - x-kubernetes-validations: - - message: ClusterName is immutable - rule: self == oldSelf - - message: clusterName must consist of lowercase alphanumeric - characters or '-', start and end with an alphanumeric character, - and be between 1 and 253 characters - rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$') config: - description: |- - config is a list of references to ConfigMaps containing serialized - MachineConfig resources to be injected into the ignition configurations of - nodes in the NodePool. The MachineConfig API schema is defined here: - - https://github.com/openshift/machine-config-operator/blob/18963e4f8fe66e8c513ca4b131620760a414997f/pkg/apis/machineconfiguration.openshift.io/v1/types.go#L185 - - Each ConfigMap must have a single key named "config" whose value is the YML - with one or more serialized machineconfiguration.openshift.io resources: - - * KubeletConfig - * ContainerRuntimeConfig - * MachineConfig - * ClusterImagePolicy - * ImageContentSourcePolicy - * ImageDigestMirrorSet - - This is validated in the backend and signaled back via validMachineConfig condition. - Changing this field will trigger a NodePool rollout. + description: config is a list of references to ConfigMaps containing + serialized items: description: |- LocalObjectReference contains enough information to let you locate the @@ -149,12 +132,10 @@ spec: type: string type: object x-kubernetes-map-type: atomic - maxItems: 10 type: array management: - description: |- - management specifies behavior for managing nodes in the pool, such as - upgrade strategies and auto-repair behaviors. + description: management specifies behavior for managing nodes + in the pool, such as properties: autoRepair: default: false @@ -287,42 +268,25 @@ spec: is 'InPlace' rule: '!has(self.inPlace) || self.upgradeType == ''InPlace''' nodeDrainTimeout: - description: |- - nodeDrainTimeout is the maximum amount of time that the controller will spend on retrying to drain a node until it succeeds. - The default value is 0, meaning that the node can retry drain without any time limitations. - Changing this field propagate inplace into existing Nodes. + description: nodeDrainTimeout is the maximum amount of time that + the controller will spend on retrying to drain a node until + it succeeds. type: string nodeLabels: additionalProperties: type: string - description: |- - nodeLabels propagates a list of labels to Nodes, only once on creation. - Valid values are those in https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set + description: nodeLabels propagates a list of labels to Nodes, + only once on creation. + maxProperties: 100 type: object nodeVolumeDetachTimeout: - description: |- - nodeVolumeDetachTimeout is the maximum amount of time that the controller will spend on detaching volumes from a node. - The default value is 0, meaning that the volumes will be detached from the node without any time limitations. - After the timeout, any remaining attached volumes will be ignored and the removal of the machine will continue. - Changing this field propagate inplace into existing Nodes. + description: nodeVolumeDetachTimeout is the maximum amount of + time that the controller will spend on detaching volumes from + a node. type: string osImageStream: - description: |- - osImageStream specifies an OS stream to be used for nodes in this pool. - - This field can be optionally set to a known OSImageStream name to change - the OS and Extension images with a well-known, tested, release-provided - set of images. This enables a streamlined way of switching the pool's - node OS to a different version than the cluster default, such as - transitioning to a major RHEL version. - - When set, the referenced stream overrides the default OS images for the - pool. When omitted, the pool uses the release version's default stream - (rhel-9 for OCP < 5.0, rhel-10 for OCP >= 5.0). - Changing this field triggers a rollout. Forward transitions - (rhel-9 -> rhel-10) are allowed; backward transitions - (rhel-10 -> rhel-9) are rejected by CEL validation because - in-place OS downgrades are not supported. + description: osImageStream specifies an OS stream to be used for + nodes in this pool. properties: name: description: name is a required reference to an OSImageStream @@ -334,29 +298,14 @@ spec: required: - name type: object - x-kubernetes-validations: - - message: OS stream downgrade from rhel-10 to rhel-9 is not allowed; - create a new NodePool instead - rule: '!has(oldSelf.name) || oldSelf.name != ''rhel-10'' || - (has(self.name) && self.name != ''rhel-9'')' pausedUntil: - description: |- - pausedUntil is a field that can be used to pause reconciliation on the NodePool controller. Resulting in any change to the NodePool being ignored. - Either a date can be provided in RFC3339 format or a boolean as in 'true', 'false', 'True', 'False'. If a date is - provided: reconciliation is paused on the resource until that date. If the boolean true is - provided: reconciliation is paused on the resource until the field is removed. - maxLength: 35 - minLength: 1 + description: pausedUntil is a field that can be used to pause + reconciliation on the NodePool controller. Resulting in any + change to the NodePool being ignored. type: string - x-kubernetes-validations: - - message: PausedUntil must be a date in RFC3339 format or 'True', - 'true', 'False' or 'false' - rule: self.matches('^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.*$') - || self in ['true', 'false', 'True', 'False'] platform: - description: |- - platform specifies the underlying infrastructure provider for the NodePool - and is used to configure platform specific behavior. + description: platform specifies the underlying infrastructure + provider for the NodePool properties: agent: description: agent specifies the configuration used when using @@ -1846,20 +1795,8 @@ spec: - type type: object release: - description: |- - release specifies the OCP release used for this NodePool. It drives the machine ignition configuration (including - the kubelet version) and other platform-specific properties (e.g. an AMI on AWS). - - Version-skew rules and effects: - - The minor-version skew relative to the control-plane release must be <= N-2. - This is not currently enforced, but exceeding this limit is unsupported and - may lead to unpredictable behavior. - - If the specified release is higher than the HostedCluster's release, the - NodePool will be degraded and the ValidReleaseImage condition will be false. - - If the specified release is lower than the NodePool's current y-stream, - the NodePool will be degraded and the ValidReleaseImage condition will be false. - - Changing this field triggers a NodePool rollout. + description: release specifies the OCP release used for this NodePool. + It drives the machine ignition configuration (including properties: image: description: |- @@ -1876,15 +1813,13 @@ spec: - image type: object replicas: - description: |- - replicas is the desired number of nodes the pool should maintain. If unset, the controller default value is 0. - replicas is mutually exclusive with autoscaling. If autoscaling is configured, replicas must be omitted and autoscaling will control the NodePool size internally. + description: replicas is the desired number of nodes the pool + should maintain. If unset, the controller default value is 0. format: int32 type: integer taints: - description: |- - taints if specified, propagates a list of taints to Nodes, only once on creation. - These taints are additive to the ones applied by other controllers + description: taints if specified, propagates a list of taints + to Nodes, only once on creation. items: description: |- taint is as v1 Core but without TimeAdded. @@ -1927,19 +1862,8 @@ spec: maxItems: 50 type: array tuningConfig: - description: |- - tuningConfig is a list of references to ConfigMaps containing serialized - Tuned or PerformanceProfile resources to define the tuning configuration to be applied to - nodes in the NodePool. The Tuned API is defined here: - - https://github.com/openshift/cluster-node-tuning-operator/blob/2c76314fb3cc8f12aef4a0dcd67ddc3677d5b54f/pkg/apis/tuned/v1/tuned_types.go - - The PerformanceProfile API is defined here: - https://github.com/openshift/cluster-node-tuning-operator/tree/b41042d42d4ba5bb2e99960248cf1d6ae4935018/pkg/apis/performanceprofile/v2 - - Each ConfigMap must have a single key named "tuning" whose value is the - JSON or YAML of a serialized Tuned or PerformanceProfile. - Changing this field will trigger a NodePool rollout. + description: tuningConfig is a list of references to ConfigMaps + containing serialized items: description: |- LocalObjectReference contains enough information to let you locate the @@ -1956,7 +1880,6 @@ spec: type: string type: object x-kubernetes-map-type: atomic - maxItems: 10 type: array required: - clusterName @@ -1964,26 +1887,6 @@ spec: - platform - release type: object - x-kubernetes-validations: - - message: Arch is required once set - rule: '!has(oldSelf.arch) || has(self.arch)' - - message: Setting Arch to arm64 is only supported for AWS, Azure, - Agent, GCP and None - rule: self.arch != 'arm64' || has(self.platform.aws) || has(self.platform.azure) - || has(self.platform.agent) || self.platform.type == 'GCP' || - self.platform.type == 'None' - - message: Both replicas or autoScaling should not be set - rule: '!has(self.replicas) || !has(self.autoScaling)' - - message: s390x is only supported on KubeVirt platform - rule: self.arch != 's390x' || has(self.platform.kubevirt) - - message: ImageType 'Windows' requires arch 'amd64' (AWS only) - rule: '!has(self.platform.aws) || !has(self.platform.aws.imageType) - || self.platform.aws.imageType != ''Windows'' || self.arch == - ''amd64''' - - message: Scale-from-zero (autoScaling.min=0) is currently only supported - for AWS platform - rule: '!has(self.autoScaling) || self.autoScaling.min > 0 || self.platform.type - == ''AWS''' required: - nodePool type: object diff --git a/hyperfleet-operator/go.mod b/hyperfleet-operator/go.mod index 19060ce4..196806ff 100644 --- a/hyperfleet-operator/go.mod +++ b/hyperfleet-operator/go.mod @@ -3,8 +3,8 @@ module github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator go 1.26.3 replace ( + github.com/openshift-online/rosa-hyperfleet-api/api => ../api github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-db => ../hyperfleet-db - github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api => ./api ) require ( @@ -18,8 +18,8 @@ require ( github.com/jackc/pgx/v5 v5.10.0 github.com/onsi/ginkgo/v2 v2.27.4 github.com/onsi/gomega v1.42.1 + github.com/openshift-online/rosa-hyperfleet-api/api 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/api v0.0.0-20260416105050-3c6b218b8a80 github.com/openshift/hypershift/api v0.0.0-20260625052409-9acec4759a16 github.com/prometheus/client_golang v1.23.2 diff --git a/hyperfleet-operator/internal/controller/cluster_controller.go b/hyperfleet-operator/internal/controller/cluster_controller.go index 3aa3a9fc..05ba5b94 100644 --- a/hyperfleet-operator/internal/controller/cluster_controller.go +++ b/hyperfleet-operator/internal/controller/cluster_controller.go @@ -39,7 +39,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/dynamo" "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/render" hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" diff --git a/hyperfleet-operator/internal/controller/cluster_controller_test.go b/hyperfleet-operator/internal/controller/cluster_controller_test.go index a52077b4..3a315f71 100644 --- a/hyperfleet-operator/internal/controller/cluster_controller_test.go +++ b/hyperfleet-operator/internal/controller/cluster_controller_test.go @@ -32,7 +32,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/dynamo" "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/render" hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" @@ -507,7 +507,7 @@ func newTestCluster(name string) *hyperfleetv1alpha1.Cluster { }, Spec: hyperfleetv1alpha1.ClusterSpec{ CreatorARN: "arn:aws:iam::123456789012:user/admin", - HostedCluster: hypershiftv1beta1.HostedClusterSpec{ + HostedCluster: hyperfleetv1alpha1.HostedClusterSpecPassthrough{ Release: hypershiftv1beta1.Release{Image: "quay.io/openshift-release-dev/ocp-release:4.17.0-ec.2-x86_64"}, IssuerURL: "https://oidc.example.com/cluster-01", PullSecret: corev1.LocalObjectReference{Name: "pull-secret"}, diff --git a/hyperfleet-operator/internal/controller/manifest_controller.go b/hyperfleet-operator/internal/controller/manifest_controller.go index 414613f8..bd905ada 100644 --- a/hyperfleet-operator/internal/controller/manifest_controller.go +++ b/hyperfleet-operator/internal/controller/manifest_controller.go @@ -38,7 +38,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/source" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/dynamo" ) diff --git a/hyperfleet-operator/internal/controller/manifest_controller_test.go b/hyperfleet-operator/internal/controller/manifest_controller_test.go index 710d6733..e921e1c6 100644 --- a/hyperfleet-operator/internal/controller/manifest_controller_test.go +++ b/hyperfleet-operator/internal/controller/manifest_controller_test.go @@ -30,7 +30,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/dynamo" ) diff --git a/hyperfleet-operator/internal/controller/nodepool_controller.go b/hyperfleet-operator/internal/controller/nodepool_controller.go index ecf8008d..0c2c4a48 100644 --- a/hyperfleet-operator/internal/controller/nodepool_controller.go +++ b/hyperfleet-operator/internal/controller/nodepool_controller.go @@ -37,7 +37,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/source" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/dynamo" "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/render" ) @@ -109,7 +109,10 @@ func (r *NodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c statusPrefix := dynamo.StatusPrefix(mc) // Generate NodePool resource and create ApplyDesire. - m := render.NodePoolResource(&nodePool, &cluster) + m, err := render.NodePoolResource(&nodePool, &cluster) + if err != nil { + return ctrl.Result{}, fmt.Errorf("render nodepool resource: %w", err) + } ns := m.Namespace docID := dynamo.NewDocumentID(taskKey, m.Group, m.Version, m.Resource, ns, m.Name) @@ -213,7 +216,10 @@ func (r *NodePoolReconciler) reconcileDelete(ctx context.Context, nodePool *hype specsPrefix := dynamo.SpecsPrefix(mc) statusPrefix := dynamo.StatusPrefix(mc) - m := render.NodePoolResource(nodePool, cluster) + m, err := render.NodePoolResource(nodePool, cluster) + if err != nil { + return ctrl.Result{}, fmt.Errorf("render nodepool resource for deletion: %w", err) + } ns := m.Namespace // Switch the ApplyDesire to Type=Delete in-place, using the same diff --git a/hyperfleet-operator/internal/controller/nodepool_controller_test.go b/hyperfleet-operator/internal/controller/nodepool_controller_test.go index a2b2b035..f50b6469 100644 --- a/hyperfleet-operator/internal/controller/nodepool_controller_test.go +++ b/hyperfleet-operator/internal/controller/nodepool_controller_test.go @@ -29,7 +29,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/dynamo" hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" ) @@ -361,7 +361,7 @@ func newTestNodePool() *hyperfleetv1alpha1.NodePool { Namespace: "cluster-test-cluster-id", }, Spec: hyperfleetv1alpha1.NodePoolSpec{ - NodePool: hypershiftv1beta1.NodePoolSpec{ + NodePool: hyperfleetv1alpha1.NodePoolSpecPassthrough{ ClusterName: "test-np-cluster", Replicas: ptr.To(int32(2)), Management: hypershiftv1beta1.NodePoolManagement{ diff --git a/hyperfleet-operator/internal/controller/placement_controller.go b/hyperfleet-operator/internal/controller/placement_controller.go index a316fbb0..f8a09d84 100644 --- a/hyperfleet-operator/internal/controller/placement_controller.go +++ b/hyperfleet-operator/internal/controller/placement_controller.go @@ -34,7 +34,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" ) // PlacementReconciler watches Cluster CRs and ensures each has a Placement. diff --git a/hyperfleet-operator/internal/controller/placement_controller_test.go b/hyperfleet-operator/internal/controller/placement_controller_test.go index 2da4cee0..a1150d8e 100644 --- a/hyperfleet-operator/internal/controller/placement_controller_test.go +++ b/hyperfleet-operator/internal/controller/placement_controller_test.go @@ -26,7 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" ) var _ = Describe("Placement Controller", func() { diff --git a/hyperfleet-operator/internal/controller/suite_test.go b/hyperfleet-operator/internal/controller/suite_test.go index d26f75cf..916b38e7 100644 --- a/hyperfleet-operator/internal/controller/suite_test.go +++ b/hyperfleet-operator/internal/controller/suite_test.go @@ -36,7 +36,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" - hyperfleetiov1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetiov1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" // +kubebuilder:scaffold:imports ) diff --git a/hyperfleet-operator/internal/dynamo/statusstream/manager.go b/hyperfleet-operator/internal/dynamo/statusstream/manager.go index fa5cd53b..3b298446 100644 --- a/hyperfleet-operator/internal/dynamo/statusstream/manager.go +++ b/hyperfleet-operator/internal/dynamo/statusstream/manager.go @@ -10,7 +10,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodbstreams" "sigs.k8s.io/controller-runtime/pkg/client" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" ) type watcherHandle struct { diff --git a/hyperfleet-operator/internal/render/cluster.go b/hyperfleet-operator/internal/render/cluster.go index 44fa4b57..17fddfe3 100644 --- a/hyperfleet-operator/internal/render/cluster.go +++ b/hyperfleet-operator/internal/render/cluster.go @@ -11,7 +11,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" ) // ClusterResources generates the 7 Kubernetes resources for a cluster on the MC. @@ -23,7 +23,10 @@ func ClusterResources(cluster *hyperfleetv1alpha1.Cluster, rcfg RegionalConfig) // Zone shard 0 is hardcoded; will be dynamically assigned per-cluster in a future phase. zoneDomain := fmt.Sprintf("0.%s", rcfg.BaseDomain) - hc := hostedCluster(cluster, h4, zoneDomain) + hc, err := hostedCluster(cluster, h4, zoneDomain) + if err != nil { + return nil, err + } return []Resource{ namespace(clusterID, ns), @@ -175,14 +178,17 @@ func apiServingCert(clusterID, clusterName, h4, baseDomain, ns string) Resource } } -func hostedCluster(cluster *hyperfleetv1alpha1.Cluster, h4, zoneDomain string) Resource { +func hostedCluster(cluster *hyperfleetv1alpha1.Cluster, h4, zoneDomain string) (Resource, error) { clusterID := ClusterIDFromNamespace(cluster.Namespace) clusterName := cluster.Name // human-readable ns := cluster.Namespace // already "cluster-" baseDomain := zoneDomain apiHost := fmt.Sprintf("api.%s.%s.%s", clusterName, h4, baseDomain) - hcSpec := cluster.Spec.HostedCluster.DeepCopy() + hcSpec, err := toHostedClusterSpec(&cluster.Spec.HostedCluster) + if err != nil { + return Resource{}, fmt.Errorf("converting HostedClusterSpec for cluster %s/%s: %w", ns, clusterName, err) + } // --- Platform-managed overrides (always set by the operator) --- hcSpec.InfraID = clusterID @@ -193,7 +199,11 @@ func hostedCluster(cluster *hyperfleetv1alpha1.Cluster, h4, zoneDomain string) R hcSpec.SSHKey = corev1.LocalObjectReference{Name: "ssh-key"} hcSpec.KubeAPIServerDNSName = apiHost hcSpec.Services = servicePublishingStrategies(clusterName, h4, baseDomain) - hcSpec.Configuration = apiServerConfiguration() + if hcSpec.Configuration == nil { + hcSpec.Configuration = apiServerConfiguration() + } else { + hcSpec.Configuration.APIServer = apiServerConfiguration().APIServer + } // --- Defaults (only set if customer didn't specify) --- if hcSpec.Etcd.ManagementType == "" { @@ -253,7 +263,7 @@ func hostedCluster(cluster *hyperfleetv1alpha1.Cluster, h4, zoneDomain string) R }, Spec: *hcSpec, }, - } + }, nil } func servicePublishingStrategies(clusterName, h4, baseDomain string) []hypershiftv1beta1.ServicePublishingStrategyMapping { diff --git a/hyperfleet-operator/internal/render/cluster_test.go b/hyperfleet-operator/internal/render/cluster_test.go index df102cb1..e3b13a24 100644 --- a/hyperfleet-operator/internal/render/cluster_test.go +++ b/hyperfleet-operator/internal/render/cluster_test.go @@ -3,7 +3,7 @@ package render import ( "testing" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -18,7 +18,7 @@ func testCluster() *hyperfleetv1alpha1.Cluster { }, Spec: hyperfleetv1alpha1.ClusterSpec{ CreatorARN: "arn:aws:iam::123456789012:user/admin", - HostedCluster: hypershiftv1beta1.HostedClusterSpec{ + HostedCluster: hyperfleetv1alpha1.HostedClusterSpecPassthrough{ Release: hypershiftv1beta1.Release{Image: "quay.io/ocp:4.17"}, IssuerURL: "https://oidc.example.com/abc12345", Networking: hypershiftv1beta1.ClusterNetworking{ diff --git a/hyperfleet-operator/internal/render/convert.go b/hyperfleet-operator/internal/render/convert.go new file mode 100644 index 00000000..4aa18362 --- /dev/null +++ b/hyperfleet-operator/internal/render/convert.go @@ -0,0 +1,34 @@ +package render + +import ( + "encoding/json" + "fmt" + + hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" +) + +func toHostedClusterSpec(p *hyperfleetv1alpha1.HostedClusterSpecPassthrough) (*hypershiftv1beta1.HostedClusterSpec, error) { + data, err := json.Marshal(p) + if err != nil { + return nil, fmt.Errorf("marshaling HostedClusterSpecPassthrough: %w", err) + } + var spec hypershiftv1beta1.HostedClusterSpec + if err := json.Unmarshal(data, &spec); err != nil { + return nil, fmt.Errorf("unmarshalling to HostedClusterSpec: %w", err) + } + return &spec, nil +} + +func toNodePoolSpec(p *hyperfleetv1alpha1.NodePoolSpecPassthrough) (*hypershiftv1beta1.NodePoolSpec, error) { + data, err := json.Marshal(p) + if err != nil { + return nil, fmt.Errorf("marshaling NodePoolSpecPassthrough: %w", err) + } + var spec hypershiftv1beta1.NodePoolSpec + if err := json.Unmarshal(data, &spec); err != nil { + return nil, fmt.Errorf("unmarshalling to NodePoolSpec: %w", err) + } + return &spec, nil +} diff --git a/hyperfleet-operator/internal/render/nodepool.go b/hyperfleet-operator/internal/render/nodepool.go index 9eaf6e36..6325d9d1 100644 --- a/hyperfleet-operator/internal/render/nodepool.go +++ b/hyperfleet-operator/internal/render/nodepool.go @@ -7,17 +7,20 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" ) // NodePoolResource generates the HyperShift NodePool resource for the MC. -func NodePoolResource(nodePool *hyperfleetv1alpha1.NodePool, cluster *hyperfleetv1alpha1.Cluster) Resource { +func NodePoolResource(nodePool *hyperfleetv1alpha1.NodePool, cluster *hyperfleetv1alpha1.Cluster) (Resource, error) { clusterID := ClusterIDFromNamespace(cluster.Namespace) clusterName := cluster.Name // human-readable ns := cluster.Namespace // already "cluster-" npName := fmt.Sprintf("%s-%s", clusterName, nodePool.Name) - npSpec := nodePool.Spec.NodePool.DeepCopy() + npSpec, err := toNodePoolSpec(&nodePool.Spec.NodePool) + if err != nil { + return Resource{}, fmt.Errorf("converting NodePoolSpec for nodepool %s/%s: %w", ns, npName, err) + } npSpec.ClusterName = clusterName npSpec.Release.Image = "quay.io/openshift-release-dev/ocp-release:5.0.0-ec.2-multi" @@ -66,5 +69,5 @@ func NodePoolResource(nodePool *hyperfleetv1alpha1.NodePool, cluster *hyperfleet }, Spec: *npSpec, }, - } + }, nil } diff --git a/hyperfleet-operator/internal/render/nodepool_test.go b/hyperfleet-operator/internal/render/nodepool_test.go index 36a70213..c0b2ffc1 100644 --- a/hyperfleet-operator/internal/render/nodepool_test.go +++ b/hyperfleet-operator/internal/render/nodepool_test.go @@ -3,7 +3,7 @@ package render import ( "testing" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -16,7 +16,7 @@ func testNodePool() *hyperfleetv1alpha1.NodePool { Namespace: "cluster-abc12345", }, Spec: hyperfleetv1alpha1.NodePoolSpec{ - NodePool: hypershiftv1beta1.NodePoolSpec{ + NodePool: hyperfleetv1alpha1.NodePoolSpecPassthrough{ Replicas: ptr.To(int32(3)), Management: hypershiftv1beta1.NodePoolManagement{ AutoRepair: true, @@ -44,7 +44,10 @@ func testNodePool() *hyperfleetv1alpha1.NodePool { } func TestNodePoolResourceGVR(t *testing.T) { - r := NodePoolResource(testNodePool(), testCluster()) + r, err := NodePoolResource(testNodePool(), testCluster()) + if err != nil { + t.Fatalf("NodePoolResource: %v", err) + } if r.Group != "hypershift.openshift.io" { t.Errorf("Group = %q, want %q", r.Group, "hypershift.openshift.io") @@ -58,7 +61,10 @@ func TestNodePoolResourceGVR(t *testing.T) { } func TestNodePoolResourceNaming(t *testing.T) { - r := NodePoolResource(testNodePool(), testCluster()) + r, err := NodePoolResource(testNodePool(), testCluster()) + if err != nil { + t.Fatalf("NodePoolResource: %v", err) + } wantName := "my-cluster-workers" if r.Name != wantName { @@ -71,7 +77,10 @@ func TestNodePoolResourceNaming(t *testing.T) { } func TestNodePoolResourceObject(t *testing.T) { - r := NodePoolResource(testNodePool(), testCluster()) + r, err := NodePoolResource(testNodePool(), testCluster()) + if err != nil { + t.Fatalf("NodePoolResource: %v", err) + } np, ok := r.Object.(*hypershiftv1beta1.NodePool) if !ok { t.Fatalf("Object is %T, want *NodePool", r.Object) @@ -104,7 +113,7 @@ func TestNodePoolResourceDefaults(t *testing.T) { Namespace: "cluster-abc12345", }, Spec: hyperfleetv1alpha1.NodePoolSpec{ - NodePool: hypershiftv1beta1.NodePoolSpec{ + NodePool: hyperfleetv1alpha1.NodePoolSpecPassthrough{ Platform: hypershiftv1beta1.NodePoolPlatform{ Type: hypershiftv1beta1.AWSPlatform, AWS: &hypershiftv1beta1.AWSNodePoolPlatform{ @@ -117,7 +126,10 @@ func TestNodePoolResourceDefaults(t *testing.T) { }, } - r := NodePoolResource(minimalNP, testCluster()) + r, err := NodePoolResource(minimalNP, testCluster()) + if err != nil { + t.Fatalf("NodePoolResource: %v", err) + } np := r.Object.(*hypershiftv1beta1.NodePool) tests := []struct { @@ -162,7 +174,10 @@ func TestNodePoolResourceDefaults(t *testing.T) { } func TestNodePoolResourceLabels(t *testing.T) { - r := NodePoolResource(testNodePool(), testCluster()) + r, err := NodePoolResource(testNodePool(), testCluster()) + if err != nil { + t.Fatalf("NodePoolResource: %v", err) + } np := r.Object.(*hypershiftv1beta1.NodePool) if np.Labels["hyperfleet.io/cluster-id"] != "abc12345" { diff --git a/hyperfleet-operator/test/cluster_test.go b/hyperfleet-operator/test/cluster_test.go index c6cf6690..5cf60ed7 100644 --- a/hyperfleet-operator/test/cluster_test.go +++ b/hyperfleet-operator/test/cluster_test.go @@ -10,7 +10,7 @@ import ( . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/types" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" dynamo "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/dynamo" ) diff --git a/hyperfleet-operator/test/helpers_test.go b/hyperfleet-operator/test/helpers_test.go index 4dff72d7..316f72aa 100644 --- a/hyperfleet-operator/test/helpers_test.go +++ b/hyperfleet-operator/test/helpers_test.go @@ -8,7 +8,7 @@ import ( dynamodbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/hypershift/api/util/ipnet" corev1 "k8s.io/api/core/v1" @@ -140,7 +140,7 @@ func newTestCluster(name string) *hyperfleetv1alpha1.Cluster { ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "cluster-e2e-cluster-id"}, Spec: hyperfleetv1alpha1.ClusterSpec{ CreatorARN: "arn:aws:iam::111222333444:user/e2etester", - HostedCluster: hypershiftv1beta1.HostedClusterSpec{ + HostedCluster: hyperfleetv1alpha1.HostedClusterSpecPassthrough{ Release: hypershiftv1beta1.Release{Image: "quay.io/ocp:4.17"}, IssuerURL: "https://oidc.e2e.example.com/" + name, PullSecret: corev1.LocalObjectReference{Name: "pull-secret"}, @@ -206,7 +206,7 @@ func newTestNodePool() *hyperfleetv1alpha1.NodePool { return &hyperfleetv1alpha1.NodePool{ ObjectMeta: metav1.ObjectMeta{Name: "e2e-nodepool", Namespace: "cluster-e2e-cluster-id"}, Spec: hyperfleetv1alpha1.NodePoolSpec{ - NodePool: hypershiftv1beta1.NodePoolSpec{ + NodePool: hyperfleetv1alpha1.NodePoolSpecPassthrough{ ClusterName: "e2e-test-01", Replicas: ptr.To(int32(3)), Management: hypershiftv1beta1.NodePoolManagement{ diff --git a/hyperfleet-operator/test/manifest_test.go b/hyperfleet-operator/test/manifest_test.go index cb7b740e..c9de9dbe 100644 --- a/hyperfleet-operator/test/manifest_test.go +++ b/hyperfleet-operator/test/manifest_test.go @@ -9,7 +9,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/controller" dynamo "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/dynamo" ) diff --git a/hyperfleet-operator/test/suite_test.go b/hyperfleet-operator/test/suite_test.go index ea6b5740..bcba6f27 100644 --- a/hyperfleet-operator/test/suite_test.go +++ b/hyperfleet-operator/test/suite_test.go @@ -28,7 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/manager" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/controller" dynamo "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/dynamo" "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/dynamo/statusstream" diff --git a/platform-api/Containerfile b/platform-api/Containerfile index ffb84f0c..1630bdb6 100644 --- a/platform-api/Containerfile +++ b/platform-api/Containerfile @@ -7,12 +7,14 @@ ARG TARGETARCH=amd64 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 api/go.mod api/go.sum ./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 api/ ./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..c1661748 100644 --- a/platform-api/go.mod +++ b/platform-api/go.mod @@ -3,8 +3,9 @@ module github.com/openshift-online/rosa-hyperfleet-api/platform-api go 1.26.3 replace ( + github.com/openshift-online/rosa-hyperfleet-api/api => ../api + 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 ) require ( @@ -20,8 +21,10 @@ 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/api v0.0.0 + github.com/openshift-online/rosa-hyperfleet-api/hack/api-codegen v0.0.0-00010101000000-000000000000 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/api v0.0.0-20260416105050-3c6b218b8a80 github.com/openshift/hypershift/api v0.0.0-20260625052409-9acec4759a16 github.com/prometheus/client_golang v1.23.2 github.com/redis/go-redis/v9 v9.21.0 @@ -87,7 +90,6 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/gomega v1.42.1 // indirect - github.com/openshift/api v0.0.0-20260416105050-3c6b218b8a80 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.70.1 // indirect @@ -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/pkg/clients/hyperfleetdb/client.go b/platform-api/pkg/clients/hyperfleetdb/client.go index e7b20a67..e0d79fe9 100644 --- a/platform-api/pkg/clients/hyperfleetdb/client.go +++ b/platform-api/pkg/clients/hyperfleetdb/client.go @@ -12,7 +12,7 @@ import ( k8stypes "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" ) const accountIDLabel = "hyperfleet.io/account-id" diff --git a/platform-api/pkg/clients/hyperfleetdb/client_test.go b/platform-api/pkg/clients/hyperfleetdb/client_test.go index 933c3036..cd0380ef 100644 --- a/platform-api/pkg/clients/hyperfleetdb/client_test.go +++ b/platform-api/pkg/clients/hyperfleetdb/client_test.go @@ -11,7 +11,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client/fake" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" ) func testScheme() *runtime.Scheme { diff --git a/platform-api/pkg/clients/hyperfleetdb/convert.go b/platform-api/pkg/clients/hyperfleetdb/convert.go index 7c1c8377..429528e4 100644 --- a/platform-api/pkg/clients/hyperfleetdb/convert.go +++ b/platform-api/pkg/clients/hyperfleetdb/convert.go @@ -1,13 +1,14 @@ package hyperfleetdb import ( + "encoding/json" "strings" "time" hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/types" ) @@ -75,6 +76,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{ @@ -88,13 +91,12 @@ func PlatformCreateToClusterCR(clusterID, accountID string, req *types.ClusterCr }, nil } -// ApplyPlatformUpdateToClusterCR applies an update request to an existing CR. -func ApplyPlatformUpdateToClusterCR(cr *hyperfleetv1alpha1.Cluster, req *types.ClusterUpdateRequest) error { - if req.Spec == nil { - return nil - } - cr.Spec = *req.Spec - return nil +// MergeSpecJSON merges raw JSON into dst. Only fields present in the JSON +// overwrite dst; omitted fields are preserved. This avoids data loss from +// non-omitempty struct fields (e.g. HostedCluster, NodePool passthrough) +// that would serialize as empty objects if marshaled from a typed Go struct. +func MergeSpecJSON(dst any, specJSON json.RawMessage) error { + return json.Unmarshal(specJSON, dst) } // ClusterStatusFromCR builds the status response from a Cluster CR. @@ -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{ @@ -162,15 +166,6 @@ func PlatformCreateToNodePoolCR(accountID string, req *types.NodePoolCreateReque }, nil } -// ApplyPlatformUpdateToNodePoolCR applies an update request to an existing CR. -func ApplyPlatformUpdateToNodePoolCR(cr *hyperfleetv1alpha1.NodePool, req *types.NodePoolUpdateRequest) error { - if req.Spec == nil { - return nil - } - cr.Spec = *req.Spec - return nil -} - // NodePoolStatusFromCR builds the status response from a NodePool CR. func NodePoolStatusFromCR(cr *hyperfleetv1alpha1.NodePool) *types.NodePoolStatusResponse { platform := NodePoolCRToPlatform(cr) diff --git a/platform-api/pkg/clients/hyperfleetdb/convert_test.go b/platform-api/pkg/clients/hyperfleetdb/convert_test.go index 59829c07..c6497aea 100644 --- a/platform-api/pkg/clients/hyperfleetdb/convert_test.go +++ b/platform-api/pkg/clients/hyperfleetdb/convert_test.go @@ -3,7 +3,7 @@ package hyperfleetdb import ( "testing" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/types" @@ -14,7 +14,7 @@ func TestPlatformCreateToNodePoolCR_SetsAccountLabel(t *testing.T) { ClusterID: "test-cluster-id", Name: "my-nodepool", Spec: &hyperfleetv1alpha1.NodePoolSpec{ - NodePool: hypershiftv1beta1.NodePoolSpec{ + NodePool: hyperfleetv1alpha1.NodePoolSpecPassthrough{ Platform: hypershiftv1beta1.NodePoolPlatform{ Type: hypershiftv1beta1.AWSPlatform, AWS: &hypershiftv1beta1.AWSNodePoolPlatform{ @@ -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,13 +37,21 @@ 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) { req := &types.ClusterCreateRequest{ Name: "my-cluster", Spec: &hyperfleetv1alpha1.ClusterSpec{ - HostedCluster: hypershiftv1beta1.HostedClusterSpec{ + HostedCluster: hyperfleetv1alpha1.HostedClusterSpecPassthrough{ Platform: hypershiftv1beta1.PlatformSpec{ Type: hypershiftv1beta1.AWSPlatform, AWS: &hypershiftv1beta1.AWSPlatformSpec{ @@ -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/conversion/types.go b/platform-api/pkg/conversion/types.go new file mode 100644 index 00000000..ee1c497e --- /dev/null +++ b/platform-api/pkg/conversion/types.go @@ -0,0 +1,155 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package conversion + +import ( + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" + 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" +) + +// ServiceSetFields contains platform-managed fields injected during UnprojectX conversions +type ServiceSetFields struct { + // AccountID is service-set (platform-managed, hidden from API) + AccountID string `json:"accountId"` + // AdditionalTrustBundle is service-set (platform-managed, hidden from API) + AdditionalTrustBundle *corev1.LocalObjectReference `json:"additionalTrustBundle"` + // AllowedUnsafeSysctls is service-set (platform-managed, hidden from API) + AllowedUnsafeSysctls []string `json:"allowedUnsafeSysctls"` + // ApiServer is service-set (platform-managed, hidden from API) + ApiServer *v1alpha1.APIServerNetworkConfiguration `json:"apiServer"` + // Arch is service-set (platform-managed, hidden from API) + Arch string `json:"arch"` + // AuditWebhook is service-set (platform-managed, hidden from API) + AuditWebhook *corev1.LocalObjectReference `json:"auditWebhook"` + // Authentication is service-set (platform-managed, hidden from API) + Authentication *v1alpha1.ClusterAuthentication `json:"authentication"` + // AutoNode is service-set (platform-managed, hidden from API) + AutoNode hypershiftv1beta1.AutoNode `json:"autoNode"` + // AutoScaling is service-set (platform-managed, hidden from API) + AutoScaling *hypershiftv1beta1.NodePoolAutoScaling `json:"autoScaling"` + // Autoscaling is service-set (platform-managed, hidden from API) + Autoscaling hypershiftv1beta1.ClusterAutoscaling `json:"autoscaling"` + // Capabilities is service-set (platform-managed, hidden from API) + Capabilities *hypershiftv1beta1.Capabilities `json:"capabilities"` + // Channel is service-set (platform-managed, hidden from API) + Channel string `json:"channel"` + // ClusterID is service-set (platform-managed, hidden from API) + ClusterID string `json:"clusterID"` + // ClusterName is service-set (platform-managed, hidden from API) + ClusterName string `json:"clusterName"` + // Config is service-set (platform-managed, hidden from API) + Config []corev1.LocalObjectReference `json:"config"` + // Configuration is service-set (platform-managed, hidden from API) + Configuration *v1alpha1.ClusterConfiguration `json:"configuration"` + // ControlPlaneRelease is service-set (platform-managed, hidden from API) + ControlPlaneRelease *hypershiftv1beta1.Release `json:"controlPlaneRelease"` + // ControllerAvailabilityPolicy is service-set (platform-managed, hidden from API) + ControllerAvailabilityPolicy hypershiftv1beta1.AvailabilityPolicy `json:"controllerAvailabilityPolicy"` + // CpuManagerPolicy is service-set (platform-managed, hidden from API) + CpuManagerPolicy *string `json:"cpuManagerPolicy"` + // CpuManagerPolicyOptions is service-set (platform-managed, hidden from API) + CpuManagerPolicyOptions map[string]string `json:"cpuManagerPolicyOptions"` + // CpuManagerReconcilePeriod is service-set (platform-managed, hidden from API) + CpuManagerReconcilePeriod *metav1.Duration `json:"cpuManagerReconcilePeriod"` + // CreatorARN is service-set (platform-managed, hidden from API) + CreatorARN string `json:"creatorARN"` + // Dns is service-set (platform-managed, hidden from API) + Dns hypershiftv1beta1.DNSSpec `json:"dns"` + // EvictionHard is service-set (platform-managed, hidden from API) + EvictionHard map[string]string `json:"evictionHard"` + // EvictionSoft is service-set (platform-managed, hidden from API) + EvictionSoft map[string]string `json:"evictionSoft"` + // EvictionSoftGracePeriod is service-set (platform-managed, hidden from API) + EvictionSoftGracePeriod map[string]string `json:"evictionSoftGracePeriod"` + // Extensions is service-set (platform-managed, hidden from API) + Extensions []string `json:"extensions"` + // FeatureGate is service-set (platform-managed, hidden from API) + FeatureGate *v1alpha1.FeatureGateConfiguration `json:"featureGate"` + // Files is service-set (platform-managed, hidden from API) + Files []v1alpha1.FileSpec `json:"files"` + // Fips is service-set (platform-managed, hidden from API) + Fips bool `json:"fips"` + // Image is service-set (platform-managed, hidden from API) + Image *v1alpha1.ImageConfiguration `json:"image"` + // InfraID is service-set (platform-managed, hidden from API) + InfraID string `json:"infraID"` + // InfrastructureAvailabilityPolicy is service-set (platform-managed, hidden from API) + InfrastructureAvailabilityPolicy hypershiftv1beta1.AvailabilityPolicy `json:"infrastructureAvailabilityPolicy"` + // Ingress is service-set (platform-managed, hidden from API) + Ingress *v1alpha1.IngressConfiguration `json:"ingress"` + // InternalID is service-set (platform-managed, hidden from API) + InternalID string `json:"internalId"` + // InternalPoolID is service-set (platform-managed, hidden from API) + InternalPoolID string `json:"internalPoolId"` + // IssuerURL is service-set (platform-managed, hidden from API) + IssuerURL string `json:"issuerURL"` + // KernelArguments is service-set (platform-managed, hidden from API) + KernelArguments []string `json:"kernelArguments"` + // KernelType is service-set (platform-managed, hidden from API) + KernelType *string `json:"kernelType"` + // KubeAPIServerDNSName is service-set (platform-managed, hidden from API) + KubeAPIServerDNSName string `json:"kubeAPIServerDNSName"` + // Kubelet is service-set (platform-managed, hidden from API) + Kubelet *v1alpha1.KubeletConfig `json:"kubelet"` + // Labels is service-set (platform-managed, hidden from API) + Labels map[string]string `json:"labels"` + // MachineConfig is service-set (platform-managed, hidden from API) + MachineConfig *v1alpha1.MachineConfigSpec `json:"machineConfig"` + // Management is service-set (platform-managed, hidden from API) + Management hypershiftv1beta1.NodePoolManagement `json:"management"` + // MemoryThrottlingFactor is service-set (platform-managed, hidden from API) + MemoryThrottlingFactor *float64 `json:"memoryThrottlingFactor"` + // Network is service-set (platform-managed, hidden from API) + Network *v1alpha1.NetworkConfiguration `json:"network"` + // NodeDrainTimeout is service-set (platform-managed, hidden from API) + NodeDrainTimeout *metav1.Duration `json:"nodeDrainTimeout"` + // NodeLabels is service-set (platform-managed, hidden from API) + NodeLabels map[string]string `json:"nodeLabels"` + // NodeSelector is service-set (platform-managed, hidden from API) + NodeSelector map[string]string `json:"nodeSelector"` + // NodeVolumeDetachTimeout is service-set (platform-managed, hidden from API) + NodeVolumeDetachTimeout *metav1.Duration `json:"nodeVolumeDetachTimeout"` + // Oauth is service-set (platform-managed, hidden from API) + Oauth *v1alpha1.OAuthConfiguration `json:"oauth"` + // OlmCatalogPlacement is service-set (platform-managed, hidden from API) + OlmCatalogPlacement hypershiftv1beta1.OLMCatalogPlacement `json:"olmCatalogPlacement"` + // OperatorConfiguration is service-set (platform-managed, hidden from API) + OperatorConfiguration *hypershiftv1beta1.OperatorConfiguration `json:"operatorConfiguration"` + // OsImageStream is service-set (platform-managed, hidden from API) + OsImageStream hypershiftv1beta1.OSImageStreamReference `json:"osImageStream"` + // PausedUntil is service-set (platform-managed, hidden from API) + PausedUntil *string `json:"pausedUntil"` + // Proxy is service-set (platform-managed, hidden from API) + Proxy *v1alpha1.ProxyConfiguration `json:"proxy"` + // PullSecret is service-set (platform-managed, hidden from API) + PullSecret corev1.LocalObjectReference `json:"pullSecret"` + // Release is service-set (platform-managed, hidden from API) + Release hypershiftv1beta1.Release `json:"release"` + // Scheduler is service-set (platform-managed, hidden from API) + Scheduler *v1alpha1.SchedulerConfiguration `json:"scheduler"` + // SecretEncryption is service-set (platform-managed, hidden from API) + SecretEncryption *hypershiftv1beta1.SecretEncryptionSpec `json:"secretEncryption"` + // ServiceAccountSigningKey is service-set (platform-managed, hidden from API) + ServiceAccountSigningKey *corev1.LocalObjectReference `json:"serviceAccountSigningKey"` + // Services is service-set (platform-managed, hidden from API) + Services []hypershiftv1beta1.ServicePublishingStrategyMapping `json:"services"` + // SshKey is service-set (platform-managed, hidden from API) + SshKey corev1.LocalObjectReference `json:"sshKey"` + // SystemdUnits is service-set (platform-managed, hidden from API) + SystemdUnits []v1alpha1.SystemdUnit `json:"systemdUnits"` + // Taints is service-set (platform-managed, hidden from API) + Taints []hypershiftv1beta1.Taint `json:"taints"` + // Tolerations is service-set (platform-managed, hidden from API) + Tolerations []corev1.Toleration `json:"tolerations"` + // TopologyManagerPolicy is service-set (platform-managed, hidden from API) + TopologyManagerPolicy *string `json:"topologyManagerPolicy"` + // TopologyManagerScope is service-set (platform-managed, hidden from API) + TopologyManagerScope *string `json:"topologyManagerScope"` + // TuningConfig is service-set (platform-managed, hidden from API) + TuningConfig []corev1.LocalObjectReference `json:"tuningConfig"` + // UpdateService is service-set (platform-managed, hidden from API) + UpdateService configv1.URL `json:"updateService"` +} diff --git a/platform-api/pkg/conversion/v1alpha1/cluster.go b/platform-api/pkg/conversion/v1alpha1/cluster.go new file mode 100644 index 00000000..c21404b8 --- /dev/null +++ b/platform-api/pkg/conversion/v1alpha1/cluster.go @@ -0,0 +1,60 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "encoding/json" + + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" + rest "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1/public" + "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/conversion" +) + +// ProjectCluster converts CRD Cluster to REST (visible fields only). +// Uses JSON roundtrip: marshal CRD → unmarshal into REST, so hidden fields are +// automatically dropped (REST types lack them). +func ProjectCluster(crd *v1alpha1.Cluster) *rest.Cluster { + if crd == nil { + return nil + } + + spec := projectClusterSpec(crd.Spec) + status := projectClusterStatus(crd.Status) + return &rest.Cluster{ + Spec: spec, + Status: status, + } +} + +func projectClusterSpec(crd v1alpha1.ClusterSpec) rest.ClusterSpec { + data, _ := json.Marshal(crd) + var out rest.ClusterSpec + _ = json.Unmarshal(data, &out) + return out +} + +func projectClusterStatus(crd v1alpha1.ClusterStatus) rest.ClusterStatus { + data, _ := json.Marshal(crd) + var out rest.ClusterStatus + _ = json.Unmarshal(data, &out) + return out +} + +// UnprojectCluster converts REST ClusterSpec to CRD with service-set enrichment. +// Uses JSON roundtrip: marshal REST → unmarshal into CRD, then overlay service-set fields. +func UnprojectCluster(spec *rest.ClusterSpec, enrichment *conversion.ServiceSetFields) *v1alpha1.ClusterSpec { + if spec == nil { + return nil + } + + data, _ := json.Marshal(spec) + var crdSpec v1alpha1.ClusterSpec + _ = json.Unmarshal(data, &crdSpec) + + if enrichment != nil { + ssData, _ := json.Marshal(enrichment) + _ = json.Unmarshal(ssData, &crdSpec) + } + + return &crdSpec +} diff --git a/platform-api/pkg/conversion/v1alpha1/managementcluster.go b/platform-api/pkg/conversion/v1alpha1/managementcluster.go new file mode 100644 index 00000000..8cbd3b1c --- /dev/null +++ b/platform-api/pkg/conversion/v1alpha1/managementcluster.go @@ -0,0 +1,60 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "encoding/json" + + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" + rest "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1/public" + "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/conversion" +) + +// ProjectManagementCluster converts CRD ManagementCluster to REST (visible fields only). +// Uses JSON roundtrip: marshal CRD → unmarshal into REST, so hidden fields are +// automatically dropped (REST types lack them). +func ProjectManagementCluster(crd *v1alpha1.ManagementCluster) *rest.ManagementCluster { + if crd == nil { + return nil + } + + spec := projectManagementClusterSpec(crd.Spec) + status := projectManagementClusterStatus(crd.Status) + return &rest.ManagementCluster{ + Spec: spec, + Status: status, + } +} + +func projectManagementClusterSpec(crd v1alpha1.ManagementClusterSpec) rest.ManagementClusterSpec { + data, _ := json.Marshal(crd) + var out rest.ManagementClusterSpec + _ = json.Unmarshal(data, &out) + return out +} + +func projectManagementClusterStatus(crd v1alpha1.ManagementClusterStatus) rest.ManagementClusterStatus { + data, _ := json.Marshal(crd) + var out rest.ManagementClusterStatus + _ = json.Unmarshal(data, &out) + return out +} + +// UnprojectManagementCluster converts REST ManagementClusterSpec to CRD with service-set enrichment. +// Uses JSON roundtrip: marshal REST → unmarshal into CRD, then overlay service-set fields. +func UnprojectManagementCluster(spec *rest.ManagementClusterSpec, enrichment *conversion.ServiceSetFields) *v1alpha1.ManagementClusterSpec { + if spec == nil { + return nil + } + + data, _ := json.Marshal(spec) + var crdSpec v1alpha1.ManagementClusterSpec + _ = json.Unmarshal(data, &crdSpec) + + if enrichment != nil { + ssData, _ := json.Marshal(enrichment) + _ = json.Unmarshal(ssData, &crdSpec) + } + + return &crdSpec +} diff --git a/platform-api/pkg/conversion/v1alpha1/manifest.go b/platform-api/pkg/conversion/v1alpha1/manifest.go new file mode 100644 index 00000000..f34ebf52 --- /dev/null +++ b/platform-api/pkg/conversion/v1alpha1/manifest.go @@ -0,0 +1,60 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "encoding/json" + + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" + rest "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1/public" + "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/conversion" +) + +// ProjectManifest converts CRD Manifest to REST (visible fields only). +// Uses JSON roundtrip: marshal CRD → unmarshal into REST, so hidden fields are +// automatically dropped (REST types lack them). +func ProjectManifest(crd *v1alpha1.Manifest) *rest.Manifest { + if crd == nil { + return nil + } + + spec := projectManifestSpec(crd.Spec) + status := projectManifestStatus(crd.Status) + return &rest.Manifest{ + Spec: spec, + Status: status, + } +} + +func projectManifestSpec(crd v1alpha1.ManifestSpec) rest.ManifestSpec { + data, _ := json.Marshal(crd) + var out rest.ManifestSpec + _ = json.Unmarshal(data, &out) + return out +} + +func projectManifestStatus(crd v1alpha1.ManifestStatus) rest.ManifestStatus { + data, _ := json.Marshal(crd) + var out rest.ManifestStatus + _ = json.Unmarshal(data, &out) + return out +} + +// UnprojectManifest converts REST ManifestSpec to CRD with service-set enrichment. +// Uses JSON roundtrip: marshal REST → unmarshal into CRD, then overlay service-set fields. +func UnprojectManifest(spec *rest.ManifestSpec, enrichment *conversion.ServiceSetFields) *v1alpha1.ManifestSpec { + if spec == nil { + return nil + } + + data, _ := json.Marshal(spec) + var crdSpec v1alpha1.ManifestSpec + _ = json.Unmarshal(data, &crdSpec) + + if enrichment != nil { + ssData, _ := json.Marshal(enrichment) + _ = json.Unmarshal(ssData, &crdSpec) + } + + return &crdSpec +} diff --git a/platform-api/pkg/conversion/v1alpha1/nodepool.go b/platform-api/pkg/conversion/v1alpha1/nodepool.go new file mode 100644 index 00000000..02d367df --- /dev/null +++ b/platform-api/pkg/conversion/v1alpha1/nodepool.go @@ -0,0 +1,60 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "encoding/json" + + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" + rest "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1/public" + "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/conversion" +) + +// ProjectNodePool converts CRD NodePool to REST (visible fields only). +// Uses JSON roundtrip: marshal CRD → unmarshal into REST, so hidden fields are +// automatically dropped (REST types lack them). +func ProjectNodePool(crd *v1alpha1.NodePool) *rest.NodePool { + if crd == nil { + return nil + } + + spec := projectNodePoolSpec(crd.Spec) + status := projectNodePoolStatus(crd.Status) + return &rest.NodePool{ + Spec: spec, + Status: status, + } +} + +func projectNodePoolSpec(crd v1alpha1.NodePoolSpec) rest.NodePoolSpec { + data, _ := json.Marshal(crd) + var out rest.NodePoolSpec + _ = json.Unmarshal(data, &out) + return out +} + +func projectNodePoolStatus(crd v1alpha1.NodePoolStatus) rest.NodePoolStatus { + data, _ := json.Marshal(crd) + var out rest.NodePoolStatus + _ = json.Unmarshal(data, &out) + return out +} + +// UnprojectNodePool converts REST NodePoolSpec to CRD with service-set enrichment. +// Uses JSON roundtrip: marshal REST → unmarshal into CRD, then overlay service-set fields. +func UnprojectNodePool(spec *rest.NodePoolSpec, enrichment *conversion.ServiceSetFields) *v1alpha1.NodePoolSpec { + if spec == nil { + return nil + } + + data, _ := json.Marshal(spec) + var crdSpec v1alpha1.NodePoolSpec + _ = json.Unmarshal(data, &crdSpec) + + if enrichment != nil { + ssData, _ := json.Marshal(enrichment) + _ = json.Unmarshal(ssData, &crdSpec) + } + + return &crdSpec +} diff --git a/platform-api/pkg/conversion/v1alpha1/placement.go b/platform-api/pkg/conversion/v1alpha1/placement.go new file mode 100644 index 00000000..0cd5fca6 --- /dev/null +++ b/platform-api/pkg/conversion/v1alpha1/placement.go @@ -0,0 +1,60 @@ +// Code generated by conversion-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "encoding/json" + + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" + rest "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1/public" + "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/conversion" +) + +// ProjectPlacement converts CRD Placement to REST (visible fields only). +// Uses JSON roundtrip: marshal CRD → unmarshal into REST, so hidden fields are +// automatically dropped (REST types lack them). +func ProjectPlacement(crd *v1alpha1.Placement) *rest.Placement { + if crd == nil { + return nil + } + + spec := projectPlacementSpec(crd.Spec) + status := projectPlacementStatus(crd.Status) + return &rest.Placement{ + Spec: spec, + Status: status, + } +} + +func projectPlacementSpec(crd v1alpha1.PlacementSpec) rest.PlacementSpec { + data, _ := json.Marshal(crd) + var out rest.PlacementSpec + _ = json.Unmarshal(data, &out) + return out +} + +func projectPlacementStatus(crd v1alpha1.PlacementStatus) rest.PlacementStatus { + data, _ := json.Marshal(crd) + var out rest.PlacementStatus + _ = json.Unmarshal(data, &out) + return out +} + +// UnprojectPlacement converts REST PlacementSpec to CRD with service-set enrichment. +// Uses JSON roundtrip: marshal REST → unmarshal into CRD, then overlay service-set fields. +func UnprojectPlacement(spec *rest.PlacementSpec, enrichment *conversion.ServiceSetFields) *v1alpha1.PlacementSpec { + if spec == nil { + return nil + } + + data, _ := json.Marshal(spec) + var crdSpec v1alpha1.PlacementSpec + _ = json.Unmarshal(data, &crdSpec) + + if enrichment != nil { + ssData, _ := json.Marshal(enrichment) + _ = json.Unmarshal(ssData, &crdSpec) + } + + return &crdSpec +} diff --git a/platform-api/pkg/handlers/cluster.go b/platform-api/pkg/handlers/cluster.go index 1eb90e9c..ac6ece76 100644 --- a/platform-api/pkg/handlers/cluster.go +++ b/platform-api/pkg/handlers/cluster.go @@ -3,6 +3,7 @@ package handlers import ( "encoding/json" "fmt" + "io" "log/slog" "net/http" "strconv" @@ -12,9 +13,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 +25,7 @@ type ClusterHandler struct { db *hyperfleetdb.Client oidcIssuerBaseURL string defaultClusterExpiration time.Duration + validator *validation.FieldValidator logger *slog.Logger } @@ -31,6 +35,7 @@ func NewClusterHandler(db *hyperfleetdb.Client, oidcIssuerBaseURL string, defaul db: db, oidcIssuerBaseURL: oidcIssuerBaseURL, defaultClusterExpiration: defaultClusterExpiration, + validator: validation.NewFieldValidator(), logger: logger, } } @@ -108,6 +113,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) @@ -190,8 +200,14 @@ func (h *ClusterHandler) Update(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) clusterID := vars["id"] + body, err := io.ReadAll(r.Body) + if err != nil { + h.writeError(w, http.StatusBadRequest, "CLUSTERS-MGMT-UPDATE-001", "Failed to read request body") + return + } + var req types.ClusterUpdateRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if err := json.Unmarshal(body, &req); err != nil { h.writeError(w, http.StatusBadRequest, "CLUSTERS-MGMT-UPDATE-001", "Invalid request body") return } @@ -214,18 +230,26 @@ func (h *ClusterHandler) Update(w http.ResponseWriter, r *http.Request) { return } - existingIssuerURL := cr.Spec.HostedCluster.IssuerURL - existingExpiration := cr.Spec.ExpirationTimestamp + if errs := h.validator.ValidateUpdate(req.Spec, &cr.Spec, featuregate.Default); errs != nil { + h.writeValidationErrors(w, errs) + return + } - if err := hyperfleetdb.ApplyPlatformUpdateToClusterCR(cr, &req); err != nil { - h.logger.Error("failed to merge cluster spec", "error", err) - h.writeError(w, http.StatusBadRequest, "CLUSTERS-MGMT-UPDATE-002", "Invalid cluster spec") + // Extract raw "spec" JSON from the request body so the merge only + // overwrites fields the caller actually sent, preserving service-set + // fields that lack omitempty (e.g. hostedCluster, nodePool). + var envelope struct { + Spec json.RawMessage `json:"spec"` + } + if err := json.Unmarshal(body, &envelope); err != nil { + h.writeError(w, http.StatusBadRequest, "CLUSTERS-MGMT-UPDATE-001", "Invalid request body") return } - cr.Spec.HostedCluster.IssuerURL = existingIssuerURL - if cr.Spec.ExpirationTimestamp == nil { - cr.Spec.ExpirationTimestamp = existingExpiration + if err := hyperfleetdb.MergeSpecJSON(&cr.Spec, envelope.Spec); err != nil { + h.logger.Error("failed to merge cluster spec", "error", err) + h.writeError(w, http.StatusBadRequest, "CLUSTERS-MGMT-UPDATE-002", "Invalid cluster spec") + return } if err := h.db.UpdateCluster(ctx, cr); err != nil { @@ -295,6 +319,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/cluster_test.go b/platform-api/pkg/handlers/cluster_test.go index 1ceb7838..cccd61d9 100644 --- a/platform-api/pkg/handlers/cluster_test.go +++ b/platform-api/pkg/handlers/cluster_test.go @@ -11,6 +11,7 @@ import ( "net/http/httptest" "os" "testing" + "time" "github.com/gorilla/mux" corev1 "k8s.io/api/core/v1" @@ -18,7 +19,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client/fake" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/clients/hyperfleetdb" @@ -51,7 +52,7 @@ func testClusterCR(clusterID, clusterName, accountID string) *hyperfleetv1alpha1 Labels: map[string]string{"hyperfleet.io/account-id": accountID}, }, Spec: hyperfleetv1alpha1.ClusterSpec{ - HostedCluster: hypershiftv1beta1.HostedClusterSpec{ + HostedCluster: hyperfleetv1alpha1.HostedClusterSpecPassthrough{ Platform: hypershiftv1beta1.PlatformSpec{ Type: hypershiftv1beta1.AWSPlatform, AWS: &hypershiftv1beta1.AWSPlatformSpec{ @@ -70,7 +71,7 @@ func TestClusterHandler_List_Success(t *testing.T) { testClusterCR("uuid-2", "cluster-2", testAccountID), ).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) req := httptest.NewRequest(http.MethodGet, "/api/v0/clusters", nil) req = req.WithContext(testContext(testAccountID)) @@ -98,7 +99,7 @@ func TestClusterHandler_List_Empty(t *testing.T) { scheme := newTestScheme() fc := fake.NewClientBuilder().WithScheme(scheme).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) req := httptest.NewRequest(http.MethodGet, "/api/v0/clusters", nil) req = req.WithContext(testContext(testAccountID)) @@ -126,7 +127,7 @@ func TestClusterHandler_List_Pagination(t *testing.T) { testClusterCR("uuid-c3", "c3", testAccountID), ).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) req := httptest.NewRequest(http.MethodGet, "/api/v0/clusters?limit=2&offset=1", nil) req = req.WithContext(testContext(testAccountID)) @@ -154,7 +155,7 @@ func TestClusterHandler_Create_Success(t *testing.T) { scheme := newTestScheme() fc := fake.NewClientBuilder().WithScheme(scheme).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) body, _ := json.Marshal(map[string]any{ "name": "my-cluster", @@ -192,7 +193,7 @@ func TestClusterHandler_Create_SetsCreatorARN(t *testing.T) { scheme := newTestScheme() fc := fake.NewClientBuilder().WithScheme(scheme).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) body, _ := json.Marshal(map[string]any{ "name": "my-cluster", @@ -221,7 +222,7 @@ func TestClusterHandler_Create_InvalidJSON(t *testing.T) { scheme := newTestScheme() fc := fake.NewClientBuilder().WithScheme(scheme).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) req := httptest.NewRequest(http.MethodPost, "/api/v0/clusters", bytes.NewReader([]byte("not json"))) req = req.WithContext(testContext(testAccountID)) @@ -249,7 +250,7 @@ func TestClusterHandler_Create_MissingFields(t *testing.T) { scheme := newTestScheme() fc := fake.NewClientBuilder().WithScheme(scheme).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) body, _ := json.Marshal(tt.body) req := httptest.NewRequest(http.MethodPost, "/api/v0/clusters", bytes.NewReader(body)) @@ -271,7 +272,7 @@ func TestClusterHandler_Get_Success(t *testing.T) { testClusterCR("cluster-123", "test-cluster", testAccountID), ).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) req := httptest.NewRequest(http.MethodGet, "/api/v0/clusters/cluster-123", nil) req = req.WithContext(testContext(testAccountID)) @@ -299,7 +300,7 @@ func TestClusterHandler_Get_NotFound(t *testing.T) { scheme := newTestScheme() fc := fake.NewClientBuilder().WithScheme(scheme).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) req := httptest.NewRequest(http.MethodGet, "/api/v0/clusters/no-such-cluster", nil) req = req.WithContext(testContext(testAccountID)) @@ -325,7 +326,7 @@ func TestClusterHandler_Delete_Success(t *testing.T) { testClusterCR("cluster-123", "test-cluster", testAccountID), ).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) req := httptest.NewRequest(http.MethodDelete, "/api/v0/clusters/cluster-123", nil) req = req.WithContext(testContext(testAccountID)) @@ -349,7 +350,7 @@ func TestClusterHandler_Delete_NotFound(t *testing.T) { scheme := newTestScheme() fc := fake.NewClientBuilder().WithScheme(scheme).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) req := httptest.NewRequest(http.MethodDelete, "/api/v0/clusters/no-such-cluster", nil) req = req.WithContext(testContext(testAccountID)) @@ -381,7 +382,7 @@ func TestClusterHandler_GetStatus_Success(t *testing.T) { fc := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cr). WithStatusSubresource(cr).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) req := httptest.NewRequest(http.MethodGet, "/api/v0/clusters/cluster-123/statuses", nil) req = req.WithContext(testContext(testAccountID)) @@ -406,7 +407,7 @@ func TestClusterHandler_GetStatus_NotFound(t *testing.T) { scheme := newTestScheme() fc := fake.NewClientBuilder().WithScheme(scheme).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) req := httptest.NewRequest(http.MethodGet, "/api/v0/clusters/no-such/statuses", nil) req = req.WithContext(testContext(testAccountID)) @@ -426,7 +427,7 @@ func TestClusterHandler_Update_Success(t *testing.T) { testClusterCR("cluster-123", "test-cluster", testAccountID), ).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) body, _ := json.Marshal(map[string]any{ "spec": map[string]any{ @@ -457,7 +458,7 @@ func TestClusterHandler_Update_NotFound(t *testing.T) { scheme := newTestScheme() fc := fake.NewClientBuilder().WithScheme(scheme).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) body, _ := json.Marshal(map[string]any{ "spec": map[string]any{"name": "x"}, @@ -479,7 +480,7 @@ func TestClusterHandler_Update_MissingSpec(t *testing.T) { scheme := newTestScheme() fc := fake.NewClientBuilder().WithScheme(scheme).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) body, _ := json.Marshal(map[string]any{}) @@ -501,7 +502,7 @@ func TestClusterHandler_Create_DuplicateName(t *testing.T) { testClusterCR("existing-id", "test-cluster", testAccountID), ).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) body, _ := json.Marshal(map[string]any{ "name": "test-cluster", @@ -532,7 +533,7 @@ func TestClusterHandler_Create_SameNameDifferentAccount(t *testing.T) { testClusterCR("existing-id", "test-cluster", otherAccount), ).Build() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", logger) + handler := NewClusterHandler(hyperfleetdb.NewClientFrom(fc, logger), "https://oidc.example.com", 0, logger) body, _ := json.Marshal(map[string]any{ "name": "test-cluster", diff --git a/platform-api/pkg/handlers/management_cluster.go b/platform-api/pkg/handlers/management_cluster.go index cb8fe488..5edc4551 100644 --- a/platform-api/pkg/handlers/management_cluster.go +++ b/platform-api/pkg/handlers/management_cluster.go @@ -8,7 +8,7 @@ import ( "github.com/gorilla/mux" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/clients/hyperfleetdb" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/middleware" diff --git a/platform-api/pkg/handlers/nodepool.go b/platform-api/pkg/handlers/nodepool.go index d2c7bf14..1d900d0c 100644 --- a/platform-api/pkg/handlers/nodepool.go +++ b/platform-api/pkg/handlers/nodepool.go @@ -2,25 +2,31 @@ package handlers import ( "encoding/json" + "io" "log/slog" "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" "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 +101,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") @@ -107,7 +118,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") @@ -155,8 +167,14 @@ func (h *NodePoolHandler) Update(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) nodepoolID := vars["id"] + body, err := io.ReadAll(r.Body) + if err != nil { + h.writeError(w, http.StatusBadRequest, "NODEPOOLS-MGMT-UPDATE-001", "Failed to read request body") + return + } + var req types.NodePoolUpdateRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if err := json.Unmarshal(body, &req); err != nil { h.writeError(w, http.StatusBadRequest, "NODEPOOLS-MGMT-UPDATE-001", "Invalid request body") return } @@ -179,7 +197,20 @@ func (h *NodePoolHandler) Update(w http.ResponseWriter, r *http.Request) { return } - if err := hyperfleetdb.ApplyPlatformUpdateToNodePoolCR(cr, &req); err != nil { + if errs := h.validator.ValidateUpdate(req.Spec, &cr.Spec, featuregate.Default); errs != nil { + h.writeValidationErrors(w, errs) + return + } + + var envelope struct { + Spec json.RawMessage `json:"spec"` + } + if err := json.Unmarshal(body, &envelope); err != nil { + h.writeError(w, http.StatusBadRequest, "NODEPOOLS-MGMT-UPDATE-001", "Invalid request body") + return + } + + if err := hyperfleetdb.MergeSpecJSON(&cr.Spec, envelope.Spec); 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 @@ -249,6 +280,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/handlers/zoa_test.go b/platform-api/pkg/handlers/zoa_test.go index 419b32ca..ab57b532 100644 --- a/platform-api/pkg/handlers/zoa_test.go +++ b/platform-api/pkg/handlers/zoa_test.go @@ -21,7 +21,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client/fake" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/clients/hyperfleetdb" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/middleware" diff --git a/platform-api/pkg/types/cluster.go b/platform-api/pkg/types/cluster.go index ac03c971..bc771397 100644 --- a/platform-api/pkg/types/cluster.go +++ b/platform-api/pkg/types/cluster.go @@ -3,7 +3,7 @@ package types import ( "time" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" ) // Cluster represents a cluster resource diff --git a/platform-api/pkg/types/nodepool.go b/platform-api/pkg/types/nodepool.go index 62f63dff..a6c96daf 100644 --- a/platform-api/pkg/types/nodepool.go +++ b/platform-api/pkg/types/nodepool.go @@ -3,7 +3,7 @@ package types import ( "time" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" ) // NodePool represents a nodepool resource diff --git a/platform-api/pkg/validation/field_validator.go b/platform-api/pkg/validation/field_validator.go new file mode 100644 index 00000000..eae425c4 --- /dev/null +++ b/platform-api/pkg/validation/field_validator.go @@ -0,0 +1,220 @@ +package validation + +import ( + "encoding/json" + "fmt" + "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" +) + +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: + if isZeroValue(fields[fieldPath]) { + return nil + } + 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 +} + +// 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 + 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..900e083c --- /dev/null +++ b/platform-api/pkg/validation/field_validator_test.go @@ -0,0 +1,253 @@ +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" +) + +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_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}, + }) + + 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) + } +} diff --git a/platform-api/pkg/zoa/jobbuilder.go b/platform-api/pkg/zoa/jobbuilder.go index d971eb5f..0d8d6659 100644 --- a/platform-api/pkg/zoa/jobbuilder.go +++ b/platform-api/pkg/zoa/jobbuilder.go @@ -10,7 +10,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" ) const ( diff --git a/platform-api/pkg/zoa/reconciler.go b/platform-api/pkg/zoa/reconciler.go index 54655c65..21a70674 100644 --- a/platform-api/pkg/zoa/reconciler.go +++ b/platform-api/pkg/zoa/reconciler.go @@ -6,7 +6,7 @@ import ( "log/slog" "time" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/clients/hyperfleetdb" ) diff --git a/platform-api/pkg/zoa/reconciler_test.go b/platform-api/pkg/zoa/reconciler_test.go index d342fb8a..67ea8562 100644 --- a/platform-api/pkg/zoa/reconciler_test.go +++ b/platform-api/pkg/zoa/reconciler_test.go @@ -19,7 +19,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + hyperfleetv1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" "github.com/openshift-online/rosa-hyperfleet-api/platform-api/pkg/clients/hyperfleetdb" ) diff --git a/test/e2e-sdk/sdk_sanity_test.go b/test/e2e-sdk/sdk_sanity_test.go index 62e23a95..99c24571 100644 --- a/test/e2e-sdk/sdk_sanity_test.go +++ b/test/e2e-sdk/sdk_sanity_test.go @@ -54,7 +54,7 @@ import ( hyperfleet "github.com/openshift-online/rosa-hyperfleet-api/clientset" hfrest "github.com/openshift-online/rosa-hyperfleet-api/clientset/rest" "github.com/openshift-online/rosa-hyperfleet-api/clientset/wrappers" - v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1" awstest "github.com/openshift-online/rosa-hyperfleet-api/test/helpers/aws" hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -303,7 +303,7 @@ var _ = Describe("SDK E2E: cluster and nodepool lifecycle", Ordered, func() { cluster, err := cs.HyperfleetV1alpha1().Clusters(customerAccountID).Create(ctx, &v1alpha1.Cluster{ ObjectMeta: metav1.ObjectMeta{Name: clusterName}, Spec: v1alpha1.ClusterSpec{ - HostedCluster: hypershiftv1beta1.HostedClusterSpec{ + HostedCluster: v1alpha1.HostedClusterSpecPassthrough{ Release: hypershiftv1beta1.Release{Image: version}, Platform: hypershiftv1beta1.PlatformSpec{ Type: hypershiftv1beta1.AWSPlatform, @@ -376,7 +376,7 @@ var _ = Describe("SDK E2E: cluster and nodepool lifecycle", Ordered, func() { np, err := cs.HyperfleetV1alpha1().NodePools(clusterID).Create(ctx, &v1alpha1.NodePool{ ObjectMeta: metav1.ObjectMeta{Name: npName}, Spec: v1alpha1.NodePoolSpec{ - NodePool: hypershiftv1beta1.NodePoolSpec{ + NodePool: v1alpha1.NodePoolSpecPassthrough{ ClusterName: clusterName, Replicas: &initialReplicas, Platform: hypershiftv1beta1.NodePoolPlatform{ @@ -428,7 +428,7 @@ var _ = Describe("SDK E2E: cluster and nodepool lifecycle", Ordered, func() { extraNp, err := cs.HyperfleetV1alpha1().NodePools(clusterID).Create(ctx, &v1alpha1.NodePool{ ObjectMeta: metav1.ObjectMeta{Name: extraNpName}, Spec: v1alpha1.NodePoolSpec{ - NodePool: hypershiftv1beta1.NodePoolSpec{ + NodePool: v1alpha1.NodePoolSpecPassthrough{ ClusterName: clusterName, Replicas: &extraReplicas, Platform: hypershiftv1beta1.NodePoolPlatform{ diff --git a/test/go.mod b/test/go.mod index e2dd11e4..f93256c0 100644 --- a/test/go.mod +++ b/test/go.mod @@ -9,8 +9,8 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/gomega v1.39.1 + github.com/openshift-online/rosa-hyperfleet-api/api v0.0.0 github.com/openshift-online/rosa-hyperfleet-api/clientset 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 k8s.io/apimachinery v0.36.0 ) @@ -75,6 +75,6 @@ require ( ) replace ( + github.com/openshift-online/rosa-hyperfleet-api/api => ../api github.com/openshift-online/rosa-hyperfleet-api/clientset => ../clientset - github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api => ../hyperfleet-operator/api )