diff --git a/CLAUDE.md b/CLAUDE.md index fef70863..c0a50f84 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ Three components: make build # All components make test # All unit tests make lint # golangci-lint v2 across all modules -make verify # go.mod tidiness +make verify-mod # go.mod tidiness make deps # Download and tidy all modules make build-api # Platform API @@ -28,16 +28,19 @@ make test-operator # Operator unit tests make test-hyperfleet-db # FleetDB unit tests make test-operator-int # Operator integration tests (Postgres + DynamoDB) -make manifests # Generate CRDs (controller-gen) -make generate # Generate deepcopy +make generate # Run all code generators (passthrough, deepcopy, registry, CRDs, conversion, clientset, openapi) +make verify # Fail if any generated output is out of date -make codegen # Full codegen pipeline (openapi, passthrough, conversion) +make manifests # Generate CRDs (controller-gen + CEL strip) +make generate-deepcopy # Generate deepcopy methods only +make codegen # Full codegen pipeline (passthrough + registry + verify compile) make generate-clientset # Regenerate typed clientset from CRD types make generate-openapi # Regenerate OpenAPI spec from CRD types make verify-codegen # Verify codegen output is up to date make verify-clientset # Verify clientset matches committed files make verify-openapi # Verify OpenAPI spec is up to date +make verify-mod # Verify go.mod tidiness make test-unit # All unit tests (api, operator, codegen, clientset) make test-integration # Integration tests (fleetdb, operator) @@ -54,7 +57,7 @@ clientset/go.mod ← generated typed K8s client for Hyper hyperfleet-operator/go.mod ← requires: fleetdb, api platform-api/go.mod ← requires: fleetdb, api hack/api-codegen/go.mod ← codegen tools (openapi-gen, crd-variants, conversion-gen) -hack/clientset/cmd/wire-gen/go.mod ← wire generation for clientset +hack/clientset/cmd/bridge-gen/go.mod ← bridge and platform generation for clientset hack/tools/go.mod ← dev tooling dependencies ``` diff --git a/Makefile b/Makefile index f8919563..6c79a3ff 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,8 @@ coverage-api-codegen \ test-e2e test-e2e-api test-e2e-cli test-e2e-platform-monitoring test-e2e-zoa test-e2e-authz test-e2e-sdk \ e2e-authz-infra-up e2e-authz-infra-down e2e-init-db \ - fmt vet verify deps mod-tidy \ - manifests generate generate-clientset verify-clientset setup-envtest \ + fmt vet verify verify-mod deps mod-tidy \ + manifests generate generate-deepcopy 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 \ @@ -37,7 +37,7 @@ TOOLS_BIN_DIR := $(TOOLS_DIR)/bin GOLANGCI_LINT := $(abspath $(TOOLS_BIN_DIR)/golangci-lint) CONTROLLER_GEN := $(abspath $(TOOLS_BIN_DIR)/controller-gen) CLIENT_GEN := $(abspath $(TOOLS_BIN_DIR)/client-gen) -WIRE_GEN := $(abspath $(TOOLS_BIN_DIR)/wire-gen) +BRIDGE_GEN := $(abspath $(TOOLS_BIN_DIR)/bridge-gen) SETUP_ENVTEST := $(abspath $(TOOLS_BIN_DIR)/setup-envtest) GINKGO := $(abspath $(TOOLS_BIN_DIR)/ginkgo) @@ -48,11 +48,11 @@ SDK_INPUT ?= v1alpha1/public SDK_CLIENTSET ?= generated SDK_OUTPUT_DIR ?= $(abspath clientset) SDK_OUTPUT_PKG ?= $(SDK_MODULE)/clientset -WIRE_INPUT_DIR ?= $(abspath api/v1alpha1/public) -WIRE_OUTPUT_DIR ?= $(abspath clientset/transport) -WIRE_OUTPUT_PKG ?= transport -WRAPPERS_OUTPUT_DIR ?= $(abspath clientset/wrappers) -WRAPPERS_OUTPUT_PKG ?= wrappers +BRIDGE_INPUT_DIR ?= $(abspath api/v1alpha1/public) +BRIDGE_OUTPUT_DIR ?= $(abspath clientset/transport) +BRIDGE_OUTPUT_PKG ?= transport +PLATFORM_OUTPUT_DIR ?= $(abspath clientset/platform) +PLATFORM_OUTPUT_PKG ?= platform TYPED_PKG_IMPORT ?= $(SDK_MODULE)/clientset/generated/typed/v1alpha1/public API_PKG_IMPORT ?= $(SDK_MODULE)/api/v1alpha1/public SDK_HEADER_FILE ?= $(abspath hack/clientset/license-boilerplate.go.txt) @@ -74,8 +74,8 @@ $(SETUP_ENVTEST): $(TOOLS_DIR)/go.mod $(CLIENT_GEN): $(TOOLS_DIR)/go.mod cd $(TOOLS_DIR); go build -tags=tools -o $(abspath $(TOOLS_BIN_DIR))/client-gen k8s.io/code-generator/cmd/client-gen -$(WIRE_GEN): hack/clientset/cmd/wire-gen/main.go - cd hack/clientset/cmd/wire-gen && go build -o $(WIRE_GEN) . +$(BRIDGE_GEN): hack/clientset/cmd/bridge-gen/main.go + cd hack/clientset/cmd/bridge-gen && go build -o $(BRIDGE_GEN) . $(GINKGO): $(TOOLS_DIR)/go.mod cd $(TOOLS_DIR); go build -tags=tools -o $(abspath $(TOOLS_BIN_DIR))/ginkgo github.com/onsi/ginkgo/v2/ginkgo @@ -95,7 +95,7 @@ help: @echo "Test:" @echo " test All tests (unit + integration)" @echo " test-unit Unit tests: API + operator + codegen + clientset (no external services)" - @echo " test-clientset Clientset unit tests (transport, wrappers)" + @echo " test-clientset Clientset unit tests (transport, platform)" @echo " test-integration Integration tests: FleetDB + operator (podman)" @echo " test-e2e-authz E2E authz (starts local infra)" @echo " test-e2e-api E2E API" @@ -110,13 +110,15 @@ help: @echo " lint golangci-lint on all modules" @echo " fmt Format Go source" @echo " vet go vet on all modules" - @echo " verify Verify go.mod tidiness" + @echo " verify-mod Verify go.mod tidiness" @echo "" @echo "Code Generation:" - @echo " manifests Generate CRD manifests" - @echo " generate Generate deepcopy methods" - @echo " generate-clientset Generate typed client SDK from CRD types" - @echo " verify-clientset Fail if generated clientset is out of date" + @echo " manifests Generate CRD manifests (controller-gen + CEL strip)" + @echo " generate Run all code generators in one pass" + @echo " generate-deepcopy Generate deepcopy methods only" + @echo " verify Fail if any generated output is out of date" + @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" @@ -222,7 +224,7 @@ test-e2e-zoa: $(GINKGO) --output-dir=$(TEST_OUTPUT_DIR) ./test/e2e-zoa test-e2e-sdk: $(GINKGO) - BASE_URL="$${BASE_URL}" \ + E2E_BASE_URL="$${BASE_URL}" \ E2E_ACCOUNT_ID="$${E2E_ACCOUNT_ID}" \ E2E_CUSTOMER_ACCOUNT_ID="$${E2E_CUSTOMER_ACCOUNT_ID}" \ CUSTOMER_AWS_PROFILE="$${CUSTOMER_AWS_PROFILE}" \ @@ -257,7 +259,7 @@ fmt: cd platform-api && go fmt ./... cd hack/api-codegen && go fmt ./... cd clientset && go fmt ./... - cd hack/clientset/cmd/wire-gen && go fmt ./... + cd hack/clientset/cmd/bridge-gen && go fmt ./... vet: cd hyperfleet-db && go vet ./... @@ -265,7 +267,7 @@ vet: cd platform-api && go vet ./... cd hack/api-codegen && go vet ./... cd clientset && go vet ./... - cd hack/clientset/cmd/wire-gen && go vet ./... + cd hack/clientset/cmd/bridge-gen && go vet ./... lint: $(GOLANGCI_LINT) cd hyperfleet-db && $(GOLANGCI_LINT) run --config ../.golangci.yml --timeout 5m ./... @@ -273,7 +275,7 @@ lint: $(GOLANGCI_LINT) cd platform-api && $(GOLANGCI_LINT) run --config ../.golangci.yml --timeout 5m ./... cd hack/api-codegen && $(GOLANGCI_LINT) run --config ../../.golangci.yml --timeout 5m ./... cd clientset && $(GOLANGCI_LINT) run --config ../.golangci.yml --timeout 5m ./... - cd hack/clientset/cmd/wire-gen && $(GOLANGCI_LINT) run --config $(abspath .golangci.yml) --timeout 5m ./... + cd hack/clientset/cmd/bridge-gen && $(GOLANGCI_LINT) run --config $(abspath .golangci.yml) --timeout 5m ./... # All Go modules in the repo (used by verify and MintMaker/Renovate post-upgrade). override MOD_TIDY_DIRS := hyperfleet-db api hyperfleet-operator platform-api test clientset hack/tools hack/api-codegen @@ -285,7 +287,7 @@ mod-tidy: (cd "$$d" && go mod tidy); \ done -verify: mod-tidy +verify-mod: mod-tidy git diff --exit-code $(MOD_TIDY_FILES) deps: @@ -295,13 +297,17 @@ deps: # ── Code Generation ────────────────────────────────────────────────────── -manifests: $(CONTROLLER_GEN) +CRD_VARIANTS := $(abspath bin/crd-variants) +CRD_BASES_DIR := hyperfleet-operator/config/crd/bases + +manifests: $(CONTROLLER_GEN) build-api-codegen cd hyperfleet-operator && $(CONTROLLER_GEN) crd:allowDangerousTypes=true paths="../api/v1alpha1" output:crd:dir=config/crd/bases + $(CRD_VARIANTS) --strip-passthrough-cel --api-dir api/v1alpha1 --crd-dir $(CRD_BASES_DIR) -generate: $(CONTROLLER_GEN) +generate-deepcopy: $(CONTROLLER_GEN) $(CONTROLLER_GEN) object paths="./api/..." -generate-clientset: $(CLIENT_GEN) $(WIRE_GEN) +generate-clientset: codegen-conversion $(CLIENT_GEN) $(BRIDGE_GEN) cd api && $(CLIENT_GEN) \ --input-base "$(SDK_API_PKG)" \ --input "$(SDK_INPUT)" \ @@ -309,17 +315,17 @@ generate-clientset: $(CLIENT_GEN) $(WIRE_GEN) --output-dir "$(SDK_OUTPUT_DIR)" \ --output-pkg "$(SDK_OUTPUT_PKG)" \ --go-header-file "$(SDK_HEADER_FILE)" - $(WIRE_GEN) \ - --mode mappings \ - --input-dir "$(WIRE_INPUT_DIR)" \ - --output-dir "$(WIRE_OUTPUT_DIR)" \ - --output-pkg "$(WIRE_OUTPUT_PKG)" \ + $(BRIDGE_GEN) \ + --mode bridge \ + --input-dir "$(BRIDGE_INPUT_DIR)" \ + --output-dir "$(BRIDGE_OUTPUT_DIR)" \ + --output-pkg "$(BRIDGE_OUTPUT_PKG)" \ --go-header-file "$(SDK_HEADER_FILE)" - $(WIRE_GEN) \ - --mode wrappers \ - --input-dir "$(WIRE_INPUT_DIR)" \ - --output-dir "$(WRAPPERS_OUTPUT_DIR)" \ - --output-pkg "$(WRAPPERS_OUTPUT_PKG)" \ + $(BRIDGE_GEN) \ + --mode platform \ + --input-dir "$(BRIDGE_INPUT_DIR)" \ + --output-dir "$(PLATFORM_OUTPUT_DIR)" \ + --output-pkg "$(PLATFORM_OUTPUT_PKG)" \ --typed-pkg-import "$(TYPED_PKG_IMPORT)" \ --typed-client-prefix "V1alpha1Public" \ --api-pkg-import "$(API_PKG_IMPORT)" \ @@ -335,7 +341,7 @@ codegen-passthrough: build-api-codegen -output-dir v1alpha1 \ -package v1alpha1 -codegen-registry: generate build-api-codegen +codegen-registry: codegen-passthrough generate-deepcopy build-api-codegen ./bin/marker-scanner \ -input-dirs api/v1alpha1 \ -output-file hack/api-codegen/pkg/registry/field_metadata.go \ @@ -351,6 +357,13 @@ verify-codegen: codegen git diff --exit-code api/v1alpha1/zz_generated.deepcopy.go git diff --exit-code hack/api-codegen/pkg/registry/ +# generate runs all code generators in dependency order. +# codegen-registry already depends on codegen-passthrough and generate-deepcopy, +# so those are transitively covered; they are listed explicitly here for clarity. +generate: codegen-registry manifests codegen-conversion generate-clientset generate-openapi + +verify: verify-codegen verify-conversion verify-clientset verify-openapi verify-mod + 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 @@ -372,7 +385,7 @@ verify-conversion: codegen-conversion 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 +generate-openapi: codegen-conversion cd hack/api-codegen && go build -o ../../bin/openapi-gen ./cmd/openapi-gen ./bin/openapi-gen \ -input-dirs ./api/v1alpha1 \ diff --git a/api/v1alpha1/cluster_types.go b/api/v1alpha1/cluster_types.go index 3673e598..e98c1f83 100644 --- a/api/v1alpha1/cluster_types.go +++ b/api/v1alpha1/cluster_types.go @@ -140,12 +140,12 @@ type PlacementReference struct { // +genclient // +genclient:nonNamespaced -// +wire:field=name,meta=name -// +wire:field=id,meta=uid -// +wire:field=resource_version,meta=resourceVersion -// +wire:field=generation,meta=generation -// +wire:watch=disabled -// +wire:wait +// +bridge:field=name,meta=name +// +bridge:field=id,meta=uid +// +bridge:field=resource_version,meta=resourceVersion +// +bridge:field=generation,meta=generation +// +bridge:watch=disabled +// +bridge:wait // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:scope=Namespaced,shortName=hfc diff --git a/api/v1alpha1/nodepool_types.go b/api/v1alpha1/nodepool_types.go index d7567309..776b204e 100644 --- a/api/v1alpha1/nodepool_types.go +++ b/api/v1alpha1/nodepool_types.go @@ -89,12 +89,13 @@ type NodePoolStatus struct { } // +genclient -// +wire:field=name,meta=name -// +wire:field=id,meta=uid -// +wire:field=resource_version,meta=resourceVersion -// +wire:field=generation,meta=generation -// +wire:watch=disabled -// +wire:wait +// +bridge:field=cluster_id,meta=namespace +// +bridge:field=name,meta=name +// +bridge:field=id,meta=uid +// +bridge:field=resource_version,meta=resourceVersion +// +bridge:field=generation,meta=generation +// +bridge:watch=disabled +// +bridge:wait // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:scope=Namespaced,shortName=hfnp diff --git a/api/v1alpha1/public/cluster_types.go b/api/v1alpha1/public/cluster_types.go index b92adcf4..ef01e378 100644 --- a/api/v1alpha1/public/cluster_types.go +++ b/api/v1alpha1/public/cluster_types.go @@ -15,12 +15,12 @@ import ( // +kubebuilder:subresource:status // +genclient // +genclient:nonNamespaced -// +wire:field=name,meta=name -// +wire:field=id,meta=uid -// +wire:field=resource_version,meta=resourceVersion -// +wire:field=generation,meta=generation -// +wire:watch=disabled -// +wire:wait +// +bridge:field=name,meta=name +// +bridge:field=id,meta=uid +// +bridge:field=resource_version,meta=resourceVersion +// +bridge:field=generation,meta=generation +// +bridge:watch=disabled +// +bridge:wait type Cluster struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/api/v1alpha1/public/nodepool_types.go b/api/v1alpha1/public/nodepool_types.go index 2a753640..b3c94d39 100644 --- a/api/v1alpha1/public/nodepool_types.go +++ b/api/v1alpha1/public/nodepool_types.go @@ -13,12 +13,13 @@ import ( // +kubebuilder:resource:scope=Namespaced // +kubebuilder:subresource:status // +genclient -// +wire:field=name,meta=name -// +wire:field=id,meta=uid -// +wire:field=resource_version,meta=resourceVersion -// +wire:field=generation,meta=generation -// +wire:watch=disabled -// +wire:wait +// +bridge:field=cluster_id,meta=namespace +// +bridge:field=name,meta=name +// +bridge:field=id,meta=uid +// +bridge:field=resource_version,meta=resourceVersion +// +bridge:field=generation,meta=generation +// +bridge:watch=disabled +// +bridge:wait type NodePool struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/api/v1alpha1/public/openapi.yaml b/api/v1alpha1/public/openapi.yaml index 9e7440df..6a72b984 100644 --- a/api/v1alpha1/public/openapi.yaml +++ b/api/v1alpha1/public/openapi.yaml @@ -3033,14 +3033,49 @@ components: additionalProperties: true description: HostedClusterSpecPassthrough mirrors HostedClusterSpec from upstream HyperShift properties: + additionalTrustBundle: + description: additionalTrustBundle is a local reference to a ConfigMap that must have a "ca-bundle.crt" key + type: object + x-kubernetes-map-type: atomic + auditWebhook: + description: auditWebhook contains metadata for configuring an audit webhook endpoint + type: object + x-kubernetes-map-type: atomic autoNode: description: autoNode specifies the configuration for automatic node provisioning and lifecycle management. type: object + autoscaling: + description: autoscaling specifies auto-scaling behavior that applies to all NodePools + 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. + 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: $ref: '#/components/schemas/ClusterConfiguration' + controlPlaneRelease: + description: controlPlaneRelease is like spec.release but only for the components running on the management cluster. + type: object + controllerAvailabilityPolicy: + description: controllerAvailabilityPolicy specifies the availability policy applied to critical control plane components like the Kube API Server. + enum: + - HighlyAvailable + - SingleReplica + type: string + dns: + description: dns specifies the DNS configuration for the hosted cluster ingress. + type: object etcd: description: etcd specifies configuration for the control plane etcd cluster. The type: object @@ -3078,11 +3113,27 @@ components: required: - source type: object - maxItems: 50 type: array + infraID: + description: infraID is a globally unique identifier for the cluster. + type: string + infrastructureAvailabilityPolicy: + 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: description: issuerURL is an OIDC issuer URL which will be used as the issuer in all type: string + kubeAPIServerDNSName: + description: kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + type: string + labels: + additionalProperties: + type: string + description: labels when specified, define what custom labels are added to the hcp pods. + type: object networking: description: networking specifies network configuration for the hosted cluster. type: object @@ -3091,6 +3142,17 @@ components: 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' + nodeSelector: + additionalProperties: + type: string + description: nodeSelector when specified, is propagated to all control plane Deployments and Stateful sets running management side. + type: object + olmCatalogPlacement: + description: olmCatalogPlacement specifies the placement of OLM catalog components. By default, + enum: + - management + - guest + type: string operatorConfiguration: description: operatorConfiguration specifies configuration for individual OCP operators in the cluster. type: object @@ -3100,23 +3162,238 @@ components: platform: description: platform specifies the underlying infrastructure provider for the cluster 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. + type: object + x-kubernetes-map-type: atomic release: description: release specifies the desired OCP release payload for all the hosted cluster components. type: object + secretEncryption: + description: secretEncryption specifies a Kubernetes secret encryption strategy for the + 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 + type: object + x-kubernetes-map-type: atomic + services: + 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. + This includes APIServer;OAuthServer;Konnectivity;Ignition. + If a given service is not present in this list, it will be exposed publicly by default. + properties: + service: + description: |- + service identifies the type of service being published. + It can be APIServer;OAuthServer;Konnectivity;Ignition + OVNSbDb;OIDC are no-op and kept for backward compatibility. + This field is immutable. + enum: + - APIServer + - OAuthServer + - OIDC + - Konnectivity + - Ignition + - OVNSbDb + type: string + servicePublishingStrategy: + description: servicePublishingStrategy specifies how to publish a service endpoint. + properties: + loadBalancer: + description: loadBalancer configures exposing a service using a dedicated LoadBalancer. + properties: + hostname: + description: |- + hostname is the name of the DNS record that will be created pointing to the LoadBalancer and passed through to consumers of the service. + If omitted, the value will be inferred from the corev1.Service Load balancer type .status. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: hostname must be a valid domain name (e.g., example.com) + rule: self.matches('^(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$') + type: object + nodePort: + description: nodePort configures exposing a service using a NodePort. + properties: + address: + description: address is the host/ip that the NodePort service is exposed over. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: address must be a valid hostname, IPv4, or IPv6 address + rule: self.matches('^(([a-zA-Z0-9][-a-zA-Z0-9]*\\.)+[a-zA-Z]{2,}|localhost)$') || self.matches('^((\\d{1,3}\\.){3}\\d{1,3})$') || self.matches('^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$') + port: + description: |- + port is the port of the NodePort service. If <=0, the port is dynamically + assigned when the service is created. + format: int32 + type: integer + required: + - address + type: object + route: + description: |- + route configures exposing a service using a Route through and an ingress controller behind a cloud Load Balancer. + The specifics of the setup are platform dependent. + properties: + hostname: + description: |- + hostname is the name of the DNS record that will be created pointing to the Route and passed through to consumers of the service. + If omitted, the value will be inferred from management ingress.Spec.Domain. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: hostname must be a valid domain name (e.g., example.com) + rule: self.matches('^(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$') + type: object + type: + description: |- + type is the publishing strategy used for the service. + It can be LoadBalancer;NodePort;Route;None;S3 + enum: + - LoadBalancer + - NodePort + - Route + - None + - S3 + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: nodePort is required when type is NodePort, and forbidden otherwise + rule: 'self.type == ''NodePort'' ? has(self.nodePort) : !has(self.nodePort)' + - message: only route is allowed when type is Route, and forbidden otherwise + rule: 'self.type == ''Route'' ? !has(self.nodePort) && !has(self.loadBalancer) : !has(self.route)' + - message: only loadBalancer is required when type is LoadBalancer, and forbidden otherwise + rule: 'self.type == ''LoadBalancer'' ? !has(self.nodePort) && !has(self.route) : !has(self.loadBalancer)' + - message: None does not allowed any configuration for loadBalancer, nodePort, or route + rule: 'self.type == ''None'' ? !has(self.nodePort) && !has(self.route) && !has(self.loadBalancer) : true' + - message: S3 does not allowed any configuration for loadBalancer, nodePort, or route + rule: 'self.type == ''S3'' ? !has(self.nodePort) && !has(self.route) && !has(self.loadBalancer) : true' + required: + - service + - servicePublishingStrategy + type: object + 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. + type: object + x-kubernetes-map-type: atomic + tolerations: + description: tolerations when specified, define what custom tolerations are added to the hcp pods. + 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 + updateService: + description: updateService may be used to specify the preferred upstream update service. + type: string required: - etcd - - fips - networking - platform + - pullSecret - release + - services type: object NodePoolSpecPassthrough: additionalProperties: true description: NodePoolSpecPassthrough mirrors NodePoolSpec from upstream HyperShift properties: + arch: + description: arch is the preferred processor architecture for the NodePool. Different platforms might have different supported architectures. + type: string + autoScaling: + description: autoScaling specifies auto-scaling behavior for the NodePool. + type: object + x-kubernetes-validations: + - 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. type: string + config: + description: config is a list of references to ConfigMaps containing serialized + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + management: + description: management specifies behavior for managing nodes in the pool, such as + type: object + x-kubernetes-validations: + - message: The 'inPlace' field can only be set when 'upgradeType' 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. + type: string + nodeLabels: + additionalProperties: + type: string + description: nodeLabels propagates a list of labels to Nodes, only once on creation. + type: object + nodeVolumeDetachTimeout: + 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. + type: object + 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. + type: string platform: description: platform specifies the underlying infrastructure provider for the NodePool type: object @@ -3127,8 +3404,67 @@ components: 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. + items: + description: |- + taint is as v1 Core but without TimeAdded. + https://github.com/kubernetes/kubernetes/blob/ed8cad1e80d096257921908a52ac69cf1f41a098/staging/src/k8s.io/api/core/v1/types.go#L3037-L3053 + Validation replicates the same validation as the upstream https://github.com/kubernetes/kubernetes/blob/9a2a7537f035969a68e432b4cc276dbce8ce1735/pkg/util/taints/taints.go#L273. + See also https://kubernetes.io/docs/concepts/overview/working-with-objects/names/. + properties: + effect: + description: |- + effect is the effect of the taint on pods + that do not tolerate the taint. + Valid effects are NoSchedule, PreferNoSchedule and NoExecute. + enum: + - NoSchedule + - PreferNoSchedule + - NoExecute + type: string + key: + description: key is the taint key to be applied to a node. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: key must be a qualified name with an optional subdomain prefix e.g. example.com/MyName + rule: self.matches('^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\\/)?[A-Za-z0-9]([-A-Za-z0-9_.]{0,61}[A-Za-z0-9])?$') + value: + description: value is the taint value corresponding to the taint key. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: Value must start and end with alphanumeric characters and can only contain '-', '_', '.' in the middle + rule: self.matches('^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$') + required: + - effect + - key + type: object + type: array + tuningConfig: + description: tuningConfig is a list of references to ConfigMaps containing serialized + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array required: - clusterName + - management - platform - release type: object diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index e3ed3108..4a6cea83 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -376,7 +376,7 @@ func (in *HostedClusterSpecPassthrough) DeepCopyInto(out *HostedClusterSpecPasst } if in.Configuration != nil { in, out := &in.Configuration, &out.Configuration - *out = new(ClusterConfiguration) + *out = new(v1beta1.ClusterConfiguration) (*in).DeepCopyInto(*out) } if in.OperatorConfiguration != nil { diff --git a/api/v1alpha1/hostedclusterspec.passthrough.go b/api/v1alpha1/zz_generated.passthrough.go similarity index 88% rename from api/v1alpha1/hostedclusterspec.passthrough.go rename to api/v1alpha1/zz_generated.passthrough.go index 741f97ce..9144772a 100644 --- a/api/v1alpha1/hostedclusterspec.passthrough.go +++ b/api/v1alpha1/zz_generated.passthrough.go @@ -12,56 +12,69 @@ import ( // 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 + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +required 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 + // +optional 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 + // +optional ClusterID string `json:"clusterID,omitempty"` // infraID is a globally unique identifier for the cluster. // +k8s:openapi-gen=false // +hyperfleet:write-mode=service-set + // +optional 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 + // +optional 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 + // +optional Channel string `json:"channel,omitempty"` // platform specifies the underlying infrastructure provider for the cluster - // +k8s:openapi-gen=true - // +hyperfleet:write-mode=mutable + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +required 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 + // +optional 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 + // +optional 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 + // +optional 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 + // +optional DNS hypershiftv1beta1.DNSSpec `json:"dns,omitempty"` // networking specifies network configuration for the hosted cluster. - // +k8s:openapi-gen=true - // +hyperfleet:write-mode=mutable + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +required Networking hypershiftv1beta1.ClusterNetworking `json:"networking"` // autoscaling specifies auto-scaling behavior that applies to all NodePools // +k8s:openapi-gen=false // +hyperfleet:write-mode=service-set + // +optional Autoscaling hypershiftv1beta1.ClusterAutoscaling `json:"autoscaling,omitempty"` // autoNode specifies the configuration for automatic node provisioning and lifecycle management. // +k8s:openapi-gen=true @@ -69,147 +82,173 @@ type HostedClusterSpecPassthrough struct { // +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 + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +required 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 + // +required 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 + // +required 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 + // +optional SSHKey corev1.LocalObjectReference `json:"sshKey"` // issuerURL is an OIDC issuer URL which will be used as the issuer in all - // +k8s:openapi-gen=true - // +hyperfleet:write-mode=mutable + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +optional 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 + // +optional 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"` + // +optional + Configuration *hypershiftv1beta1.ClusterConfiguration `json:"configuration,omitempty"` // operatorConfiguration specifies configuration for individual OCP operators in the cluster. // +k8s:openapi-gen=true // +hyperfleet:write-mode=service-set + // +optional OperatorConfiguration *hypershiftv1beta1.OperatorConfiguration `json:"operatorConfiguration,omitempty"` // auditWebhook contains metadata for configuring an audit webhook endpoint // +k8s:openapi-gen=false // +hyperfleet:write-mode=service-set + // +optional 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 + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +optional 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 + // +optional AdditionalTrustBundle *corev1.LocalObjectReference `json:"additionalTrustBundle,omitempty"` // secretEncryption specifies a Kubernetes secret encryption strategy for the // +k8s:openapi-gen=false // +hyperfleet:write-mode=service-set + // +optional 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 + // +optional 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 + // +optional PausedUntil *string `json:"pausedUntil,omitempty"` // olmCatalogPlacement specifies the placement of OLM catalog components. By default, // +k8s:openapi-gen=false // +hyperfleet:write-mode=service-set + // +optional 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 + // +optional 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 + // +optional 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 + // +optional 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 + // +optional 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=true - // +hyperfleet:write-mode=mutable + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +required ClusterName string `json:"clusterName"` // release specifies the OCP release used for this NodePool. It drives the machine ignition configuration (including - // +k8s:openapi-gen=true - // +hyperfleet:write-mode=mutable + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +required Release hypershiftv1beta1.Release `json:"release"` // platform specifies the underlying infrastructure provider for the NodePool - // +k8s:openapi-gen=true - // +hyperfleet:write-mode=mutable + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +required 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 + // +k8s:openapi-gen=false + // +hyperfleet:write-mode=service-set + // +optional 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 + // +required Management hypershiftv1beta1.NodePoolManagement `json:"management"` // autoScaling specifies auto-scaling behavior for the NodePool. // +k8s:openapi-gen=false // +hyperfleet:write-mode=service-set + // +optional 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 + // +optional 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 + // +optional 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 + // +optional 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 + // +optional 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 + // +optional 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 + // +optional PausedUntil *string `json:"pausedUntil,omitempty"` // tuningConfig is a list of references to ConfigMaps containing serialized // +k8s:openapi-gen=false // +hyperfleet:write-mode=service-set + // +optional 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 + // +optional Arch string `json:"arch,omitempty"` // osImageStream specifies an OS stream to be used for nodes in this pool. // +k8s:openapi-gen=false diff --git a/clientset/docs/architecture.md b/clientset/docs/architecture.md index 8ec2a39f..9ee710d3 100644 --- a/clientset/docs/architecture.md +++ b/clientset/docs/architecture.md @@ -5,7 +5,7 @@ The SDK provides a typed Go client for the Hyperfleet platform API, using the same interface style as `client-go`: ```go -cs.HyperfleetV1alpha1().Clusters().Get(ctx, "my-cluster", wrappers.GetOptions{}) +cs.HyperfleetV1alpha1().Clusters().Get(ctx, "my-cluster", platform.GetOptions{}) ``` It is built in two parts: @@ -40,18 +40,18 @@ make generate-clientset # regenerate from CRD types make verify-clientset # fail if generated output differs from committed files ``` -`generate-clientset` runs two generators in sequence — `client-gen` for the typed clients and `wire-gen` for field mappings and wrappers: +`generate-clientset` runs two generators in sequence — `client-gen` for the typed clients and `bridge-gen` for field mappings and wrappers: ```makefile SDK_CLIENTSET ?= generated SDK_OUTPUT_DIR ?= $(abspath clientset) SDK_OUTPUT_PKG ?= $(SDK_MODULE)/clientset -WIRE_INPUT_DIR ?= $(abspath api/v1alpha1) -WIRE_OUTPUT_DIR ?= $(abspath clientset/transport) -WRAPPERS_OUTPUT_DIR ?= $(abspath clientset/wrappers) +BRIDGE_INPUT_DIR ?= $(abspath api/v1alpha1) +BRIDGE_OUTPUT_DIR ?= $(abspath clientset/transport) +PLATFORM_OUTPUT_DIR ?= $(abspath clientset/platform) ``` -`wire-gen` is a stdlib-only command built from `hack/clientset/cmd/wire-gen/` with its +`bridge-gen` is a stdlib-only command built from `hack/clientset/cmd/bridge-gen/` with its own `go.mod`. It is compiled automatically as a dependency of the target. ### What gets generated @@ -67,13 +67,13 @@ clientset/generated/ fake/ # fake implementations for testing clientset/transport/ - wire_mappings_generated.go # defaultMappings from +wire:field markers + bridge_mappings_generated.go # defaultMappings from +bridge:field markers -clientset/wrappers/ - wire_wrappers_generated.go # platform-scoped interfaces + WaitUntil from +wire:wait markers +clientset/platform/ + bridge_wrappers_generated.go # platform-scoped interfaces + WaitUntil from +bridge:wait markers ``` -> Files with `_generated.go` suffix are generated by wire-gen. Do not edit them manually. +> Files with `_generated.go` suffix are generated by bridge-gen. Do not edit them manually. > Files under `clientset/generated/` are generated by client-gen. Do not edit them manually. ### install package (hand-written, required by generated code) @@ -106,7 +106,7 @@ The platform API differs from a standard Kubernetes API in three ways that requi |---|---| | Requests are signed with AWS SigV4 | `transport/sigv4.go` — custom RoundTripper | | Resources are account-scoped, not namespace-scoped | SigV4 transport extracts the Kubernetes namespace from the URL, maps it to `X-Amz-Account-Id`, and strips the `/namespaces/{ns}/` segment | -| Wire format is flat JSON, not Kubernetes nested metadata | `transport/wire.go` — request/response adapter | +| Wire format is flat JSON, not Kubernetes nested metadata | `transport/bridge.go` — request/response adapter | ### `rest/config.go` — SDK configuration @@ -130,7 +130,7 @@ Every outbound request goes through `SigV4RoundTripper.RoundTrip`: 2. The request body is buffered, hashed (SHA-256), and restored so SigV4 can include the payload hash in the signature. 3. The request is signed with `aws/signer/v4` against the `execute-api` service. -### `transport/wire.go` — request/response adapter +### `transport/bridge.go` — request/response adapter The `Adapter` RoundTripper handles four transformations: @@ -154,7 +154,7 @@ Both single-object and list (`{"items": [...]}`) responses are handled. **Request rewriting** — the Kubernetes serializer produces nested metadata. The adapter flattens it back to the platform wire format before sending. For namespaced POST requests (e.g. nodepool create), the namespace segment encodes the parent cluster ID; the adapter injects it as `"cluster_id"` in the body before the SigV4 transport strips the namespace from the URL. -**Pagination rewrite** — `wrappers.ListOptions.Offset` is bridged by encoding the integer as a numeric string in `metav1.ListOptions.Continue`. The adapter detects this encoding and rewrites `?continue=N` to `?offset=N` so the platform API receives the parameter it expects. +**Pagination rewrite** — `platform.ListOptions.Offset` is bridged by encoding the integer as a numeric string in `metav1.ListOptions.Continue`. The adapter detects this encoding and rewrites `?continue=N` to `?offset=N` so the platform API receives the parameter it expects. **Error response translation** — platform API errors use a different envelope from `metav1.Status`: @@ -172,9 +172,9 @@ client-go's `transformResponse` cannot parse this format and falls back to `Stat This ensures `k8s.io/apimachinery/pkg/api/errors` helpers (`IsNotFound`, `IsForbidden`, etc.) classify errors correctly and that callers receive the full server message rather than a generic unknown error. -### `wrappers/options.go` — platform-scoped option types +### `platform/options.go` — platform-scoped option types -Rather than exposing `metav1.GetOptions`, `metav1.ListOptions`, etc. (which carry Kubernetes-specific fields the platform API does not honor), the wrappers package defines its own minimal option types: +Rather than exposing `metav1.GetOptions`, `metav1.ListOptions`, etc. (which carry Kubernetes-specific fields the platform API does not honor), the platform package defines its own minimal option types: ```go type GetOptions struct{} @@ -247,18 +247,18 @@ An empty version string causes `metav1.AddToGroupVersion` to panic when register `client-gen` derives the method name from the directory structure. Because the types live directly under `v1alpha1/` with no parent group directory, it generates `V1alpha1()` (not `HyperfleetV1alpha1()`). The `hyperfleet.go` wrapper renames it and wraps it with the generated wrappers client: ```go -func (c *Clientset) HyperfleetV1alpha1() wrappers.V1alpha1Interface { - return wrappers.NewV1alpha1Client(c.generated.V1alpha1()) +func (c *Clientset) HyperfleetV1alpha1() platform.V1alpha1PublicInterface { + return platform.NewV1alpha1PublicClient(c.generated.V1alpha1Public()) } ``` --- -### `wrappers/wire_wrappers_generated.go` — platform interface + WaitUntil +### `platform/bridge_wrappers_generated.go` — platform interface + WaitUntil -The Hyperfleet platform API does not support the Kubernetes watch stream protocol. The `wrappers` package provides generated wrapper types that: +The Hyperfleet platform API does not support the Kubernetes watch stream protocol. The `platform` package provides generated wrapper types that: -1. Expose only the operations the platform API supports, using platform-specific option types from `wrappers/options.go`. +1. Expose only the operations the platform API supports, using platform-specific option types from `platform/options.go`. 2. Route `Update` calls by UID — the wrapper deep-copies the object and sets `Name = UID` before calling the inner client, so the generated client builds the PUT URL with the UID regardless of what the caller has in `metadata.name`. The `name` field in the body is discarded by the server's update DTO. 3. Add `WaitUntil` — a polling-based alternative to Watch that repeatedly calls `Get` and evaluates a caller-supplied condition. @@ -267,7 +267,7 @@ The Hyperfleet platform API does not support the Kubernetes watch stream protoco On each CRD type, a marker drives WaitUntil generation: ```go -// +wire:wait → WaitUntil method is generated +// +bridge:wait → WaitUntil method is generated ``` **`WaitUntil` contract** @@ -313,6 +313,6 @@ err := cs.HyperfleetV1alpha1().Clusters().WaitUntil( ## Testing ```bash -make test-clientset # unit tests: transport, wrappers (no external services) +make test-clientset # unit tests: transport, platform (no external services) make verify-clientset # regenerate and fail if output differs from committed files ``` diff --git a/clientset/hyperfleet.go b/clientset/hyperfleet.go index 565c1d64..1fedf741 100644 --- a/clientset/hyperfleet.go +++ b/clientset/hyperfleet.go @@ -24,7 +24,7 @@ limitations under the License. // AccountID: "123456789012", // AWSConfig: awsCfg, // }) -// cluster, err := cs.HyperfleetV1alpha1().Clusters().Get(ctx, "my-cluster", wrappers.GetOptions{}) +// cluster, err := cs.HyperfleetV1alpha1().Clusters().Get(ctx, "my-cluster", platform.GetOptions{}) package hyperfleet import ( @@ -33,9 +33,9 @@ import ( generatedclientset "github.com/openshift-online/rosa-hyperfleet-api/clientset/generated" "github.com/openshift-online/rosa-hyperfleet-api/clientset/generated/scheme" + "github.com/openshift-online/rosa-hyperfleet-api/clientset/platform" hfrest "github.com/openshift-online/rosa-hyperfleet-api/clientset/rest" "github.com/openshift-online/rosa-hyperfleet-api/clientset/transport" - "github.com/openshift-online/rosa-hyperfleet-api/clientset/wrappers" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" k8srest "k8s.io/client-go/rest" @@ -43,7 +43,7 @@ import ( // Interface is the top-level client interface for the Hyperfleet platform API. type Interface interface { - HyperfleetV1alpha1() wrappers.V1alpha1PublicInterface + HyperfleetV1alpha1() platform.V1alpha1PublicInterface } // Clientset implements Interface. @@ -53,8 +53,8 @@ type Clientset struct { // HyperfleetV1alpha1 returns the typed client for the hyperfleet.io/v1alpha1 group. // Watch is disabled (returns ErrWatchNotSupported); use WaitUntil for polling-based waits. -func (c *Clientset) HyperfleetV1alpha1() wrappers.V1alpha1PublicInterface { - return wrappers.NewV1alpha1PublicClient(c.generated.V1alpha1Public()) +func (c *Clientset) HyperfleetV1alpha1() platform.V1alpha1PublicInterface { + return platform.NewV1alpha1PublicClient(c.generated.V1alpha1Public()) } // NewForConfig creates a Clientset from a Config, wiring AWS SigV4 authentication diff --git a/clientset/wrappers/wire_wrappers_generated.go b/clientset/platform/bridge_wrappers_generated.go similarity index 99% rename from clientset/wrappers/wire_wrappers_generated.go rename to clientset/platform/bridge_wrappers_generated.go index 0e3b3ff4..c3ee0fb9 100644 --- a/clientset/wrappers/wire_wrappers_generated.go +++ b/clientset/platform/bridge_wrappers_generated.go @@ -13,9 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by wire-gen. DO NOT EDIT. +// Code generated by bridge-gen. DO NOT EDIT. -package wrappers +package platform import ( "context" @@ -28,8 +28,8 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" - typedclient "github.com/openshift-online/rosa-hyperfleet-api/clientset/generated/typed/v1alpha1/public" v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1/public" + typedclient "github.com/openshift-online/rosa-hyperfleet-api/clientset/generated/typed/v1alpha1/public" ) // ClusterInterface is the platform-scoped client for Cluster resources. @@ -229,6 +229,7 @@ func (c *nodePoolClient) WaitUntil(ctx context.Context, id string, condition fun } } } + // V1alpha1PublicInterface is the platform-scoped typed client for the hyperfleet.io/v1alpha1 group. type V1alpha1PublicInterface interface { RESTClient() rest.Interface diff --git a/clientset/wrappers/options.go b/clientset/platform/options.go similarity index 98% rename from clientset/wrappers/options.go rename to clientset/platform/options.go index 7bf5b76c..6d822bd9 100644 --- a/clientset/wrappers/options.go +++ b/clientset/platform/options.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package wrappers +package platform // GetOptions configures a single-resource read. // Currently only the default behavior is supported. diff --git a/clientset/wrappers/wrappers_test.go b/clientset/platform/platform_test.go similarity index 99% rename from clientset/wrappers/wrappers_test.go rename to clientset/platform/platform_test.go index 93ae5828..12f5a970 100644 --- a/clientset/wrappers/wrappers_test.go +++ b/clientset/platform/platform_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package wrappers +package platform import ( "context" diff --git a/clientset/transport/wire.go b/clientset/transport/bridge.go similarity index 85% rename from clientset/transport/wire.go rename to clientset/transport/bridge.go index f28224fe..a8f84dac 100644 --- a/clientset/transport/wire.go +++ b/clientset/transport/bridge.go @@ -29,31 +29,45 @@ import ( // FieldMapping declares a correspondence between a platform-api wire-format // field name and a Kubernetes metadata field name. type FieldMapping struct { - Wire string // flat field name in the platform-api response/request body - Meta string // field name inside the Kubernetes metadata object + Bridge string // flat field name in the platform-api response/request body + Meta string // field name inside the Kubernetes metadata object } // Adapter wraps an inner RoundTripper and rewrites platform-api wire format // responses into the Kubernetes metadata structure expected by the generated decoder. type Adapter struct { inner http.RoundTripper - mappings []FieldMapping + mappings map[string][]FieldMapping } // NewAdapter returns an Adapter that wraps inner. Field mappings are loaded -// from defaultMappings, which is generated by wire-gen from +wire:field markers +// from defaultMappings, which is generated by bridge-gen from +bridge:field markers // on the CRD types and must not be configured by callers. func NewAdapter(inner http.RoundTripper) *Adapter { return &Adapter{inner: inner, mappings: defaultMappings} } +// resourceFromPath extracts the resource type from a URL path by scanning +// segments right-to-left and returning the first segment that matches a +// mappings key (e.g. "clusters", "nodepools"). Returns "" if no match. +func resourceFromPath(path string, mappings map[string][]FieldMapping) string { + segments := strings.Split(path, "/") + for i := len(segments) - 1; i >= 0; i-- { + if _, ok := mappings[segments[i]]; ok { + return segments[i] + } + } + return "" +} + // RoundTrip implements http.RoundTripper. It rewrites the request body from // Kubernetes wire format to the platform-api flat format, adjusts pagination // query parameters, forwards the request via the inner transport, then rewrites // the response body back to Kubernetes format. func (a *Adapter) RoundTrip(req *http.Request) (*http.Response, error) { + mappings := a.mappings[resourceFromPath(req.URL.Path, a.mappings)] var err error - req, err = a.adaptRequest(req) + req, err = a.adaptRequest(req, mappings) if err != nil { return nil, err } @@ -62,7 +76,7 @@ func (a *Adapter) RoundTrip(req *http.Request) (*http.Response, error) { if err != nil { return nil, err } - return a.adaptResponse(resp) + return a.adaptResponse(resp, mappings) } // adaptRequest transforms a Kubernetes-format request body into the platform-api @@ -78,7 +92,7 @@ func (a *Adapter) RoundTrip(req *http.Request) (*http.Response, error) { // For namespaced POST requests the namespace segment encodes the parent resource // ID (e.g. clusterID); it is injected as "cluster_id" in the body before the // SigV4 transport strips the namespace from the URL. -func (a *Adapter) adaptRequest(req *http.Request) (*http.Request, error) { +func (a *Adapter) adaptRequest(req *http.Request, mappings []FieldMapping) (*http.Request, error) { if req.Body == nil { return req, nil } @@ -110,9 +124,9 @@ func (a *Adapter) adaptRequest(req *http.Request) (*http.Request, error) { return req, nil } - for _, fm := range a.mappings { + for _, fm := range mappings { if v, ok := meta[fm.Meta]; ok { - raw[fm.Wire] = v + raw[fm.Bridge] = v } } delete(raw, "metadata") @@ -166,7 +180,7 @@ func (a *Adapter) adaptRequest(req *http.Request) (*http.Request, error) { // The Kubernetes runtime decoder populates v1alpha1.Cluster by JSON field name, // so it expects the cluster name in metadata.name, the UUID in metadata.uid, etc. // Fields that have no mapping are preserved as-is (spec, status pass through). -func (a *Adapter) adaptResponse(resp *http.Response) (*http.Response, error) { +func (a *Adapter) adaptResponse(resp *http.Response, mappings []FieldMapping) (*http.Response, error) { if resp.StatusCode < 200 || resp.StatusCode >= 300 { return adaptErrorResponse(resp) } @@ -188,9 +202,13 @@ func (a *Adapter) adaptResponse(resp *http.Response) (*http.Response, error) { var adapted []byte if itemsJSON, ok := raw["items"]; ok { - adapted = a.adaptList(raw, itemsJSON) - } else if a.hasWireField(raw) { - adapted = a.adaptItem(raw) + if len(mappings) == 0 { + adapted = body + } else { + adapted = a.adaptList(raw, itemsJSON, mappings) + } + } else if a.hasWireField(raw, mappings) { + adapted = a.adaptItem(raw, mappings) } else { adapted = body } @@ -294,9 +312,9 @@ func httpStatusToReason(code int) string { // hasWireField reports whether any mapped wire field is present in raw, // used to detect single-object responses. -func (a *Adapter) hasWireField(raw map[string]json.RawMessage) bool { - for _, fm := range a.mappings { - if _, ok := raw[fm.Wire]; ok { +func (a *Adapter) hasWireField(raw map[string]json.RawMessage, mappings []FieldMapping) bool { + for _, fm := range mappings { + if _, ok := raw[fm.Bridge]; ok { return true } } @@ -304,7 +322,7 @@ func (a *Adapter) hasWireField(raw map[string]json.RawMessage) bool { } // adaptList rewrites {"items": [...]} to {"items": [, ...]}. -func (a *Adapter) adaptList(raw map[string]json.RawMessage, itemsJSON json.RawMessage) []byte { +func (a *Adapter) adaptList(raw map[string]json.RawMessage, itemsJSON json.RawMessage, mappings []FieldMapping) []byte { var items []json.RawMessage if err := json.Unmarshal(itemsJSON, &items); err != nil { out, _ := json.Marshal(raw) @@ -318,7 +336,7 @@ func (a *Adapter) adaptList(raw map[string]json.RawMessage, itemsJSON json.RawMe adapted[i] = item continue } - adapted[i] = a.adaptItem(m) + adapted[i] = a.adaptItem(m, mappings) } raw["items"], _ = json.Marshal(adapted) @@ -334,7 +352,7 @@ func (a *Adapter) adaptList(raw map[string]json.RawMessage, itemsJSON json.RawMe // continuation. The Hyperfleet platform API uses offset-based pagination instead: // it has no cursor mechanism and accepts an integer "offset" parameter. // -// The wrappers.ListOptions.Offset field is bridged by encoding the integer offset +// The platform.ListOptions.Offset field is bridged by encoding the integer offset // as a numeric string in ListOptions.Continue before calling the inner client. // This method recognizes that encoding and rewrites the query parameter so the // platform API receives the value it expects. @@ -362,13 +380,13 @@ func (a *Adapter) adaptListQuery(req *http.Request) *http.Request { // adaptItem lifts wire-format envelope fields into metadata using the configured // mappings. All other fields (spec, status, etc.) are preserved unchanged. -func (a *Adapter) adaptItem(m map[string]json.RawMessage) json.RawMessage { +func (a *Adapter) adaptItem(m map[string]json.RawMessage, mappings []FieldMapping) json.RawMessage { meta := make(map[string]json.RawMessage) - for _, fm := range a.mappings { - if v, ok := m[fm.Wire]; ok { + for _, fm := range mappings { + if v, ok := m[fm.Bridge]; ok { meta[fm.Meta] = v - delete(m, fm.Wire) + delete(m, fm.Bridge) } } diff --git a/clientset/transport/bridge_mappings_generated.go b/clientset/transport/bridge_mappings_generated.go new file mode 100644 index 00000000..2005b708 --- /dev/null +++ b/clientset/transport/bridge_mappings_generated.go @@ -0,0 +1,38 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by bridge-gen. DO NOT EDIT. + +package transport + +// defaultMappings maps platform-api wire-format field names to Kubernetes +// metadata field names, keyed by the lowercase plural resource name that +// matches the URL path segment (e.g. "clusters", "nodepools"). +// Generated from +bridge:field markers on CRD types. +var defaultMappings = map[string][]FieldMapping{ + "clusters": { + {Bridge: "generation", Meta: "generation"}, + {Bridge: "id", Meta: "uid"}, + {Bridge: "name", Meta: "name"}, + {Bridge: "resource_version", Meta: "resourceVersion"}, + }, + "nodepools": { + {Bridge: "cluster_id", Meta: "namespace"}, + {Bridge: "generation", Meta: "generation"}, + {Bridge: "id", Meta: "uid"}, + {Bridge: "name", Meta: "name"}, + {Bridge: "resource_version", Meta: "resourceVersion"}, + }, +} diff --git a/clientset/transport/wire_test.go b/clientset/transport/bridge_test.go similarity index 85% rename from clientset/transport/wire_test.go rename to clientset/transport/bridge_test.go index 6f957e7f..e848ca34 100644 --- a/clientset/transport/wire_test.go +++ b/clientset/transport/bridge_test.go @@ -83,9 +83,11 @@ func assertNoField(t *testing.T, m map[string]json.RawMessage, key string) { } // mustAdaptRequest calls adaptRequest and fails the test if it returns an error. +// The resource type is derived from the request URL path. func mustAdaptRequest(t *testing.T, a *Adapter, req *http.Request) *http.Request { t.Helper() - out, err := a.adaptRequest(req) + mappings := a.mappings[resourceFromPath(req.URL.Path, a.mappings)] + out, err := a.adaptRequest(req, mappings) if err != nil { t.Fatalf("adaptRequest: unexpected error: %v", err) } @@ -93,9 +95,11 @@ func mustAdaptRequest(t *testing.T, a *Adapter, req *http.Request) *http.Request } // mustAdaptResponse calls adaptResponse and fails the test if it returns an error. -func mustAdaptResponse(t *testing.T, a *Adapter, resp *http.Response) *http.Response { +// resource is the lowercase plural resource name (e.g. "clusters", "nodepools", or "" +// for tests that do not depend on field mappings). +func mustAdaptResponse(t *testing.T, a *Adapter, resource string, resp *http.Response) *http.Response { t.Helper() - out, err := a.adaptResponse(resp) + out, err := a.adaptResponse(resp, a.mappings[resource]) if err != nil { t.Fatalf("adaptResponse: unexpected error: %v", err) } @@ -238,7 +242,7 @@ func TestAdaptRequest_ReadErrorPropagated(t *testing.T) { req, _ := http.NewRequest(http.MethodPut, "https://example.com/api/v0/clusters/id", io.NopCloser(errReader{err: errors.New("read failure")})) - if _, err := a.adaptRequest(req); err == nil { + if _, err := a.adaptRequest(req, nil); err == nil { t.Error("expected error when body read fails") } } @@ -248,7 +252,7 @@ func TestAdaptRequest_CloseErrorPropagated(t *testing.T) { req, _ := http.NewRequest(http.MethodPut, "https://example.com/api/v0/clusters/id", errCloser{Reader: strings.NewReader(`{"metadata":{"name":"c"},"spec":{}}`), err: errors.New("close failure")}) - if _, err := a.adaptRequest(req); err == nil { + if _, err := a.adaptRequest(req, nil); err == nil { t.Error("expected error when body close fails") } } @@ -264,7 +268,7 @@ func TestAdaptResponse_SingleItemLiftedIntoMetadata(t *testing.T) { Header: make(http.Header), } - out := mustAdaptResponse(t, a, resp) + out := mustAdaptResponse(t, a, "clusters", resp) m := readResponseBody(out) assertNoField(t, m, "id") @@ -289,7 +293,7 @@ func TestAdaptResponse_ListItemsAdapted(t *testing.T) { Header: make(http.Header), } - out := mustAdaptResponse(t, a, resp) + out := mustAdaptResponse(t, a, "clusters", resp) m := readResponseBody(out) var items []map[string]json.RawMessage @@ -335,7 +339,7 @@ func TestAdaptResponse_NonPlatformAPIErrorPassesThrough(t *testing.T) { Header: make(http.Header), } - out := mustAdaptResponse(t, a, resp) + out := mustAdaptResponse(t, a, "", resp) b, err := io.ReadAll(out.Body) if err != nil { t.Fatalf("reading response body: %v", err) @@ -469,7 +473,7 @@ func TestAdaptResponse_PlatformAPIErrorSurfacedAsMetav1Status(t *testing.T) { Header: make(http.Header), } - out := mustAdaptResponse(t, a, resp) + out := mustAdaptResponse(t, a, "", resp) m := readMetav1Status(t, out) assertField(t, m, "kind", `"Status"`) assertField(t, m, "status", `"Failure"`) @@ -495,7 +499,7 @@ func TestAdaptResponse_NoWireFieldsPassesThrough(t *testing.T) { Header: make(http.Header), } - out := mustAdaptResponse(t, a, resp) + out := mustAdaptResponse(t, a, "", resp) b, _ := io.ReadAll(out.Body) if string(b) != original { t.Errorf("body unexpectedly changed: %s", b) @@ -510,7 +514,7 @@ func TestAdaptResponse_MalformedListItemPassesThroughUnchanged(t *testing.T) { Header: make(http.Header), } - out := mustAdaptResponse(t, a, resp) + out := mustAdaptResponse(t, a, "", resp) m := readResponseBody(out) var items []json.RawMessage @@ -520,6 +524,69 @@ func TestAdaptResponse_MalformedListItemPassesThroughUnchanged(t *testing.T) { } } +func TestAdaptResponse_ListWithEmptyMappingsPassesThroughUnchanged(t *testing.T) { + a := newAdapter() + original := `{"items":[{"id":"x","name":"foo","spec":{}}]}` + resp := &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(original)), + Header: make(http.Header), + } + + out := mustAdaptResponse(t, a, "", resp) + b, _ := io.ReadAll(out.Body) + if string(b) != original { + t.Errorf("body unexpectedly changed: %s", b) + } + var m map[string]json.RawMessage + _ = json.Unmarshal(b, &m) + var items []map[string]json.RawMessage + _ = json.Unmarshal(m["items"], &items) + if len(items) != 1 { + t.Fatalf("len(items) = %d, want 1", len(items)) + } + if _, ok := items[0]["metadata"]; ok { + t.Error("metadata injected into list item with empty mappings") + } +} + +func TestAdaptResponse_NodepoolClusterIDMappedToNamespace(t *testing.T) { + a := newAdapter() + resp := &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader( + `{"id":"np-uid","name":"my-np","cluster_id":"cluster-uid","resource_version":"1","generation":1,"spec":{}}`)), + Header: make(http.Header), + } + + out := mustAdaptResponse(t, a, "nodepools", resp) + m := readResponseBody(out) + + var meta map[string]json.RawMessage + _ = json.Unmarshal(m["metadata"], &meta) + assertField(t, meta, "namespace", `"cluster-uid"`) + assertNoField(t, m, "cluster_id") +} + +func TestAdaptResponse_ClusterDoesNotMapClusterIDToNamespace(t *testing.T) { + a := newAdapter() + resp := &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader( + `{"id":"cluster-uid","name":"my-cluster","resource_version":"1","generation":1,"spec":{}}`)), + Header: make(http.Header), + } + + out := mustAdaptResponse(t, a, "clusters", resp) + m := readResponseBody(out) + + var meta map[string]json.RawMessage + _ = json.Unmarshal(m["metadata"], &meta) + if _, ok := meta["namespace"]; ok { + t.Error("clusters mapping should not produce a namespace field") + } +} + func TestAdaptResponse_ReadErrorPropagated(t *testing.T) { a := newAdapter() resp := &http.Response{ @@ -528,7 +595,7 @@ func TestAdaptResponse_ReadErrorPropagated(t *testing.T) { Header: make(http.Header), } - if _, err := a.adaptResponse(resp); err == nil { + if _, err := a.adaptResponse(resp, nil); err == nil { t.Error("expected error when response body read fails") } } @@ -541,7 +608,7 @@ func TestAdaptResponse_CloseErrorPropagated(t *testing.T) { Header: make(http.Header), } - if _, err := a.adaptResponse(resp); err == nil { + if _, err := a.adaptResponse(resp, nil); err == nil { t.Error("expected error when response body close fails") } } diff --git a/clientset/transport/wire_mappings_generated.go b/clientset/transport/wire_mappings_generated.go deleted file mode 100644 index cbec772c..00000000 --- a/clientset/transport/wire_mappings_generated.go +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright 2026. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -// Code generated by wire-gen. DO NOT EDIT. - -package transport - -// defaultMappings maps platform-api wire-format field names to Kubernetes -// metadata field names. Generated from +wire:field markers on CRD types. -var defaultMappings = []FieldMapping{ - {Wire: "generation", Meta: "generation"}, - {Wire: "id", Meta: "uid"}, - {Wire: "name", Meta: "name"}, - {Wire: "resource_version", Meta: "resourceVersion"}, -} diff --git a/docs/api/v2-sdk-initiative.md b/docs/api/v2-sdk-initiative.md index 993e724c..830d425f 100644 --- a/docs/api/v2-sdk-initiative.md +++ b/docs/api/v2-sdk-initiative.md @@ -140,7 +140,7 @@ The v1 SDK is generated from a proprietary metamodel DSL (`ocm-api-model`). The The v2 SDK exposes its generated interface directly — there is no v1-compatibility adapter. Consumers migrate to the new interface (see [Interface Decision](#interface-decision)). -**Generated core**: Auto-generated from the HyperFleet CRD types (`api/v1alpha1/`) using `client-gen` (from `k8s.io/code-generator`), the same tool that generates typed clientsets for any Kubernetes operator. This produces typed verb clients (`Create`, `Get`, `List`, `Update`, `Delete`) that match the CRD types exactly. A custom `wire-gen` tool extends the generated clients with platform-specific behavior (watch suppression, `WaitUntil` polling). +**Generated core**: Auto-generated from the HyperFleet CRD types (`api/v1alpha1/`) using `client-gen` (from `k8s.io/code-generator`), the same tool that generates typed clientsets for any Kubernetes operator. This produces typed verb clients (`Create`, `Get`, `List`, `Update`, `Delete`) that match the CRD types exactly. A custom `bridge-gen` tool extends the generated clients with platform-specific behavior (watch suppression, `WaitUntil` polling). ### SDK Release Cadence and Strategy @@ -152,7 +152,7 @@ In v1, the SDK was released separately from the backend. The v2 api will support ├────────────────────────────────────────────┤ │ Generated Core (from CRD types) │ │ Typed clients, models (client-gen) │ -│ Platform wrappers (wire-gen) │ +│ Platform wrappers (bridge-gen) │ ├────────────────────────────────────────────┤ │ Connection / Auth / Transport │ │ AWS SigV4 auth, retry, logging │ @@ -168,16 +168,16 @@ Go types (api/v1alpha1/*.go) ↓ client-gen (k8s.io/code-generator) ↓ Typed clientset: Create/Get/List/Update/Delete per resource ↓ - ↓ wire-gen --mode=mappings → wire field name → metadata field name mappings - ↓ wire-gen --mode=wrappers → Watch override (ErrWatchNotSupported) + ↓ bridge-gen --mode=bridge → wire field name → metadata field name mappings + ↓ bridge-gen --mode=platform → Watch override (ErrWatchNotSupported) WaitUntil polling helper ``` -Markers in the CRD type comments drive `wire-gen` output: +Markers in the CRD type comments drive `bridge-gen` output: -- `+wire:field=,meta=` — field name mapping (transport layer) -- `+wire:watch=disabled` — suppress Watch; generate an override returning `ErrWatchNotSupported` -- `+wire:wait` — generate `WaitUntil(ctx, id, condition func(*T) bool, interval, timeout)` +- `+bridge:field=,meta=` — field name mapping (transport layer) +- `+bridge:watch=disabled` — suppress Watch; generate an override returning `ErrWatchNotSupported` +- `+bridge:wait` — generate `WaitUntil(ctx, id, condition func(*T) bool, interval, timeout)` The entire pipeline runs as `make generate-clientset`. @@ -333,8 +333,8 @@ Set up the `clientset/` module with: Set up the generation pipeline: - Use `client-gen` to generate typed clientsets from `api/v1alpha1/` CRD types -- Use `wire-gen --mode=mappings` to generate wire↔metadata field name mappings from `+wire:field` markers -- Use `wire-gen --mode=wrappers` to generate Watch overrides and `WaitUntil` polling helpers from `+wire:watch=disabled` / `+wire:wait` markers +- Use `bridge-gen --mode=bridge` to generate wire↔metadata field name mappings from `+bridge:field` markers +- Use `bridge-gen --mode=platform` to generate Watch overrides and `WaitUntil` polling helpers from `+bridge:watch=disabled` / `+bridge:wait` markers - Wire the generated client into the SDK's transport layer (`clientset/transport`) - Expose the wrapped clientset through `clientset/hyperfleet.go` @@ -378,7 +378,7 @@ The rosa CLI supports **both** SDKs side by side — v1 (`ocm-sdk-go`) remains t ## Decisions Made -1. **Generation approach**: CRD-types-first. Drop the proprietary OCM metamodel DSL. Generate the v2 SDK directly from the CRD type definitions using `client-gen` (standard Kubernetes tooling) plus a custom `wire-gen` for platform-specific extensions (watch suppression, `WaitUntil` polling). This avoids the OpenAPI intermediary step and keeps generation aligned with the operator's type definitions as the single source of truth. +1. **Generation approach**: CRD-types-first. Drop the proprietary OCM metamodel DSL. Generate the v2 SDK directly from the CRD type definitions using `client-gen` (standard Kubernetes tooling) plus a custom `bridge-gen` for platform-specific extensions (watch suppression, `WaitUntil` polling). This avoids the OpenAPI intermediary step and keeps generation aligned with the operator's type definitions as the single source of truth. 2. **Auth model**: AWS SigV4 (IAM auth), not OCM SSO tokens. The HyperFleet API authenticates all requests via AWS IAM credentials. 3. **Initial surface**: Cluster + NodePool only. Tenancy and authz (account linking, policies, attachments, authorization check) are deferred to a future iteration, along with access transparency, service logs, etc. 4. **Interface style**: Kubernetes-style, modeled on `client-go` — typed resource structs (`ObjectMeta`/`Spec`/`Status`) constructed as struct literals, and a typed client exposing `Create`/`Get`/`List`/`Update`/`Patch`/`Delete` verbs. No fluent builders. diff --git a/hack/api-codegen/cmd/crd-variants/main.go b/hack/api-codegen/cmd/crd-variants/main.go index a18118d2..bdba77d2 100644 --- a/hack/api-codegen/cmd/crd-variants/main.go +++ b/hack/api-codegen/cmd/crd-variants/main.go @@ -5,20 +5,49 @@ import ( "fmt" "log" "os" + "strings" "github.com/openshift-online/rosa-hyperfleet-api/hack/api-codegen/pkg/featuregate" ) func main() { var ( - inputFile = flag.String("input", "", "Input CRD YAML file") - outputDir = flag.String("output-dir", "config/crd/variants", "Output directory for CRD variants") - baseName = flag.String("base-name", "", "Base name for output files (e.g., 'cluster' produces cluster_default.yaml)") - featureSet = flag.String("feature-set", "", "Generate only one feature set variant (default, techpreview, devpreview)") + inputFile = flag.String("input", "", "Input CRD YAML file") + outputDir = flag.String("output-dir", "config/crd/variants", "Output directory for CRD variants") + baseName = flag.String("base-name", "", "Base name for output files (e.g., 'cluster' produces cluster_default.yaml)") + featureSet = flag.String("feature-set", "", "Generate only one feature set variant (default, techpreview, devpreview)") + stripPassthroughCEL = flag.Bool("strip-passthrough-cel", false, "Strip x-kubernetes-validations from passthrough subtrees detected in --api-dir") + apiDir = flag.String("api-dir", "", "Go source directory to scan for passthrough types (used with --strip-passthrough-cel)") + crdDir = flag.String("crd-dir", "", "Directory containing CRD YAML files (used with --strip-passthrough-cel)") ) flag.Parse() + // --strip-passthrough-cel: auto-detect passthrough fields and strip in-place. + if *stripPassthroughCEL { + if *apiDir == "" || *crdDir == "" { + fmt.Fprintln(os.Stderr, "Error: --api-dir and --crd-dir are required with --strip-passthrough-cel") + flag.Usage() + os.Exit(1) + } + targets, err := featuregate.DetectPassthroughTargets(*apiDir, *crdDir) + if err != nil { + log.Fatalf("Detecting passthrough targets: %v", err) + } + if len(targets) == 0 { + fmt.Println("No passthrough fields detected — nothing to strip.") + return + } + for _, t := range targets { + if err := featuregate.StripCELFromSubtrees(t.CRDFile, t.Paths); err != nil { + log.Fatalf("Stripping CEL from %s: %v", t.CRDFile, err) + } + fmt.Printf("Stripped x-kubernetes-validations from %s in %s\n", + strings.Join(t.Paths, ", "), t.CRDFile) + } + return + } + if *inputFile == "" { fmt.Fprintln(os.Stderr, "Error: --input is required") flag.Usage() diff --git a/hack/api-codegen/pkg/conversion/generator.go b/hack/api-codegen/pkg/conversion/generator.go index eabe7a6f..2d16f7fe 100644 --- a/hack/api-codegen/pkg/conversion/generator.go +++ b/hack/api-codegen/pkg/conversion/generator.go @@ -18,7 +18,7 @@ import ( "github.com/openshift-online/rosa-hyperfleet-api/hack/api-codegen/pkg/registry" ) -var clientgenMarkerRE = regexp.MustCompile(`\+genclient\b|\+wire:`) +var clientgenMarkerRE = regexp.MustCompile(`\+genclient\b|\+bridge:`) // Generator generates REST types and conversion functions from CRD types. type Generator struct { @@ -240,10 +240,10 @@ func (g *Generator) parseTypes() error { } // extractClientMarkers scans all comment groups in a file for +genclient and -// +wire:* markers that appear in floating comment blocks (separated by a blank +// +bridge:* markers that appear in floating comment blocks (separated by a blank // line from the type's doc comment). It associates each marker set with the // nearest following type declaration, mirroring the convention used by -// client-gen and wire-gen. +// client-gen and bridge-gen. func (g *Generator) extractClientMarkers(file *ast.File) { type typePos struct { name string diff --git a/hack/api-codegen/pkg/featuregate/crd_strip_cel.go b/hack/api-codegen/pkg/featuregate/crd_strip_cel.go new file mode 100644 index 00000000..c42d092a --- /dev/null +++ b/hack/api-codegen/pkg/featuregate/crd_strip_cel.go @@ -0,0 +1,164 @@ +package featuregate + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" +) + +// StripCELFromSubtrees reads a CRD YAML file, removes all x-kubernetes-validations +// keys from within each named dot-separated field path, and writes the result back +// in-place. Paths are relative to openAPIV3Schema (e.g. "spec.hostedCluster"). +func StripCELFromSubtrees(crdPath string, fieldPaths []string) error { + data, err := os.ReadFile(crdPath) + if err != nil { + return fmt.Errorf("reading CRD: %w", err) + } + + var doc yaml.Node + if err := yaml.Unmarshal(data, &doc); err != nil { + return fmt.Errorf("parsing YAML: %w", err) + } + + for _, path := range fieldPaths { + segments := strings.Split(path, ".") + stripCELAtPath(&doc, segments) + } + + tmp, err := os.CreateTemp(filepath.Dir(crdPath), ".strip-cel-*.yaml") + if err != nil { + return fmt.Errorf("creating temp file: %w", err) + } + tmpName := tmp.Name() + + enc := yaml.NewEncoder(tmp) + enc.SetIndent(2) + if err := enc.Encode(&doc); err != nil { + tmp.Close() + os.Remove(tmpName) + return fmt.Errorf("writing YAML: %w", err) + } + if err := enc.Close(); err != nil { + tmp.Close() + os.Remove(tmpName) + return fmt.Errorf("closing encoder: %w", err) + } + if err := tmp.Close(); err != nil { + os.Remove(tmpName) + return fmt.Errorf("closing temp file: %w", err) + } + if err := os.Rename(tmpName, crdPath); err != nil { + os.Remove(tmpName) + return fmt.Errorf("renaming temp file: %w", err) + } + + return nil +} + +// stripCELAtPath navigates to the subtree at the given field path segments +// (relative to openAPIV3Schema) and recursively removes x-kubernetes-validations. +// It handles both single-version and multi-version CRDs by traversing the +// spec.versions[*].schema.openAPIV3Schema prefix automatically. +func stripCELAtPath(doc *yaml.Node, segments []string) { + // Walk every version's schema — the generator produces one version but + // the function handles multiple to be safe. + versions := findSchemaNodes(doc) + for _, schema := range versions { + target := navigateTo(schema, segments) + if target != nil { + stripCELRecursive(target) + } + } +} + +// findSchemaNodes returns the openAPIV3Schema mapping node for each version. +func findSchemaNodes(doc *yaml.Node) []*yaml.Node { + // spec.versions[*].schema.openAPIV3Schema + spec := mappingChild(doc, "spec") + if spec == nil { + return nil + } + versionsSeq := mappingChild(spec, "versions") + if versionsSeq == nil || versionsSeq.Kind != yaml.SequenceNode { + return nil + } + var schemas []*yaml.Node + for _, ver := range versionsSeq.Content { + schema := mappingChild(ver, "schema") + if schema == nil { + continue + } + openAPI := mappingChild(schema, "openAPIV3Schema") + if openAPI != nil { + schemas = append(schemas, openAPI) + } + } + return schemas +} + +// navigateTo descends through "properties" wrappers following the segment path. +// Each segment steps into the "properties" map of the current node. +func navigateTo(node *yaml.Node, segments []string) *yaml.Node { + cur := node + for _, seg := range segments { + props := mappingChild(cur, "properties") + if props == nil { + return nil + } + cur = mappingChild(props, seg) + if cur == nil { + return nil + } + } + return cur +} + +// stripCELRecursive removes x-kubernetes-validations from node and all descendants. +func stripCELRecursive(node *yaml.Node) { + if node == nil { + return + } + if node.Kind == yaml.MappingNode { + newContent := make([]*yaml.Node, 0, len(node.Content)) + for i := 0; i < len(node.Content); i += 2 { + if i+1 >= len(node.Content) { + break + } + key := node.Content[i] + val := node.Content[i+1] + if key.Value == "x-kubernetes-validations" { + continue + } + stripCELRecursive(val) + newContent = append(newContent, key, val) + } + node.Content = newContent + return + } + for _, child := range node.Content { + stripCELRecursive(child) + } +} + +// mappingChild returns the value node for key in a YAML mapping node, or nil. +func mappingChild(node *yaml.Node, key string) *yaml.Node { + if node == nil { + return nil + } + // Unwrap document node + if node.Kind == yaml.DocumentNode && len(node.Content) > 0 { + return mappingChild(node.Content[0], key) + } + if node.Kind != yaml.MappingNode { + return nil + } + for i := 0; i+1 < len(node.Content); i += 2 { + if node.Content[i].Value == key { + return node.Content[i+1] + } + } + return nil +} diff --git a/hack/api-codegen/pkg/featuregate/detect_passthrough.go b/hack/api-codegen/pkg/featuregate/detect_passthrough.go new file mode 100644 index 00000000..efd79347 --- /dev/null +++ b/hack/api-codegen/pkg/featuregate/detect_passthrough.go @@ -0,0 +1,221 @@ +package featuregate + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "reflect" + "strings" +) + +// PassthroughTarget describes a CRD file and the schema paths within it that +// contain embedded HyperShift passthrough types and should have their +// x-kubernetes-validations stripped. +type PassthroughTarget struct { + // CRDFile is the absolute path to the CRD YAML file. + CRDFile string + // Paths are dot-separated schema paths relative to openAPIV3Schema + // (e.g. "spec.hostedCluster"). + Paths []string +} + +// DetectPassthroughTargets scans the Go source files in apiDir for root CRD +// types (marked +kubebuilder:object:root=true) whose Spec structs contain +// fields typed with a name ending in "Passthrough". For each such field it +// locates the corresponding CRD file in crdDir and records the schema path. +// +// CRD files are matched by the lowercase-plural of the root type name +// (e.g. Cluster → clusters → *_clusters.yaml). +func DetectPassthroughTargets(apiDir, crdDir string) ([]PassthroughTarget, error) { + fset := token.NewFileSet() + parsedFiles, err := parseGoFilesInDir(fset, apiDir) + if err != nil { + return nil, err + } + + // Collect all struct types by name and their position in the file. + type structEntry struct { + st *ast.StructType + pos token.Pos + } + structs := make(map[string]structEntry) + + // rootTypes: names of types annotated with +kubebuilder:object:root=true. + // Detected by finding comment groups containing the marker, then mapping + // them to the nearest following type declaration in the same file. + rootTypes := make(map[string]bool) + + for _, file := range parsedFiles { + // Build a sorted list of (commentGroupEndPos, markerPresent) for this file. + // We only care about comment groups that contain the root marker. + var markerGroupEnds []token.Pos + for _, cg := range file.Comments { + if hasMarker(cg, "+kubebuilder:object:root=true") { + markerGroupEnds = append(markerGroupEnds, cg.End()) + } + } + + for _, decl := range file.Decls { + gd, ok := decl.(*ast.GenDecl) + if !ok || gd.Tok != token.TYPE { + continue + } + for _, spec := range gd.Specs { + ts, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + st, ok := ts.Type.(*ast.StructType) + if !ok { + continue + } + structs[ts.Name.Name] = structEntry{st: st, pos: gd.Pos()} + + // A type is a root type if any marker comment group in the + // same file ends before this declaration starts. We use a + // generous window: any marker group that ends within 50 lines + // before the type declaration is considered associated with it. + declLine := fset.Position(gd.Pos()).Line + for _, end := range markerGroupEnds { + endLine := fset.Position(end).Line + if endLine < declLine && declLine-endLine <= 50 { + rootTypes[ts.Name.Name] = true + break + } + } + } + } + } + + // For each root type, inspect its Spec struct for Passthrough fields. + type hit struct { + plural string // CRD plural name (e.g. "clusters") + jsonTag string // JSON field name (e.g. "hostedCluster") + } + var hits []hit + + for typeName := range rootTypes { + specName := typeName + "Spec" + entry, ok := structs[specName] + if !ok { + continue + } + plural := strings.ToLower(typeName) + "s" + for _, field := range entry.st.Fields.List { + typStr := typeString(field.Type) + if !strings.HasSuffix(typStr, "Passthrough") { + continue + } + tag := jsonTag(field) + if tag == "" || tag == "-" { + continue + } + hits = append(hits, hit{plural: plural, jsonTag: tag}) + } + } + + if len(hits) == 0 { + return nil, nil + } + + // Resolve each hit to an actual CRD file in crdDir. + entries, err := os.ReadDir(crdDir) + if err != nil { + return nil, fmt.Errorf("reading CRD dir %s: %w", crdDir, err) + } + + // Index CRD files by their plural suffix: "clusters" → full path. + crdByPlural := make(map[string]string) + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".yaml") { + continue + } + // File names look like hyperfleet.io_clusters.yaml + parts := strings.SplitN(strings.TrimSuffix(e.Name(), ".yaml"), "_", 2) + if len(parts) == 2 { + crdByPlural[parts[1]] = filepath.Join(crdDir, e.Name()) + } + } + + // Group hits by CRD file. + byFile := make(map[string][]string) + for _, h := range hits { + crdFile, ok := crdByPlural[h.plural] + if !ok { + return nil, fmt.Errorf("no CRD file found for plural %q in %s", h.plural, crdDir) + } + byFile[crdFile] = append(byFile[crdFile], "spec."+h.jsonTag) + } + + var targets []PassthroughTarget + for file, paths := range byFile { + targets = append(targets, PassthroughTarget{CRDFile: file, Paths: paths}) + } + return targets, nil +} + +// parseGoFilesInDir parses all non-test Go source files in dir. +func parseGoFilesInDir(fset *token.FileSet, dir string) ([]*ast.File, error) { + entries, err := os.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", dir, err) + } + var files []*ast.File + for _, entry := range entries { + name := entry.Name() + if entry.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { + continue + } + f, err := parser.ParseFile(fset, filepath.Join(dir, name), nil, parser.ParseComments) + if err != nil { + return nil, fmt.Errorf("parsing %s: %w", name, err) + } + files = append(files, f) + } + return files, nil +} + +// hasMarker reports whether the comment group contains the given marker text. +func hasMarker(cg *ast.CommentGroup, marker string) bool { + if cg == nil { + return false + } + for _, c := range cg.List { + if strings.Contains(c.Text, marker) { + return true + } + } + return false +} + +// typeString returns the base type name from a field type expression, +// stripping any pointer or selector qualifier. +func typeString(expr ast.Expr) string { + switch t := expr.(type) { + case *ast.Ident: + return t.Name + case *ast.StarExpr: + return typeString(t.X) + case *ast.SelectorExpr: + return typeString(t.Sel) + case *ast.ArrayType: + return typeString(t.Elt) + } + return "" +} + +// jsonTag extracts the first comma-separated segment of the "json" struct tag. +func jsonTag(field *ast.Field) string { + if field.Tag == nil { + return "" + } + raw := strings.Trim(field.Tag.Value, "`") + tag := reflect.StructTag(raw).Get("json") + if tag == "" { + return "" + } + return strings.SplitN(tag, ",", 2)[0] +} diff --git a/hack/api-codegen/pkg/passthrough/loader.go b/hack/api-codegen/pkg/passthrough/loader.go index 67a2c138..1b8fe279 100644 --- a/hack/api-codegen/pkg/passthrough/loader.go +++ b/hack/api-codegen/pkg/passthrough/loader.go @@ -109,6 +109,13 @@ func (g *Generator) GenerateTypeDef(typeName string) (*TypeDef, error) { return typeDef, nil } +// upstreamForwardedMarkerPrefixes lists the Go marker prefixes from upstream +// source comments that should be propagated into the generated passthrough type. +var upstreamForwardedMarkerPrefixes = []string{ + "+optional", + "+required", +} + // createFieldDef creates a field definition with appropriate markers func (g *Generator) createFieldDef(fieldName string, field *ast.Field) FieldDef { fieldDef := FieldDef{ @@ -124,13 +131,18 @@ func (g *Generator) createFieldDef(fieldName string, field *ast.Field) FieldDef } } - // Extract documentation (first line only, collapsed to single line) + // Extract documentation and forwarded upstream Go markers. + // Only marker lines matching upstreamForwardedMarkerPrefixes are kept; + // the first non-marker, non-empty line becomes the field description. + var upstreamMarkers []string if field.Doc != nil { - doc := strings.TrimSpace(field.Doc.Text()) - // Take only first line and collapse to single line - lines := strings.Split(doc, "\n") - if len(lines) > 0 { - fieldDef.Doc = strings.TrimSpace(lines[0]) + for _, comment := range field.Doc.List { + text := strings.TrimSpace(strings.TrimPrefix(comment.Text, "//")) + if isForwardedMarker(text) { + upstreamMarkers = append(upstreamMarkers, text) + } else if fieldDef.Doc == "" && text != "" && !strings.HasPrefix(text, "+") { + fieldDef.Doc = text + } } } @@ -143,11 +155,22 @@ func (g *Generator) createFieldDef(fieldName string, field *ast.Field) FieldDef if lookupName == "" { lookupName = fieldName } - fieldDef.Markers = g.getMarkersForField(lookupName) + fieldDef.Markers = append(g.getMarkersForField(lookupName), upstreamMarkers...) return fieldDef } +// isForwardedMarker reports whether an upstream marker should be propagated +// into the generated passthrough type. +func isForwardedMarker(marker string) bool { + for _, prefix := range upstreamForwardedMarkerPrefixes { + if strings.HasPrefix(marker, prefix) { + return true + } + } + return false +} + // typeToString converts an AST type expression to a string func (g *Generator) typeToString(expr ast.Expr) string { switch t := expr.(type) { diff --git a/hack/api-codegen/pkg/registry/field_metadata.go b/hack/api-codegen/pkg/registry/field_metadata.go index c0ca523d..18583288 100644 --- a/hack/api-codegen/pkg/registry/field_metadata.go +++ b/hack/api-codegen/pkg/registry/field_metadata.go @@ -321,347 +321,6 @@ var FieldRegistry = map[string]FieldMeta{ FieldPath: "spec.expirationTimestamp", WriteMode: Mutable, }, - "spec.hostedCluster.additionalTrustBundle": { - FieldPath: "spec.hostedCluster.additionalTrustBundle", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.auditWebhook": { - FieldPath: "spec.hostedCluster.auditWebhook", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.autoNode": { - FieldPath: "spec.hostedCluster.autoNode", - WriteMode: ServiceSet, - }, - "spec.hostedCluster.autoscaling": { - FieldPath: "spec.hostedCluster.autoscaling", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.capabilities": { - FieldPath: "spec.hostedCluster.capabilities", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.channel": { - FieldPath: "spec.hostedCluster.channel", - WriteMode: ServiceSet, - }, - "spec.hostedCluster.clusterID": { - FieldPath: "spec.hostedCluster.clusterID", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.configuration": { - FieldPath: "spec.hostedCluster.configuration", - WriteMode: ServiceSet, - }, - "spec.hostedCluster.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, - Hidden: true, - }, - "spec.hostedCluster.controllerAvailabilityPolicy": { - FieldPath: "spec.hostedCluster.controllerAvailabilityPolicy", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.dns": { - FieldPath: "spec.hostedCluster.dns", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.etcd": { - FieldPath: "spec.hostedCluster.etcd", - WriteMode: Mutable, - }, - "spec.hostedCluster.fips": { - FieldPath: "spec.hostedCluster.fips", - WriteMode: ServiceSet, - }, - "spec.hostedCluster.imageContentSources": { - FieldPath: "spec.hostedCluster.imageContentSources", - WriteMode: Mutable, - }, - "spec.hostedCluster.infraID": { - FieldPath: "spec.hostedCluster.infraID", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.infrastructureAvailabilityPolicy": { - FieldPath: "spec.hostedCluster.infrastructureAvailabilityPolicy", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.issuerURL": { - FieldPath: "spec.hostedCluster.issuerURL", - WriteMode: Mutable, - }, - "spec.hostedCluster.kubeAPIServerDNSName": { - FieldPath: "spec.hostedCluster.kubeAPIServerDNSName", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.labels": { - FieldPath: "spec.hostedCluster.labels", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.networking": { - FieldPath: "spec.hostedCluster.networking", - WriteMode: Mutable, - }, - "spec.hostedCluster.nodeSelector": { - FieldPath: "spec.hostedCluster.nodeSelector", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.olmCatalogPlacement": { - FieldPath: "spec.hostedCluster.olmCatalogPlacement", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.operatorConfiguration": { - FieldPath: "spec.hostedCluster.operatorConfiguration", - WriteMode: ServiceSet, - }, - "spec.hostedCluster.pausedUntil": { - FieldPath: "spec.hostedCluster.pausedUntil", - WriteMode: ServiceSet, - }, - "spec.hostedCluster.platform": { - FieldPath: "spec.hostedCluster.platform", - WriteMode: Mutable, - }, - "spec.hostedCluster.pullSecret": { - FieldPath: "spec.hostedCluster.pullSecret", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.release": { - FieldPath: "spec.hostedCluster.release", - WriteMode: Mutable, - }, - "spec.hostedCluster.secretEncryption": { - FieldPath: "spec.hostedCluster.secretEncryption", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.serviceAccountSigningKey": { - FieldPath: "spec.hostedCluster.serviceAccountSigningKey", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.services": { - FieldPath: "spec.hostedCluster.services", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.sshKey": { - FieldPath: "spec.hostedCluster.sshKey", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.tolerations": { - FieldPath: "spec.hostedCluster.tolerations", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.hostedCluster.updateService": { - FieldPath: "spec.hostedCluster.updateService", - WriteMode: ServiceSet, - Hidden: true, - }, "spec.internalId": { FieldPath: "spec.internalId", WriteMode: ServiceSet, @@ -676,77 +335,6 @@ var FieldRegistry = map[string]FieldMeta{ FieldPath: "spec.labels", WriteMode: Mutable, }, - "spec.nodePool.arch": { - FieldPath: "spec.nodePool.arch", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.autoScaling": { - FieldPath: "spec.nodePool.autoScaling", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.clusterName": { - FieldPath: "spec.nodePool.clusterName", - WriteMode: Mutable, - }, - "spec.nodePool.config": { - FieldPath: "spec.nodePool.config", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.management": { - FieldPath: "spec.nodePool.management", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.nodeDrainTimeout": { - FieldPath: "spec.nodePool.nodeDrainTimeout", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.nodeLabels": { - FieldPath: "spec.nodePool.nodeLabels", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.nodeVolumeDetachTimeout": { - FieldPath: "spec.nodePool.nodeVolumeDetachTimeout", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.osImageStream": { - FieldPath: "spec.nodePool.osImageStream", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.pausedUntil": { - FieldPath: "spec.nodePool.pausedUntil", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.platform": { - FieldPath: "spec.nodePool.platform", - WriteMode: Mutable, - }, - "spec.nodePool.release": { - FieldPath: "spec.nodePool.release", - WriteMode: Mutable, - }, - "spec.nodePool.replicas": { - FieldPath: "spec.nodePool.replicas", - WriteMode: Mutable, - }, - "spec.nodePool.taints": { - FieldPath: "spec.nodePool.taints", - WriteMode: ServiceSet, - Hidden: true, - }, - "spec.nodePool.tuningConfig": { - FieldPath: "spec.nodePool.tuningConfig", - WriteMode: ServiceSet, - Hidden: true, - }, "spec.properties": { FieldPath: "spec.properties", WriteMode: Mutable, diff --git a/hack/api-codegen/pkg/registry/field_metadata.json b/hack/api-codegen/pkg/registry/field_metadata.json index 43aa0ae1..d83072b8 100644 --- a/hack/api-codegen/pkg/registry/field_metadata.json +++ b/hack/api-codegen/pkg/registry/field_metadata.json @@ -300,347 +300,6 @@ "fieldPath": "spec.expirationTimestamp", "writeMode": "mutable" }, - { - "fieldPath": "spec.hostedCluster.additionalTrustBundle", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.auditWebhook", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.autoNode", - "writeMode": "service-set" - }, - { - "fieldPath": "spec.hostedCluster.autoscaling", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.capabilities", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.channel", - "writeMode": "service-set" - }, - { - "fieldPath": "spec.hostedCluster.clusterID", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.configuration", - "writeMode": "service-set" - }, - { - "fieldPath": "spec.hostedCluster.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", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.controllerAvailabilityPolicy", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.dns", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.etcd", - "writeMode": "mutable" - }, - { - "fieldPath": "spec.hostedCluster.fips", - "writeMode": "service-set" - }, - { - "fieldPath": "spec.hostedCluster.imageContentSources", - "writeMode": "mutable" - }, - { - "fieldPath": "spec.hostedCluster.infraID", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.infrastructureAvailabilityPolicy", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.issuerURL", - "writeMode": "mutable" - }, - { - "fieldPath": "spec.hostedCluster.kubeAPIServerDNSName", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.labels", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.networking", - "writeMode": "mutable" - }, - { - "fieldPath": "spec.hostedCluster.nodeSelector", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.olmCatalogPlacement", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.operatorConfiguration", - "writeMode": "service-set" - }, - { - "fieldPath": "spec.hostedCluster.pausedUntil", - "writeMode": "service-set" - }, - { - "fieldPath": "spec.hostedCluster.platform", - "writeMode": "mutable" - }, - { - "fieldPath": "spec.hostedCluster.pullSecret", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.release", - "writeMode": "mutable" - }, - { - "fieldPath": "spec.hostedCluster.secretEncryption", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.serviceAccountSigningKey", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.services", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.sshKey", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.tolerations", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.hostedCluster.updateService", - "writeMode": "service-set", - "hidden": true - }, { "fieldPath": "spec.internalId", "writeMode": "service-set", @@ -655,77 +314,6 @@ "fieldPath": "spec.labels", "writeMode": "mutable" }, - { - "fieldPath": "spec.nodePool.arch", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.autoScaling", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.clusterName", - "writeMode": "mutable" - }, - { - "fieldPath": "spec.nodePool.config", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.management", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.nodeDrainTimeout", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.nodeLabels", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.nodeVolumeDetachTimeout", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.osImageStream", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.pausedUntil", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.platform", - "writeMode": "mutable" - }, - { - "fieldPath": "spec.nodePool.release", - "writeMode": "mutable" - }, - { - "fieldPath": "spec.nodePool.replicas", - "writeMode": "mutable" - }, - { - "fieldPath": "spec.nodePool.taints", - "writeMode": "service-set", - "hidden": true - }, - { - "fieldPath": "spec.nodePool.tuningConfig", - "writeMode": "service-set", - "hidden": true - }, { "fieldPath": "spec.properties", "writeMode": "mutable" diff --git a/hack/clientset/cmd/wire-gen/go.mod b/hack/clientset/cmd/bridge-gen/go.mod similarity index 86% rename from hack/clientset/cmd/wire-gen/go.mod rename to hack/clientset/cmd/bridge-gen/go.mod index daf710da..4e252692 100644 --- a/hack/clientset/cmd/wire-gen/go.mod +++ b/hack/clientset/cmd/bridge-gen/go.mod @@ -1,3 +1,3 @@ -module github.com/openshift-online/rosa-hyperfleet-api/hack/cmd/wire-gen +module github.com/openshift-online/rosa-hyperfleet-api/hack/cmd/bridge-gen go 1.21 diff --git a/hack/clientset/cmd/wire-gen/main.go b/hack/clientset/cmd/bridge-gen/main.go similarity index 78% rename from hack/clientset/cmd/wire-gen/main.go rename to hack/clientset/cmd/bridge-gen/main.go index c349b02a..af5c3f73 100644 --- a/hack/clientset/cmd/wire-gen/main.go +++ b/hack/clientset/cmd/bridge-gen/main.go @@ -14,15 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -// wire-gen reads +wire:* markers from Go source files and emits generated Go code. +// bridge-gen reads +bridge:* markers from Go source files and emits generated Go code. // // Modes: // -// --mode=mappings (default): emits wire_mappings_generated.go containing a -// FieldMapping slice derived from +wire:field markers. +// --mode=bridge (default): emits bridge_mappings_generated.go containing a +// per-type FieldMapping map derived from +bridge:field markers. // -// --mode=wrappers: emits wire_wrappers_generated.go containing wrapper types -// for resources annotated with +wire:watch=disabled and/or +wire:wait. +// --mode=platform: emits bridge_wrappers_generated.go containing wrapper types +// for resources annotated with +bridge:watch=disabled and/or +bridge:wait. // The Watch method returns ErrWatchNotSupported; WaitUntil provides // polling-based synchronization via a caller-supplied condition function. package main @@ -44,44 +44,52 @@ import ( ) var ( - wireMarkerRE = regexp.MustCompile(`\+wire:field=([^,\s]+),meta=([^\s]+)`) - watchDisabledRE = regexp.MustCompile(`\+wire:watch=disabled`) - waitRE = regexp.MustCompile(`\+wire:wait\b`) + bridgeMarkerRE = regexp.MustCompile(`\+bridge:field=([^,\s]+),meta=([^\s]+)`) + watchDisabledRE = regexp.MustCompile(`\+bridge:watch=disabled`) + waitRE = regexp.MustCompile(`\+bridge:wait\b`) nonNamespacedRE = regexp.MustCompile(`\+genclient:nonNamespaced\b`) ) // fieldMapping holds a single wire→metadata field translation. type fieldMapping struct { - Wire string - Meta string + Bridge string + Meta string } -// resourceType describes a CRD type annotated with +wire:watch or +wire:wait. +// resourceType describes a CRD type annotated with +bridge:watch or +bridge:wait. type resourceType struct { Name string // e.g. "Cluster" PluralName string // e.g. "Clusters" + PluralLower string // e.g. "clusters" — URL path segment key LowerName string // e.g. "cluster" WatchDisabled bool Wait bool - NonNamespaced bool // set when +genclient:nonNamespaced is present + NonNamespaced bool // set when +genclient:nonNamespaced is present + Mappings []fieldMapping // from +bridge:field markers on this type } // ── Templates ──────────────────────────────────────────────────────────────── -const mappingsTmpl = `// Code generated by wire-gen. DO NOT EDIT. +const mappingsTmpl = `// Code generated by bridge-gen. DO NOT EDIT. package {{.Package}} // defaultMappings maps platform-api wire-format field names to Kubernetes -// metadata field names. Generated from +wire:field markers on CRD types. -var defaultMappings = []FieldMapping{ -{{- range .Mappings}} - {Wire: "{{.Wire}}", Meta: "{{.Meta}}"}, -{{- end}} +// metadata field names, keyed by the lowercase plural resource name that +// matches the URL path segment (e.g. "clusters", "nodepools"). +// Generated from +bridge:field markers on CRD types. +var defaultMappings = map[string][]FieldMapping{ +{{- range .Types}}{{if .Mappings}} + "{{.PluralLower}}": { + {{- range .Mappings}} + {Bridge: "{{.Bridge}}", Meta: "{{.Meta}}"}, + {{- end}} + }, +{{- end}}{{end}} } ` -const wrappersTmpl = `// Code generated by wire-gen. DO NOT EDIT. +const platformTmpl = `// Code generated by bridge-gen. DO NOT EDIT. package {{.Package}} @@ -96,8 +104,8 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" - typedclient "{{.TypedPkgImport}}" v1alpha1 "{{.ApiPkgImport}}" + typedclient "{{.TypedPkgImport}}" ) {{range .Types}} // {{.Name}}Interface is the platform-scoped client for {{.Name}} resources. @@ -201,7 +209,7 @@ func (c *{{.LowerName}}Client) WaitUntil(ctx context.Context, id string, conditi } } {{- end}} -{{end -}} +{{end}} // {{.TypedClientPrefix}}Interface is the platform-scoped typed client for the hyperfleet.io/v1alpha1 group. type {{.TypedClientPrefix}}Interface interface { RESTClient() rest.Interface @@ -241,18 +249,18 @@ func (w *wrappedV1alpha1) {{.PluralName}}(namespace string) {{.Name}}Interface { // ── main ───────────────────────────────────────────────────────────────────── func main() { - mode := flag.String("mode", "mappings", "generation mode: mappings or wrappers") - inputDir := flag.String("input-dir", "", "directory of Go source files to scan for +wire:* markers") + mode := flag.String("mode", "bridge", "generation mode: bridge or platform") + inputDir := flag.String("input-dir", "", "directory of Go source files to scan for +bridge:* markers") outputDir := flag.String("output-dir", "", "directory to write the generated file") outputPkg := flag.String("output-pkg", "transport", "Go package name for the generated file") headerFile := flag.String("go-header-file", "", "file whose contents are prepended to the generated output") - typedPkg := flag.String("typed-pkg-import", "", "[wrappers] import path of the generated typed client package") - apiPkg := flag.String("api-pkg-import", "", "[wrappers] import path of the CRD API types package") - typedClientPrefix := flag.String("typed-client-prefix", "V1alpha1", "[wrappers] group-level interface name prefix (e.g. V1alpha1 or V1alpha1Public)") + typedPkg := flag.String("typed-pkg-import", "", "[platform] import path of the generated typed client package") + apiPkg := flag.String("api-pkg-import", "", "[platform] import path of the CRD API types package") + typedClientPrefix := flag.String("typed-client-prefix", "V1alpha1", "[platform] group-level interface name prefix (e.g. V1alpha1 or V1alpha1Public)") flag.Parse() if *inputDir == "" || *outputDir == "" { - fmt.Fprintln(os.Stderr, "wire-gen: --input-dir and --output-dir are required") + fmt.Fprintln(os.Stderr, "bridge-gen: --input-dir and --output-dir are required") os.Exit(1) } @@ -263,25 +271,25 @@ func main() { } switch *mode { - case "mappings": + case "bridge": generateMappings(*inputDir, *outputDir, *outputPkg, header) - case "wrappers": + case "platform": if *typedPkg == "" || *apiPkg == "" { - fmt.Fprintln(os.Stderr, "wire-gen: wrappers mode requires --typed-pkg-import and --api-pkg-import") + fmt.Fprintln(os.Stderr, "bridge-gen: platform mode requires --typed-pkg-import and --api-pkg-import") os.Exit(1) } - generateWrappers(*inputDir, *outputDir, *outputPkg, *typedPkg, *apiPkg, *typedClientPrefix, header) + generatePlatform(*inputDir, *outputDir, *outputPkg, *typedPkg, *apiPkg, *typedClientPrefix, header) default: - fatalf("unknown mode %q; use mappings or wrappers", *mode) + fatalf("unknown mode %q; use bridge or platform", *mode) } } -// ── mappings mode ───────────────────────────────────────────────────────────── +// ── bridge mode ───────────────────────────────────────────────────────────── func generateMappings(inputDir, outputDir, pkg, header string) { - mappings := collectMappings(inputDir) + types := collectResourceTypes(inputDir) - outPath := filepath.Join(outputDir, "wire_mappings_generated.go") + outPath := filepath.Join(outputDir, "bridge_mappings_generated.go") f, err := os.Create(outPath) if err != nil { fatalf("creating %s: %v", outPath, err) @@ -290,64 +298,31 @@ func generateMappings(inputDir, outputDir, pkg, header string) { if err := f.Close(); err != nil { fatalf("closing %s: %v", outPath, err) } - fmt.Printf("wire-gen: wrote %s\n", outPath) + fmt.Printf("bridge-gen: wrote %s\n", outPath) }() if _, err := fmt.Fprint(f, header); err != nil { fatalf("writing header to %s: %v", outPath, err) } - tmpl := template.Must(template.New("mappings").Parse(mappingsTmpl)) + tmpl := template.Must(template.New("bridge").Parse(mappingsTmpl)) if err := tmpl.Execute(f, map[string]any{ - "Package": pkg, - "Mappings": mappings, + "Package": pkg, + "Types": types, }); err != nil { fatalf("rendering template: %v", err) } } -// collectMappings parses all Go source files in dir and extracts unique -// +wire:field=,meta= markers from any comment in the file. -// All comment groups are scanned so markers in floating comment blocks are captured. -func collectMappings(dir string) []fieldMapping { - fset := token.NewFileSet() - pkgs, err := parser.ParseDir(fset, dir, nil, parser.ParseComments) - if err != nil { - fatalf("parsing %s: %v", dir, err) - } - - seen := map[string]bool{} - var mappings []fieldMapping +// ── platform mode ───────────────────────────────────────────────────────────── - for _, pkg := range pkgs { - for _, file := range pkg.Files { - for _, cg := range file.Comments { - for _, comment := range cg.List { - for _, m := range wireMarkerRE.FindAllStringSubmatch(comment.Text, -1) { - key := m[1] + ":" + m[2] - if !seen[key] { - seen[key] = true - mappings = append(mappings, fieldMapping{Wire: m[1], Meta: m[2]}) - } - } - } - } - } - } - - sort.Slice(mappings, func(i, j int) bool { return mappings[i].Wire < mappings[j].Wire }) - return mappings -} - -// ── wrappers mode ───────────────────────────────────────────────────────────── - -func generateWrappers(inputDir, outputDir, pkg, typedPkgImport, apiPkgImport, typedClientPrefix, header string) { +func generatePlatform(inputDir, outputDir, pkg, typedPkgImport, apiPkgImport, typedClientPrefix, header string) { types := collectResourceTypes(inputDir) if len(types) == 0 { - fatalf("no resource types with +wire:watch or +wire:wait markers found in %s", inputDir) + fatalf("no resource types with +bridge:watch or +bridge:wait markers found in %s", inputDir) } - outPath := filepath.Join(outputDir, "wire_wrappers_generated.go") + outPath := filepath.Join(outputDir, "bridge_wrappers_generated.go") f, err := os.Create(outPath) if err != nil { fatalf("creating %s: %v", outPath, err) @@ -356,7 +331,7 @@ func generateWrappers(inputDir, outputDir, pkg, typedPkgImport, apiPkgImport, ty if err := f.Close(); err != nil { fatalf("closing %s: %v", outPath, err) } - fmt.Printf("wire-gen: wrote %s\n", outPath) + fmt.Printf("bridge-gen: wrote %s\n", outPath) }() if _, err := fmt.Fprint(f, header); err != nil { @@ -373,7 +348,7 @@ func generateWrappers(inputDir, outputDir, pkg, typedPkgImport, apiPkgImport, ty } } - tmpl := template.Must(template.New("wrappers").Parse(wrappersTmpl)) + tmpl := template.Must(template.New("platform").Parse(platformTmpl)) if err := tmpl.Execute(f, map[string]any{ "Package": pkg, "ApiPkgImport": apiPkgImport, @@ -388,7 +363,7 @@ func generateWrappers(inputDir, outputDir, pkg, typedPkgImport, apiPkgImport, ty } // collectResourceTypes scans all Go source files in dir for type declarations -// annotated with +wire:watch=disabled and/or +wire:wait. +// annotated with +bridge:watch=disabled and/or +bridge:wait. // // Because generators conventionally place markers in a floating comment block // (separated by a blank line from the actual doc comment), markers are found by @@ -431,6 +406,8 @@ func collectResourceTypes(dir string) []resourceType { // next type declaration that follows it. for _, cg := range file.Comments { var hasWatch, hasWait, hasNonNamespaced bool + var typeMappings []fieldMapping + seenMappings := map[string]bool{} for _, c := range cg.List { if watchDisabledRE.MatchString(c.Text) { hasWatch = true @@ -441,10 +418,18 @@ func collectResourceTypes(dir string) []resourceType { if nonNamespacedRE.MatchString(c.Text) { hasNonNamespaced = true } + for _, m := range bridgeMarkerRE.FindAllStringSubmatch(c.Text, -1) { + key := m[1] + ":" + m[2] + if !seenMappings[key] { + seenMappings[key] = true + typeMappings = append(typeMappings, fieldMapping{Bridge: m[1], Meta: m[2]}) + } + } } if !hasWatch && !hasWait { continue } + sort.Slice(typeMappings, func(i, j int) bool { return typeMappings[i].Bridge < typeMappings[j].Bridge }) cgEnd := cg.End() for _, td := range typeDecls { @@ -456,13 +441,16 @@ func collectResourceTypes(dir string) []resourceType { } seen[td.name] = true name := td.name + plural := name + "s" types = append(types, resourceType{ Name: name, - PluralName: name + "s", + PluralName: plural, + PluralLower: strings.ToLower(plural), LowerName: strings.ToLower(name[:1]) + name[1:], WatchDisabled: hasWatch, Wait: hasWait, NonNamespaced: hasNonNamespaced, + Mappings: typeMappings, }) break } @@ -488,6 +476,6 @@ func readHeader(path string) string { } func fatalf(format string, args ...any) { - fmt.Fprintf(os.Stderr, "wire-gen: "+format+"\n", args...) + fmt.Fprintf(os.Stderr, "bridge-gen: "+format+"\n", args...) os.Exit(1) } diff --git a/hyperfleet-operator/config/crd/bases/hyperfleet.io_clusters.yaml b/hyperfleet-operator/config/crd/bases/hyperfleet.io_clusters.yaml index c8caa6c2..7a4a7bd0 100644 --- a/hyperfleet-operator/config/crd/bases/hyperfleet.io_clusters.yaml +++ b/hyperfleet-operator/config/crd/bases/hyperfleet.io_clusters.yaml @@ -1,4 +1,3 @@ ---- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: @@ -12,4896 +11,5854 @@ spec: listKind: ClusterList plural: clusters shortNames: - - hfc + - hfc singular: cluster scope: Namespaced versions: - - additionalPrinterColumns: - - jsonPath: .status.phase - name: Phase - type: string - - jsonPath: .status.placementRef.managementCluster - name: MC - type: string - - jsonPath: .status.controlPlaneEndpoint.host - name: Endpoint - priority: 1 - type: string - - jsonPath: .spec.expirationTimestamp - name: Expires - priority: 1 - type: date - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: |- - 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. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - 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: - accountId: - description: AccountID is the AWS account that owns this cluster. - type: string - controlPlaneUpgradePolicy: - description: ControlPlaneUpgradePolicy is the control plane upgrade - policy defined by the user. - properties: - nextRun: - description: NextRun is the time the upgrade should run for "manual" - upgrade policies - format: date-time - type: string - schedule: - description: |- - Schedule defines a cron expression that calculates the next automatic upgrade scheduling. - The cron expression must follow the standard 5-field format: - ┌───────────── minute (0 - 59) - │ ┌───────────── hour (0 - 23) - │ │ ┌───────────── day of month (1 - 31) - │ │ │ ┌───────────── month (1 - 12) - │ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday) - │ │ │ │ │ - * * * * * - maxLength: 256 - pattern: ^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])-([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])(,([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]))*) - (\*|([0-9]|1[0-9]|2[0-3])|([0-9]|1[0-9]|2[0-3])-([0-9]|1[0-9]|2[0-3])|\*/([0-9]|1[0-9]|2[0-3])|([0-9]|1[0-9]|2[0-3])(,([0-9]|1[0-9]|2[0-3]))*) - (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|([1-9]|1[0-9]|2[0-9]|3[0-1])-([1-9]|1[0-9]|2[0-9]|3[0-1])|\*/([1-9]|1[0-9]|2[0-9]|3[0-1])|([1-9]|1[0-9]|2[0-9]|3[0-1])(,([1-9]|1[0-9]|2[0-9]|3[0-1]))*) - (\*|([1-9]|1[0-2])|([1-9]|1[0-2])-([1-9]|1[0-2])|\*/([1-9]|1[0-2])|([1-9]|1[0-2])(,([1-9]|1[0-2]))*) - (\*|[0-6]|[0-6]-[0-6]|\*/[0-6]|[0-6](,[0-6])*)$ - type: string - scheduleType: - description: |- - ScheduleType indicates if the control plane upgrade policy is "manual" and it's executed only one time or - whether it is "automatic" where an expression will calculate recurrent upgrades. - enum: - - Manual - - Automatic - type: string - updateType: - description: |- - UpdateType indicates if it is a control plane upgrade policy defined by the user or - triggered by Red Hat for addressing critical CVEs. - enum: - - UserInitiated - - ServiceInitiated - type: string - upgradeScope: - description: |- - UpgradeScope indicates if minor version upgrades are allowed for automatic upgrades. - Manual upgrades always allow it. - enum: - - PatchOnly - - PatchAndMinor - type: string - version: - description: Version is the desired upgrade version on "manual" - upgrade policies. - maxLength: 64 - type: string - required: - - scheduleType - - updateType - type: object - x-kubernetes-validations: - - message: version and nextRun are required when scheduleType is Manual - rule: self.scheduleType != 'Manual' || (has(self.version) && has(self.nextRun)) - - message: schedule must not be set when scheduleType is Manual - rule: self.scheduleType != 'Manual' || !has(self.schedule) - - message: schedule is required when scheduleType is Automatic - rule: self.scheduleType != 'Automatic' || has(self.schedule) - - message: version and nextRun must not be set when scheduleType is - Automatic - rule: self.scheduleType != 'Automatic' || (!has(self.version) && - !has(self.nextRun)) - - message: upgradeScope must not be set when scheduleType is Manual - rule: self.scheduleType != 'Manual' || !has(self.upgradeScope) - - message: upgradeScope is required when scheduleType is Automatic - rule: self.scheduleType != 'Automatic' || has(self.upgradeScope) - creatorARN: - 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 marks when this cluster should be - automatically deleted. - format: date-time - type: string - hostedCluster: - description: |- - 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 - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - auditWebhook: - description: auditWebhook contains metadata for configuring an - audit webhook endpoint - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - autoNode: - description: autoNode specifies the configuration for automatic - node provisioning and lifecycle management. - properties: - provisionerConfig: - description: provisionerConfig specifies the provisioner used - for automatic node management. - properties: - karpenter: - description: karpenter specifies the configuration for - the Karpenter provisioner. - properties: - aws: - description: aws specifies the AWS-specific configuration - for Karpenter. - properties: - roleARN: - description: "roleARN specifies the ARN of the - IAM role that Karpenter assumes to provision\nand - manage EC2 instances in the hosted cluster's - AWS account.\n\nThe referenced role must have - a trust relationship that allows it to be assumed\nby - the karpenter service account in the hosted - cluster via OIDC.\nExample:\n{\n\t\"Version\": - \"2012-10-17\",\n\t\"Statement\": [\n\t\t{\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Principal\": {\n\t\t\t\t\"Federated\": - \"\"\n\t\t\t},\n\t\t\t\"Action\": - \"sts:AssumeRoleWithWebIdentity\",\n\t\t\t\"Condition\": - {\n\t\t\t\t\"StringEquals\": {\n\t\t\t\t\t\":sub\": - \"system:serviceaccount:kube-system:karpenter\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n\nThe - following is an example of the policy document - for this role.\n\n{\n\t\"Version\": \"2012-10-17\",\n\t\"Statement\": - [\n\t\t{\n\t\t\t\"Sid\": \"AllowScopedEC2InstanceAccessActions\",\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Resource\": [\n\t\t\t\t\"arn:*:ec2:*::image/*\",\n\t\t\t\t\"arn:*:ec2:*::snapshot/*\",\n\t\t\t\t\"arn:*:ec2:*:*:security-group/*\",\n\t\t\t\t\"arn:*:ec2:*:*:subnet/*\"\n\t\t\t],\n\t\t\t\"Action\": - [\n\t\t\t\t\"ec2:RunInstances\",\n\t\t\t\t\"ec2:CreateFleet\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"Sid\": - \"AllowScopedEC2LaunchTemplateAccessActions\",\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Resource\": \"arn:*:ec2:*:*:launch-template/*\",\n\t\t\t\"Action\": - [\n\t\t\t\t\"ec2:RunInstances\",\n\t\t\t\t\"ec2:CreateFleet\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"Sid\": - \"AllowScopedEC2InstanceActionsWithTags\",\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Resource\": [\n\t\t\t\t\"arn:*:ec2:*:*:fleet/*\",\n\t\t\t\t\"arn:*:ec2:*:*:instance/*\",\n\t\t\t\t\"arn:*:ec2:*:*:volume/*\",\n\t\t\t\t\"arn:*:ec2:*:*:network-interface/*\",\n\t\t\t\t\"arn:*:ec2:*:*:launch-template/*\",\n\t\t\t\t\"arn:*:ec2:*:*:spot-instances-request/*\"\n\t\t\t],\n\t\t\t\"Action\": - [\n\t\t\t\t\"ec2:RunInstances\",\n\t\t\t\t\"ec2:CreateFleet\",\n\t\t\t\t\"ec2:CreateLaunchTemplate\"\n\t\t\t],\n\t\t\t\"Condition\": - {\n\t\t\t\t\"StringLike\": {\n\t\t\t\t\t\"aws:RequestTag/karpenter.sh/nodepool\": - \"*\"\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Sid\": - \"AllowScopedResourceCreationTagging\",\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Resource\": [\n\t\t\t\t\"arn:*:ec2:*:*:fleet/*\",\n\t\t\t\t\"arn:*:ec2:*:*:instance/*\",\n\t\t\t\t\"arn:*:ec2:*:*:volume/*\",\n\t\t\t\t\"arn:*:ec2:*:*:network-interface/*\",\n\t\t\t\t\"arn:*:ec2:*:*:launch-template/*\",\n\t\t\t\t\"arn:*:ec2:*:*:spot-instances-request/*\"\n\t\t\t],\n\t\t\t\"Action\": - \"ec2:CreateTags\",\n\t\t\t\"Condition\": {\n\t\t\t\t\"StringEquals\": - {\n\t\t\t\t\t\"ec2:CreateAction\": [\n\t\t\t\t\t\t\"RunInstances\",\n\t\t\t\t\t\t\"CreateFleet\",\n\t\t\t\t\t\t\"CreateLaunchTemplate\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"StringLike\": - {\n\t\t\t\t\t\"aws:RequestTag/karpenter.sh/nodepool\": - \"*\"\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Sid\": - \"AllowScopedResourceTagging\",\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Resource\": \"arn:*:ec2:*:*:instance/*\",\n\t\t\t\"Action\": - \"ec2:CreateTags\",\n\t\t\t\"Condition\": {\n\t\t\t\t\"StringLike\": - {\n\t\t\t\t\t\"aws:ResourceTag/karpenter.sh/nodepool\": - \"*\"\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Sid\": - \"AllowScopedDeletion\",\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Resource\": [\n\t\t\t\t\"arn:*:ec2:*:*:instance/*\",\n\t\t\t\t\"arn:*:ec2:*:*:launch-template/*\"\n\t\t\t],\n\t\t\t\"Action\": - [\n\t\t\t\t\"ec2:TerminateInstances\",\n\t\t\t\t\"ec2:DeleteLaunchTemplate\"\n\t\t\t],\n\t\t\t\"Condition\": - {\n\t\t\t\t\"StringLike\": {\n\t\t\t\t\t\"aws:ResourceTag/karpenter.sh/nodepool\": - \"*\"\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Sid\": - \"AllowRegionalReadActions\",\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Resource\": \"*\",\n\t\t\t\"Action\": - [\n\t\t\t\t\"ec2:DescribeImages\",\n\t\t\t\t\"ec2:DescribeInstances\",\n\t\t\t\t\"ec2:DescribeInstanceTypeOfferings\",\n\t\t\t\t\"ec2:DescribeInstanceTypes\",\n\t\t\t\t\"ec2:DescribeLaunchTemplates\",\n\t\t\t\t\"ec2:DescribeSecurityGroups\",\n\t\t\t\t\"ec2:DescribeSpotPriceHistory\",\n\t\t\t\t\"ec2:DescribeSubnets\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"Sid\": - \"AllowSSMReadActions\",\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Resource\": \"arn:*:ssm:*::parameter/aws/service/*\",\n\t\t\t\"Action\": - \"ssm:GetParameter\"\n\t\t},\n\t\t{\n\t\t\t\"Sid\": - \"AllowPricingReadActions\",\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Resource\": \"*\",\n\t\t\t\"Action\": - \"pricing:GetProducts\"\n\t\t},\n\t\t{\n\t\t\t\"Sid\": - \"AllowInterruptionQueueActions\",\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Resource\": \"*\",\n\t\t\t\"Action\": - [\n\t\t\t\t\"sqs:DeleteMessage\",\n\t\t\t\t\"sqs:GetQueueUrl\",\n\t\t\t\t\"sqs:ReceiveMessage\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"Sid\": - \"AllowPassingInstanceRole\",\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Resource\": \"arn:*:iam::*:role/*\",\n\t\t\t\"Action\": - \"iam:PassRole\",\n\t\t\t\"Condition\": {\n\t\t\t\t\"StringEquals\": - {\n\t\t\t\t\t\"iam:PassedToService\": [\n\t\t\t\t\t\t\"ec2.amazonaws.com\",\n\t\t\t\t\t\t\"ec2.amazonaws.com.cn\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Sid\": - \"AllowScopedInstanceProfileCreationActions\",\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Resource\": \"arn:*:iam::*:instance-profile/*\",\n\t\t\t\"Action\": - [\n\t\t\t\t\"iam:CreateInstanceProfile\"\n\t\t\t],\n\t\t\t\"Condition\": - {\n\t\t\t\t\"StringLike\": {\n\t\t\t\t\t\"aws:RequestTag/karpenter.k8s.aws/ec2nodeclass\": - \"*\"\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Sid\": - \"AllowScopedInstanceProfileTagActions\",\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Resource\": \"arn:*:iam::*:instance-profile/*\",\n\t\t\t\"Action\": - [\n\t\t\t\t\"iam:TagInstanceProfile\"\n\t\t\t],\n\t\t\t\"Condition\": - {\n\t\t\t\t\"StringLike\": {\n\t\t\t\t\t\"aws:ResourceTag/karpenter.k8s.aws/ec2nodeclass\": - \"*\",\n\t\t\t\t\t\"aws:RequestTag/karpenter.k8s.aws/ec2nodeclass\": - \"*\"\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Sid\": - \"AllowScopedInstanceProfileActions\",\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Resource\": \"arn:*:iam::*:instance-profile/*\",\n\t\t\t\"Action\": - [\n\t\t\t\t\"iam:AddRoleToInstanceProfile\",\n\t\t\t\t\"iam:RemoveRoleFromInstanceProfile\",\n\t\t\t\t\"iam:DeleteInstanceProfile\"\n\t\t\t],\n\t\t\t\"Condition\": - {\n\t\t\t\t\"StringLike\": {\n\t\t\t\t\t\"aws:ResourceTag/karpenter.k8s.aws/ec2nodeclass\": - \"*\"\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Sid\": - \"AllowInstanceProfileReadActions\",\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Resource\": \"arn:*:iam::*:instance-profile/*\",\n\t\t\t\"Action\": - \"iam:GetInstanceProfile\"\n\t\t},\n\t\t{\n\t\t\t\"Sid\": - \"AllowUnscopedInstanceProfileListAction\",\n\t\t\t\"Effect\": - \"Allow\",\n\t\t\t\"Resource\": \"*\",\n\t\t\t\"Action\": - \"iam:ListInstanceProfiles\"\n\t\t}\n\t]\n}" - maxLength: 2048 - type: string - x-kubernetes-validations: - - message: roleARN must be a valid AWS IAM role - ARN (e.g. arn:aws:iam::123456789012:role/MyRole) - rule: self.matches('^arn:(aws|aws-cn|aws-us-gov):iam::[0-9]{12}:role/.+$') - required: - - roleARN - type: object - platform: - description: platform specifies the infrastructure - platform that Karpenter should provision nodes on. - enum: - - AWS - maxLength: 100 - type: string - required: - - platform - type: object - x-kubernetes-validations: - - message: aws is required when platform is AWS, and forbidden - otherwise - rule: 'self.platform == ''AWS'' ? has(self.aws) : !has(self.aws)' - name: - description: name specifies the name of the provisioner - to use for automatic node management. + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .status.placementRef.managementCluster + name: MC + type: string + - jsonPath: .status.controlPlaneEndpoint.host + name: Endpoint + priority: 1 + type: string + - jsonPath: .spec.expirationTimestamp + name: Expires + priority: 1 + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + 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. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + 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: + accountId: + description: AccountID is the AWS account that owns this cluster. + type: string + controlPlaneUpgradePolicy: + description: ControlPlaneUpgradePolicy is the control plane upgrade policy defined by the user. + properties: + nextRun: + description: NextRun is the time the upgrade should run for "manual" upgrade policies + format: date-time + type: string + schedule: + description: |- + Schedule defines a cron expression that calculates the next automatic upgrade scheduling. + The cron expression must follow the standard 5-field format: + ┌───────────── minute (0 - 59) + │ ┌───────────── hour (0 - 23) + │ │ ┌───────────── day of month (1 - 31) + │ │ │ ┌───────────── month (1 - 12) + │ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday) + │ │ │ │ │ + * * * * * + maxLength: 256 + pattern: ^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])-([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])(,([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]))*) (\*|([0-9]|1[0-9]|2[0-3])|([0-9]|1[0-9]|2[0-3])-([0-9]|1[0-9]|2[0-3])|\*/([0-9]|1[0-9]|2[0-3])|([0-9]|1[0-9]|2[0-3])(,([0-9]|1[0-9]|2[0-3]))*) (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|([1-9]|1[0-9]|2[0-9]|3[0-1])-([1-9]|1[0-9]|2[0-9]|3[0-1])|\*/([1-9]|1[0-9]|2[0-9]|3[0-1])|([1-9]|1[0-9]|2[0-9]|3[0-1])(,([1-9]|1[0-9]|2[0-9]|3[0-1]))*) (\*|([1-9]|1[0-2])|([1-9]|1[0-2])-([1-9]|1[0-2])|\*/([1-9]|1[0-2])|([1-9]|1[0-2])(,([1-9]|1[0-2]))*) (\*|[0-6]|[0-6]-[0-6]|\*/[0-6]|[0-6](,[0-6])*)$ + type: string + scheduleType: + description: |- + ScheduleType indicates if the control plane upgrade policy is "manual" and it's executed only one time or + whether it is "automatic" where an expression will calculate recurrent upgrades. + enum: + - Manual + - Automatic + type: string + updateType: + description: |- + UpdateType indicates if it is a control plane upgrade policy defined by the user or + triggered by Red Hat for addressing critical CVEs. + enum: + - UserInitiated + - ServiceInitiated + type: string + upgradeScope: + description: |- + UpgradeScope indicates if minor version upgrades are allowed for automatic upgrades. + Manual upgrades always allow it. + enum: + - PatchOnly + - PatchAndMinor + type: string + version: + description: Version is the desired upgrade version on "manual" upgrade policies. + maxLength: 64 + type: string + required: + - scheduleType + - updateType + type: object + x-kubernetes-validations: + - message: version and nextRun are required when scheduleType is Manual + rule: self.scheduleType != 'Manual' || (has(self.version) && has(self.nextRun)) + - message: schedule must not be set when scheduleType is Manual + rule: self.scheduleType != 'Manual' || !has(self.schedule) + - message: schedule is required when scheduleType is Automatic + rule: self.scheduleType != 'Automatic' || has(self.schedule) + - message: version and nextRun must not be set when scheduleType is Automatic + rule: self.scheduleType != 'Automatic' || (!has(self.version) && !has(self.nextRun)) + - message: upgradeScope must not be set when scheduleType is Manual + rule: self.scheduleType != 'Manual' || !has(self.upgradeScope) + - message: upgradeScope is required when scheduleType is Automatic + rule: self.scheduleType != 'Automatic' || has(self.upgradeScope) + creatorARN: + 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 marks when this cluster should be automatically deleted. + format: date-time + type: string + hostedCluster: + description: |- + 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 + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + auditWebhook: + description: auditWebhook contains metadata for configuring an audit webhook endpoint + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + autoNode: + description: autoNode specifies the configuration for automatic node provisioning and lifecycle management. + properties: + provisionerConfig: + description: provisionerConfig specifies the provisioner used for automatic node management. + properties: + karpenter: + description: karpenter specifies the configuration for the Karpenter provisioner. + properties: + aws: + description: aws specifies the AWS-specific configuration for Karpenter. + properties: + roleARN: + description: "roleARN specifies the ARN of the IAM role that Karpenter assumes to provision\nand manage EC2 instances in the hosted cluster's AWS account.\n\nThe referenced role must have a trust relationship that allows it to be assumed\nby the karpenter service account in the hosted cluster via OIDC.\nExample:\n{\n\t\"Version\": \"2012-10-17\",\n\t\"Statement\": [\n\t\t{\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Principal\": {\n\t\t\t\t\"Federated\": \"\"\n\t\t\t},\n\t\t\t\"Action\": \"sts:AssumeRoleWithWebIdentity\",\n\t\t\t\"Condition\": {\n\t\t\t\t\"StringEquals\": {\n\t\t\t\t\t\":sub\": \"system:serviceaccount:kube-system:karpenter\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n\nThe following is an example of the policy document for this role.\n\n{\n\t\"Version\": \"2012-10-17\",\n\t\"Statement\": [\n\t\t{\n\t\t\t\"Sid\": \"AllowScopedEC2InstanceAccessActions\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Resource\": [\n\t\t\t\t\"arn:*:ec2:*::image/*\",\n\t\t\t\t\"arn:*:ec2:*::snapshot/*\",\n\t\t\t\t\"arn:*:ec2:*:*:security-group/*\",\n\t\t\t\t\"arn:*:ec2:*:*:subnet/*\"\n\t\t\t],\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:RunInstances\",\n\t\t\t\t\"ec2:CreateFleet\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"AllowScopedEC2LaunchTemplateAccessActions\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Resource\": \"arn:*:ec2:*:*:launch-template/*\",\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:RunInstances\",\n\t\t\t\t\"ec2:CreateFleet\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"AllowScopedEC2InstanceActionsWithTags\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Resource\": [\n\t\t\t\t\"arn:*:ec2:*:*:fleet/*\",\n\t\t\t\t\"arn:*:ec2:*:*:instance/*\",\n\t\t\t\t\"arn:*:ec2:*:*:volume/*\",\n\t\t\t\t\"arn:*:ec2:*:*:network-interface/*\",\n\t\t\t\t\"arn:*:ec2:*:*:launch-template/*\",\n\t\t\t\t\"arn:*:ec2:*:*:spot-instances-request/*\"\n\t\t\t],\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:RunInstances\",\n\t\t\t\t\"ec2:CreateFleet\",\n\t\t\t\t\"ec2:CreateLaunchTemplate\"\n\t\t\t],\n\t\t\t\"Condition\": {\n\t\t\t\t\"StringLike\": {\n\t\t\t\t\t\"aws:RequestTag/karpenter.sh/nodepool\": \"*\"\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"AllowScopedResourceCreationTagging\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Resource\": [\n\t\t\t\t\"arn:*:ec2:*:*:fleet/*\",\n\t\t\t\t\"arn:*:ec2:*:*:instance/*\",\n\t\t\t\t\"arn:*:ec2:*:*:volume/*\",\n\t\t\t\t\"arn:*:ec2:*:*:network-interface/*\",\n\t\t\t\t\"arn:*:ec2:*:*:launch-template/*\",\n\t\t\t\t\"arn:*:ec2:*:*:spot-instances-request/*\"\n\t\t\t],\n\t\t\t\"Action\": \"ec2:CreateTags\",\n\t\t\t\"Condition\": {\n\t\t\t\t\"StringEquals\": {\n\t\t\t\t\t\"ec2:CreateAction\": [\n\t\t\t\t\t\t\"RunInstances\",\n\t\t\t\t\t\t\"CreateFleet\",\n\t\t\t\t\t\t\"CreateLaunchTemplate\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"StringLike\": {\n\t\t\t\t\t\"aws:RequestTag/karpenter.sh/nodepool\": \"*\"\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"AllowScopedResourceTagging\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Resource\": \"arn:*:ec2:*:*:instance/*\",\n\t\t\t\"Action\": \"ec2:CreateTags\",\n\t\t\t\"Condition\": {\n\t\t\t\t\"StringLike\": {\n\t\t\t\t\t\"aws:ResourceTag/karpenter.sh/nodepool\": \"*\"\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"AllowScopedDeletion\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Resource\": [\n\t\t\t\t\"arn:*:ec2:*:*:instance/*\",\n\t\t\t\t\"arn:*:ec2:*:*:launch-template/*\"\n\t\t\t],\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:TerminateInstances\",\n\t\t\t\t\"ec2:DeleteLaunchTemplate\"\n\t\t\t],\n\t\t\t\"Condition\": {\n\t\t\t\t\"StringLike\": {\n\t\t\t\t\t\"aws:ResourceTag/karpenter.sh/nodepool\": \"*\"\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"AllowRegionalReadActions\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Resource\": \"*\",\n\t\t\t\"Action\": [\n\t\t\t\t\"ec2:DescribeImages\",\n\t\t\t\t\"ec2:DescribeInstances\",\n\t\t\t\t\"ec2:DescribeInstanceTypeOfferings\",\n\t\t\t\t\"ec2:DescribeInstanceTypes\",\n\t\t\t\t\"ec2:DescribeLaunchTemplates\",\n\t\t\t\t\"ec2:DescribeSecurityGroups\",\n\t\t\t\t\"ec2:DescribeSpotPriceHistory\",\n\t\t\t\t\"ec2:DescribeSubnets\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"AllowSSMReadActions\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Resource\": \"arn:*:ssm:*::parameter/aws/service/*\",\n\t\t\t\"Action\": \"ssm:GetParameter\"\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"AllowPricingReadActions\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Resource\": \"*\",\n\t\t\t\"Action\": \"pricing:GetProducts\"\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"AllowInterruptionQueueActions\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Resource\": \"*\",\n\t\t\t\"Action\": [\n\t\t\t\t\"sqs:DeleteMessage\",\n\t\t\t\t\"sqs:GetQueueUrl\",\n\t\t\t\t\"sqs:ReceiveMessage\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"AllowPassingInstanceRole\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Resource\": \"arn:*:iam::*:role/*\",\n\t\t\t\"Action\": \"iam:PassRole\",\n\t\t\t\"Condition\": {\n\t\t\t\t\"StringEquals\": {\n\t\t\t\t\t\"iam:PassedToService\": [\n\t\t\t\t\t\t\"ec2.amazonaws.com\",\n\t\t\t\t\t\t\"ec2.amazonaws.com.cn\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"AllowScopedInstanceProfileCreationActions\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Resource\": \"arn:*:iam::*:instance-profile/*\",\n\t\t\t\"Action\": [\n\t\t\t\t\"iam:CreateInstanceProfile\"\n\t\t\t],\n\t\t\t\"Condition\": {\n\t\t\t\t\"StringLike\": {\n\t\t\t\t\t\"aws:RequestTag/karpenter.k8s.aws/ec2nodeclass\": \"*\"\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"AllowScopedInstanceProfileTagActions\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Resource\": \"arn:*:iam::*:instance-profile/*\",\n\t\t\t\"Action\": [\n\t\t\t\t\"iam:TagInstanceProfile\"\n\t\t\t],\n\t\t\t\"Condition\": {\n\t\t\t\t\"StringLike\": {\n\t\t\t\t\t\"aws:ResourceTag/karpenter.k8s.aws/ec2nodeclass\": \"*\",\n\t\t\t\t\t\"aws:RequestTag/karpenter.k8s.aws/ec2nodeclass\": \"*\"\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"AllowScopedInstanceProfileActions\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Resource\": \"arn:*:iam::*:instance-profile/*\",\n\t\t\t\"Action\": [\n\t\t\t\t\"iam:AddRoleToInstanceProfile\",\n\t\t\t\t\"iam:RemoveRoleFromInstanceProfile\",\n\t\t\t\t\"iam:DeleteInstanceProfile\"\n\t\t\t],\n\t\t\t\"Condition\": {\n\t\t\t\t\"StringLike\": {\n\t\t\t\t\t\"aws:ResourceTag/karpenter.k8s.aws/ec2nodeclass\": \"*\"\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"AllowInstanceProfileReadActions\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Resource\": \"arn:*:iam::*:instance-profile/*\",\n\t\t\t\"Action\": \"iam:GetInstanceProfile\"\n\t\t},\n\t\t{\n\t\t\t\"Sid\": \"AllowUnscopedInstanceProfileListAction\",\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Resource\": \"*\",\n\t\t\t\"Action\": \"iam:ListInstanceProfiles\"\n\t\t}\n\t]\n}" + maxLength: 2048 + type: string + required: + - roleARN + type: object + platform: + description: platform specifies the infrastructure platform that Karpenter should provision nodes on. + enum: + - AWS + maxLength: 100 + type: string + required: + - platform + type: object + name: + description: name specifies the name of the provisioner to use for automatic node management. + enum: + - Karpenter + type: string + required: + - name + type: object + required: + - provisionerConfig + type: object + autoscaling: + description: autoscaling specifies auto-scaling behavior that applies to all NodePools + properties: + balancingIgnoredLabels: + description: |- + balancingIgnoredLabels sets "--balancing-ignore-label