From 1882b4476b50a0ec248bffa184b7e85a53d10ebb Mon Sep 17 00:00:00 2001 From: Evgeni Vakhonin Date: Sun, 14 Dec 2025 21:18:28 +0200 Subject: [PATCH 1/3] kubebuilder defaults and dynamic endpoints --- .../api/v1alpha1/jumpstarter_types.go | 6 ++ ...tarter-operator.clusterserviceversion.yaml | 6 +- ...operator.jumpstarter.dev_jumpstarters.yaml | 6 ++ ...operator.jumpstarter.dev_jumpstarters.yaml | 6 ++ ...tarter-operator.clusterserviceversion.yaml | 15 ++++- deploy/operator/dist/install.yaml | 6 ++ .../jumpstarter/endpoints/defaults.go | 61 +++++++++++++++++++ .../jumpstarter/endpoints/endpoints.go | 42 +++++++++++-- .../jumpstarter/jumpstarter_controller.go | 4 ++ 9 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 deploy/operator/internal/controller/jumpstarter/endpoints/defaults.go diff --git a/deploy/operator/api/v1alpha1/jumpstarter_types.go b/deploy/operator/api/v1alpha1/jumpstarter_types.go index db9149da..962b9080 100644 --- a/deploy/operator/api/v1alpha1/jumpstarter_types.go +++ b/deploy/operator/api/v1alpha1/jumpstarter_types.go @@ -131,6 +131,7 @@ type JumpstarterSpec struct { // Base domain used to construct FQDNs for all service endpoints. // This domain will be used to generate the default hostnames for Routes, Ingresses, and certificates. // Example: "example.com" will generate endpoints like "grpc.example.com", "router.example.com" + // +kubebuilder:default="jumpstarter.example.com" // +kubebuilder:validation:Pattern=^[a-z0-9]([a-z0-9\-\.]*[a-z0-9])?$ BaseDomain string `json:"baseDomain,omitempty"` @@ -142,10 +143,12 @@ type JumpstarterSpec struct { // Controller configuration for the main Jumpstarter API and gRPC services. // The controller handles gRPC and REST API requests from clients and exporters. + // +kubebuilder:default={} Controller ControllerConfig `json:"controller,omitempty"` // Router configuration for the Jumpstarter router service. // Routers handle gRPC traffic routing and load balancing. + // +kubebuilder:default={} Routers RoutersConfig `json:"routers,omitempty"` // Authentication configuration for client and exporter authentication. @@ -158,6 +161,7 @@ type JumpstarterSpec struct { type RoutersConfig struct { // Container image for the router pods in 'registry/repository/image:tag' format. // If not specified, defaults to the latest stable version of the Jumpstarter router. + // +kubebuilder:default="quay.io/jumpstarter-dev/jumpstarter-controller:latest" Image string `json:"image,omitempty"` // Image pull policy for the router container. @@ -192,6 +196,7 @@ type RoutersConfig struct { type ControllerConfig struct { // Container image for the controller pods in 'registry/repository/image:tag' format. // If not specified, defaults to the latest stable version of the Jumpstarter controller. + // +kubebuilder:default="quay.io/jumpstarter-dev/jumpstarter-controller:latest" Image string `json:"image,omitempty"` // Image pull policy for the controller container. @@ -525,6 +530,7 @@ type Jumpstarter struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` + // +kubebuilder:default={} Spec JumpstarterSpec `json:"spec,omitempty"` Status JumpstarterStatus `json:"status,omitempty"` } diff --git a/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml b/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml index e34de0ab..f6a02936 100644 --- a/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml +++ b/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml @@ -14,11 +14,13 @@ metadata: }, "name": "jumpstarter-sample" }, - "spec": null + "spec": { + "baseDomain": "jumpstarter.example.com" + } } ] capabilities: Basic Install - createdAt: "2025-11-25T08:56:27Z" + createdAt: "2025-12-14T17:57:58Z" operators.operatorframework.io/builder: operator-sdk-v1.41.1 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 name: jumpstarter-operator.v0.8.0 diff --git a/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml b/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml index 7b28cb83..cf5182fb 100644 --- a/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml +++ b/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml @@ -37,6 +37,7 @@ spec: metadata: type: object spec: + default: {} description: |- JumpstarterSpec defines the desired state of a Jumpstarter deployment. A deployment can be created in a namespace of the cluster, and that's where all the Jumpstarter @@ -423,6 +424,7 @@ spec: type: object type: object baseDomain: + default: jumpstarter.example.com description: |- Base domain used to construct FQDNs for all service endpoints. This domain will be used to generate the default hostnames for Routes, Ingresses, and certificates. @@ -430,6 +432,7 @@ spec: pattern: ^[a-z0-9]([a-z0-9\-\.]*[a-z0-9])?$ type: string controller: + default: {} description: |- Controller configuration for the main Jumpstarter API and gRPC services. The controller handles gRPC and REST API requests from clients and exporters. @@ -1083,6 +1086,7 @@ spec: type: object type: object image: + default: quay.io/jumpstarter-dev/jumpstarter-controller:latest description: |- Container image for the controller pods in 'registry/repository/image:tag' format. If not specified, defaults to the latest stable version of the Jumpstarter controller. @@ -1368,6 +1372,7 @@ spec: type: object type: object routers: + default: {} description: |- Router configuration for the Jumpstarter router service. Routers handle gRPC traffic routing and load balancing. @@ -1626,6 +1631,7 @@ spec: type: object type: object image: + default: quay.io/jumpstarter-dev/jumpstarter-controller:latest description: |- Container image for the router pods in 'registry/repository/image:tag' format. If not specified, defaults to the latest stable version of the Jumpstarter router. diff --git a/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml b/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml index dbb182c3..cd065e4b 100644 --- a/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml +++ b/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml @@ -37,6 +37,7 @@ spec: metadata: type: object spec: + default: {} description: |- JumpstarterSpec defines the desired state of a Jumpstarter deployment. A deployment can be created in a namespace of the cluster, and that's where all the Jumpstarter @@ -423,6 +424,7 @@ spec: type: object type: object baseDomain: + default: jumpstarter.example.com description: |- Base domain used to construct FQDNs for all service endpoints. This domain will be used to generate the default hostnames for Routes, Ingresses, and certificates. @@ -430,6 +432,7 @@ spec: pattern: ^[a-z0-9]([a-z0-9\-\.]*[a-z0-9])?$ type: string controller: + default: {} description: |- Controller configuration for the main Jumpstarter API and gRPC services. The controller handles gRPC and REST API requests from clients and exporters. @@ -1083,6 +1086,7 @@ spec: type: object type: object image: + default: quay.io/jumpstarter-dev/jumpstarter-controller:latest description: |- Container image for the controller pods in 'registry/repository/image:tag' format. If not specified, defaults to the latest stable version of the Jumpstarter controller. @@ -1368,6 +1372,7 @@ spec: type: object type: object routers: + default: {} description: |- Router configuration for the Jumpstarter router service. Routers handle gRPC traffic routing and load balancing. @@ -1626,6 +1631,7 @@ spec: type: object type: object image: + default: quay.io/jumpstarter-dev/jumpstarter-controller:latest description: |- Container image for the router pods in 'registry/repository/image:tag' format. If not specified, defaults to the latest stable version of the Jumpstarter router. diff --git a/deploy/operator/config/manifests/bases/jumpstarter-operator.clusterserviceversion.yaml b/deploy/operator/config/manifests/bases/jumpstarter-operator.clusterserviceversion.yaml index 7e93f930..8ac48346 100644 --- a/deploy/operator/config/manifests/bases/jumpstarter-operator.clusterserviceversion.yaml +++ b/deploy/operator/config/manifests/bases/jumpstarter-operator.clusterserviceversion.yaml @@ -2,7 +2,20 @@ apiVersion: operators.coreos.com/v1alpha1 kind: ClusterServiceVersion metadata: annotations: - alm-examples: '[]' + alm-examples: |- + [ + { + "apiVersion": "operator.jumpstarter.dev/v1alpha1", + "kind": "Jumpstarter", + "metadata": { + "name": "jumpstarter", + "namespace": "jumpstarter" + }, + "spec": { + "baseDomain": "jumpstarter.example.com" + } + } + ] capabilities: Basic Install name: jumpstarter-operator.v0.0.0 namespace: placeholder diff --git a/deploy/operator/dist/install.yaml b/deploy/operator/dist/install.yaml index 2216c694..ce66c231 100644 --- a/deploy/operator/dist/install.yaml +++ b/deploy/operator/dist/install.yaml @@ -440,6 +440,7 @@ spec: metadata: type: object spec: + default: {} description: |- JumpstarterSpec defines the desired state of a Jumpstarter deployment. A deployment can be created in a namespace of the cluster, and that's where all the Jumpstarter @@ -826,6 +827,7 @@ spec: type: object type: object baseDomain: + default: jumpstarter.example.com description: |- Base domain used to construct FQDNs for all service endpoints. This domain will be used to generate the default hostnames for Routes, Ingresses, and certificates. @@ -833,6 +835,7 @@ spec: pattern: ^[a-z0-9]([a-z0-9\-\.]*[a-z0-9])?$ type: string controller: + default: {} description: |- Controller configuration for the main Jumpstarter API and gRPC services. The controller handles gRPC and REST API requests from clients and exporters. @@ -1486,6 +1489,7 @@ spec: type: object type: object image: + default: quay.io/jumpstarter-dev/jumpstarter-controller:latest description: |- Container image for the controller pods in 'registry/repository/image:tag' format. If not specified, defaults to the latest stable version of the Jumpstarter controller. @@ -1771,6 +1775,7 @@ spec: type: object type: object routers: + default: {} description: |- Router configuration for the Jumpstarter router service. Routers handle gRPC traffic routing and load balancing. @@ -2029,6 +2034,7 @@ spec: type: object type: object image: + default: quay.io/jumpstarter-dev/jumpstarter-controller:latest description: |- Container image for the router pods in 'registry/repository/image:tag' format. If not specified, defaults to the latest stable version of the Jumpstarter router. diff --git a/deploy/operator/internal/controller/jumpstarter/endpoints/defaults.go b/deploy/operator/internal/controller/jumpstarter/endpoints/defaults.go new file mode 100644 index 00000000..efa515ec --- /dev/null +++ b/deploy/operator/internal/controller/jumpstarter/endpoints/defaults.go @@ -0,0 +1,61 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package endpoints + +import ( + "fmt" + + operatorv1alpha1 "github.com/jumpstarter-dev/jumpstarter-controller/deploy/operator/api/v1alpha1" +) + +// ApplyEndpointDefaults generates default endpoints for a JumpstarterSpec +// based on the baseDomain and cluster capabilities (Route vs Ingress availability). +func ApplyEndpointDefaults(spec *operatorv1alpha1.JumpstarterSpec, routeAvailable, ingressAvailable bool) { + // Skip endpoint generation if no baseDomain is set + if spec.BaseDomain == "" { + return + } + + // Generate default controller gRPC endpoint if none specified + if len(spec.Controller.GRPC.Endpoints) == 0 { + endpoint := operatorv1alpha1.Endpoint{ + Address: fmt.Sprintf("grpc.%s", spec.BaseDomain), + } + // Auto-select Route or Ingress based on cluster capabilities + if routeAvailable { + endpoint.Route = &operatorv1alpha1.RouteConfig{Enabled: true} + } else if ingressAvailable { + endpoint.Ingress = &operatorv1alpha1.IngressConfig{Enabled: true} + } + spec.Controller.GRPC.Endpoints = []operatorv1alpha1.Endpoint{endpoint} + } + + // Generate default router gRPC endpoints if none specified + if len(spec.Routers.GRPC.Endpoints) == 0 { + endpoint := operatorv1alpha1.Endpoint{ + // Use $(replica) placeholder for per-replica addresses + Address: fmt.Sprintf("router-$(replica).%s", spec.BaseDomain), + } + // Auto-select Route or Ingress based on cluster capabilities + if routeAvailable { + endpoint.Route = &operatorv1alpha1.RouteConfig{Enabled: true} + } else if ingressAvailable { + endpoint.Ingress = &operatorv1alpha1.IngressConfig{Enabled: true} + } + spec.Routers.GRPC.Endpoints = []operatorv1alpha1.Endpoint{endpoint} + } +} diff --git a/deploy/operator/internal/controller/jumpstarter/endpoints/endpoints.go b/deploy/operator/internal/controller/jumpstarter/endpoints/endpoints.go index 44f7403d..f96fe233 100644 --- a/deploy/operator/internal/controller/jumpstarter/endpoints/endpoints.go +++ b/deploy/operator/internal/controller/jumpstarter/endpoints/endpoints.go @@ -60,6 +60,12 @@ func NewReconciler(client client.Client, scheme *runtime.Scheme, config *rest.Co } } +// ApplyDefaults applies endpoint defaults to a JumpstarterSpec using the +// reconciler's discovered cluster capabilities (Route vs Ingress availability). +func (r *Reconciler) ApplyDefaults(spec *operatorv1alpha1.JumpstarterSpec) { + ApplyEndpointDefaults(spec, r.RouteAvailable, r.IngressAvailable) +} + // createOrUpdateService creates or updates a service with proper handling of immutable fields // and owner references. This is the unified service creation method. func (r *Reconciler) createOrUpdateService(ctx context.Context, service *corev1.Service, owner metav1.Object) error { @@ -195,14 +201,27 @@ func (r *Reconciler) ReconcileControllerEndpoint(ctx context.Context, owner meta } } - // If no service type is explicitly enabled, create a default ClusterIP service + // If no service type is explicitly enabled, auto-select based on cluster capabilities if (endpoint.LoadBalancer == nil || !endpoint.LoadBalancer.Enabled) && (endpoint.NodePort == nil || !endpoint.NodePort.Enabled) && (endpoint.ClusterIP == nil || !endpoint.ClusterIP.Enabled) && (endpoint.Ingress == nil || !endpoint.Ingress.Enabled) && (endpoint.Route == nil || !endpoint.Route.Enabled) { - // TODO: Default to Route or Ingress depending of the type of cluster + // Auto-select networking type based on cluster capabilities + if r.RouteAvailable { + // OpenShift cluster - use Route + if err := r.createRouteForEndpoint(ctx, owner, servicePort.Name, servicePort.Port, endpoint, baseLabels); err != nil { + return err + } + } else if r.IngressAvailable { + // Standard K8s cluster - use Ingress + if err := r.createIngressForEndpoint(ctx, owner, servicePort.Name, servicePort.Port, endpoint, baseLabels); err != nil { + return err + } + } + + // Always create ClusterIP service (needed by Route/Ingress, or as standalone fallback) if err := r.createService(ctx, owner, servicePort, "", corev1.ServiceTypeClusterIP, podSelector, baseLabels, nil, nil); err != nil { return err @@ -287,20 +306,33 @@ func (r *Reconciler) ReconcileRouterReplicaEndpoint(ctx context.Context, owner m } } - // If no service type is explicitly enabled, create a default ClusterIP service + // If no service type is explicitly enabled, auto-select based on cluster capabilities if (endpoint.LoadBalancer == nil || !endpoint.LoadBalancer.Enabled) && (endpoint.NodePort == nil || !endpoint.NodePort.Enabled) && (endpoint.ClusterIP == nil || !endpoint.ClusterIP.Enabled) && (endpoint.Ingress == nil || !endpoint.Ingress.Enabled) && (endpoint.Route == nil || !endpoint.Route.Enabled) { + + // Auto-select networking type based on cluster capabilities + if r.RouteAvailable { + // OpenShift cluster - use Route + if err := r.createRouteForEndpoint(ctx, owner, servicePort.Name, servicePort.Port, endpoint, baseLabels); err != nil { + return err + } + } else if r.IngressAvailable { + // Standard K8s cluster - use Ingress + if err := r.createIngressForEndpoint(ctx, owner, servicePort.Name, servicePort.Port, endpoint, baseLabels); err != nil { + return err + } + } + + // Always create ClusterIP service (needed by Route/Ingress, or as standalone fallback) if err := r.createService(ctx, owner, servicePort, "", corev1.ServiceTypeClusterIP, podSelector, baseLabels, nil, nil); err != nil { return err } } - // Note: Ingress resources are now created above. Route resources still need to be implemented. - return nil } diff --git a/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go b/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go index ced6c27b..08f00e41 100644 --- a/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go +++ b/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go @@ -128,6 +128,10 @@ func (r *JumpstarterReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{}, nil } + // Apply runtime-computed defaults (endpoints based on baseDomain and cluster capabilities) + // Static defaults are handled by the mutating webhook via kubebuilder annotations + r.EndpointReconciler.ApplyDefaults(&jumpstarter.Spec) + // Reconcile RBAC resources first if err := r.reconcileRBAC(ctx, &jumpstarter); err != nil { log.Error(err, "Failed to reconcile RBAC") From a1f73a1a43ad70c510015b09c819f0e95c029600 Mon Sep 17 00:00:00 2001 From: Evgeni Vakhonin Date: Tue, 16 Dec 2025 12:58:02 +0200 Subject: [PATCH 2/3] mark baseDomain as required, move service type logic --- .../api/v1alpha1/jumpstarter_types.go | 8 +-- ...tarter-operator.clusterserviceversion.yaml | 6 +-- ...operator.jumpstarter.dev_jumpstarters.yaml | 6 ++- ...operator.jumpstarter.dev_jumpstarters.yaml | 6 ++- deploy/operator/dist/install.yaml | 6 ++- .../jumpstarter/endpoints/defaults.go | 48 ++++++++++++----- .../jumpstarter/endpoints/endpoints.go | 54 ------------------- .../jumpstarter/jumpstarter_controller.go | 2 +- 8 files changed, 55 insertions(+), 81 deletions(-) diff --git a/deploy/operator/api/v1alpha1/jumpstarter_types.go b/deploy/operator/api/v1alpha1/jumpstarter_types.go index 962b9080..d16c9de4 100644 --- a/deploy/operator/api/v1alpha1/jumpstarter_types.go +++ b/deploy/operator/api/v1alpha1/jumpstarter_types.go @@ -131,9 +131,9 @@ type JumpstarterSpec struct { // Base domain used to construct FQDNs for all service endpoints. // This domain will be used to generate the default hostnames for Routes, Ingresses, and certificates. // Example: "example.com" will generate endpoints like "grpc.example.com", "router.example.com" - // +kubebuilder:default="jumpstarter.example.com" + // +kubebuilder:validation:Required // +kubebuilder:validation:Pattern=^[a-z0-9]([a-z0-9\-\.]*[a-z0-9])?$ - BaseDomain string `json:"baseDomain,omitempty"` + BaseDomain string `json:"baseDomain"` // Enable automatic TLS certificate management using cert-manager. // When enabled, jumpstarter will interact with cert-manager to automatically provision @@ -530,8 +530,8 @@ type Jumpstarter struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - // +kubebuilder:default={} - Spec JumpstarterSpec `json:"spec,omitempty"` + // +kubebuilder:validation:Required + Spec JumpstarterSpec `json:"spec"` Status JumpstarterStatus `json:"status,omitempty"` } diff --git a/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml b/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml index f6a02936..a545303a 100644 --- a/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml +++ b/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml @@ -14,13 +14,11 @@ metadata: }, "name": "jumpstarter-sample" }, - "spec": { - "baseDomain": "jumpstarter.example.com" - } + "spec": null } ] capabilities: Basic Install - createdAt: "2025-12-14T17:57:58Z" + createdAt: "2025-12-16T10:55:06Z" operators.operatorframework.io/builder: operator-sdk-v1.41.1 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 name: jumpstarter-operator.v0.8.0 diff --git a/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml b/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml index cf5182fb..c786863e 100644 --- a/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml +++ b/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml @@ -37,7 +37,6 @@ spec: metadata: type: object spec: - default: {} description: |- JumpstarterSpec defines the desired state of a Jumpstarter deployment. A deployment can be created in a namespace of the cluster, and that's where all the Jumpstarter @@ -424,7 +423,6 @@ spec: type: object type: object baseDomain: - default: jumpstarter.example.com description: |- Base domain used to construct FQDNs for all service endpoints. This domain will be used to generate the default hostnames for Routes, Ingresses, and certificates. @@ -1900,6 +1898,8 @@ spec: When enabled, jumpstarter will interact with cert-manager to automatically provision and renew TLS certificates for all endpoints. Requires cert-manager to be installed in the cluster. type: boolean + required: + - baseDomain type: object status: description: |- @@ -1907,6 +1907,8 @@ spec: This field is currently empty but can be extended to include status information such as deployment status, endpoint URLs, and health information. type: object + required: + - spec type: object served: true storage: true diff --git a/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml b/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml index cd065e4b..faf258e0 100644 --- a/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml +++ b/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml @@ -37,7 +37,6 @@ spec: metadata: type: object spec: - default: {} description: |- JumpstarterSpec defines the desired state of a Jumpstarter deployment. A deployment can be created in a namespace of the cluster, and that's where all the Jumpstarter @@ -424,7 +423,6 @@ spec: type: object type: object baseDomain: - default: jumpstarter.example.com description: |- Base domain used to construct FQDNs for all service endpoints. This domain will be used to generate the default hostnames for Routes, Ingresses, and certificates. @@ -1900,6 +1898,8 @@ spec: When enabled, jumpstarter will interact with cert-manager to automatically provision and renew TLS certificates for all endpoints. Requires cert-manager to be installed in the cluster. type: boolean + required: + - baseDomain type: object status: description: |- @@ -1907,6 +1907,8 @@ spec: This field is currently empty but can be extended to include status information such as deployment status, endpoint URLs, and health information. type: object + required: + - spec type: object served: true storage: true diff --git a/deploy/operator/dist/install.yaml b/deploy/operator/dist/install.yaml index ce66c231..eb956f08 100644 --- a/deploy/operator/dist/install.yaml +++ b/deploy/operator/dist/install.yaml @@ -440,7 +440,6 @@ spec: metadata: type: object spec: - default: {} description: |- JumpstarterSpec defines the desired state of a Jumpstarter deployment. A deployment can be created in a namespace of the cluster, and that's where all the Jumpstarter @@ -827,7 +826,6 @@ spec: type: object type: object baseDomain: - default: jumpstarter.example.com description: |- Base domain used to construct FQDNs for all service endpoints. This domain will be used to generate the default hostnames for Routes, Ingresses, and certificates. @@ -2303,6 +2301,8 @@ spec: When enabled, jumpstarter will interact with cert-manager to automatically provision and renew TLS certificates for all endpoints. Requires cert-manager to be installed in the cluster. type: boolean + required: + - baseDomain type: object status: description: |- @@ -2310,6 +2310,8 @@ spec: This field is currently empty but can be extended to include status information such as deployment status, endpoint URLs, and health information. type: object + required: + - spec type: object served: true storage: true diff --git a/deploy/operator/internal/controller/jumpstarter/endpoints/defaults.go b/deploy/operator/internal/controller/jumpstarter/endpoints/defaults.go index efa515ec..830eb71a 100644 --- a/deploy/operator/internal/controller/jumpstarter/endpoints/defaults.go +++ b/deploy/operator/internal/controller/jumpstarter/endpoints/defaults.go @@ -22,8 +22,32 @@ import ( operatorv1alpha1 "github.com/jumpstarter-dev/jumpstarter-controller/deploy/operator/api/v1alpha1" ) +// ensureEndpointServiceType ensures an endpoint has a service type enabled. +// If no service type is enabled, it auto-selects Route (if available), Ingress (if available), +// or ClusterIP as a fallback. +func ensureEndpointServiceType(endpoint *operatorv1alpha1.Endpoint, routeAvailable, ingressAvailable bool) { + // Skip if any service type is already enabled + if (endpoint.Route != nil && endpoint.Route.Enabled) || + (endpoint.Ingress != nil && endpoint.Ingress.Enabled) || + (endpoint.LoadBalancer != nil && endpoint.LoadBalancer.Enabled) || + (endpoint.NodePort != nil && endpoint.NodePort.Enabled) || + (endpoint.ClusterIP != nil && endpoint.ClusterIP.Enabled) { + return + } + + // Auto-select based on cluster capabilities, fallback to ClusterIP + if routeAvailable { + endpoint.Route = &operatorv1alpha1.RouteConfig{Enabled: true} + } else if ingressAvailable { + endpoint.Ingress = &operatorv1alpha1.IngressConfig{Enabled: true} + } else { + endpoint.ClusterIP = &operatorv1alpha1.ClusterIPConfig{Enabled: true} + } +} + // ApplyEndpointDefaults generates default endpoints for a JumpstarterSpec // based on the baseDomain and cluster capabilities (Route vs Ingress availability). +// It also ensures all existing endpoints have a service type enabled. func ApplyEndpointDefaults(spec *operatorv1alpha1.JumpstarterSpec, routeAvailable, ingressAvailable bool) { // Skip endpoint generation if no baseDomain is set if spec.BaseDomain == "" { @@ -35,13 +59,13 @@ func ApplyEndpointDefaults(spec *operatorv1alpha1.JumpstarterSpec, routeAvailabl endpoint := operatorv1alpha1.Endpoint{ Address: fmt.Sprintf("grpc.%s", spec.BaseDomain), } - // Auto-select Route or Ingress based on cluster capabilities - if routeAvailable { - endpoint.Route = &operatorv1alpha1.RouteConfig{Enabled: true} - } else if ingressAvailable { - endpoint.Ingress = &operatorv1alpha1.IngressConfig{Enabled: true} - } + ensureEndpointServiceType(&endpoint, routeAvailable, ingressAvailable) spec.Controller.GRPC.Endpoints = []operatorv1alpha1.Endpoint{endpoint} + } else { + // Ensure existing endpoints have a service type enabled + for i := range spec.Controller.GRPC.Endpoints { + ensureEndpointServiceType(&spec.Controller.GRPC.Endpoints[i], routeAvailable, ingressAvailable) + } } // Generate default router gRPC endpoints if none specified @@ -50,12 +74,12 @@ func ApplyEndpointDefaults(spec *operatorv1alpha1.JumpstarterSpec, routeAvailabl // Use $(replica) placeholder for per-replica addresses Address: fmt.Sprintf("router-$(replica).%s", spec.BaseDomain), } - // Auto-select Route or Ingress based on cluster capabilities - if routeAvailable { - endpoint.Route = &operatorv1alpha1.RouteConfig{Enabled: true} - } else if ingressAvailable { - endpoint.Ingress = &operatorv1alpha1.IngressConfig{Enabled: true} - } + ensureEndpointServiceType(&endpoint, routeAvailable, ingressAvailable) spec.Routers.GRPC.Endpoints = []operatorv1alpha1.Endpoint{endpoint} + } else { + // Ensure existing endpoints have a service type enabled + for i := range spec.Routers.GRPC.Endpoints { + ensureEndpointServiceType(&spec.Routers.GRPC.Endpoints[i], routeAvailable, ingressAvailable) + } } } diff --git a/deploy/operator/internal/controller/jumpstarter/endpoints/endpoints.go b/deploy/operator/internal/controller/jumpstarter/endpoints/endpoints.go index f96fe233..15db7e06 100644 --- a/deploy/operator/internal/controller/jumpstarter/endpoints/endpoints.go +++ b/deploy/operator/internal/controller/jumpstarter/endpoints/endpoints.go @@ -201,33 +201,6 @@ func (r *Reconciler) ReconcileControllerEndpoint(ctx context.Context, owner meta } } - // If no service type is explicitly enabled, auto-select based on cluster capabilities - if (endpoint.LoadBalancer == nil || !endpoint.LoadBalancer.Enabled) && - (endpoint.NodePort == nil || !endpoint.NodePort.Enabled) && - (endpoint.ClusterIP == nil || !endpoint.ClusterIP.Enabled) && - (endpoint.Ingress == nil || !endpoint.Ingress.Enabled) && - (endpoint.Route == nil || !endpoint.Route.Enabled) { - - // Auto-select networking type based on cluster capabilities - if r.RouteAvailable { - // OpenShift cluster - use Route - if err := r.createRouteForEndpoint(ctx, owner, servicePort.Name, servicePort.Port, endpoint, baseLabels); err != nil { - return err - } - } else if r.IngressAvailable { - // Standard K8s cluster - use Ingress - if err := r.createIngressForEndpoint(ctx, owner, servicePort.Name, servicePort.Port, endpoint, baseLabels); err != nil { - return err - } - } - - // Always create ClusterIP service (needed by Route/Ingress, or as standalone fallback) - if err := r.createService(ctx, owner, servicePort, "", corev1.ServiceTypeClusterIP, - podSelector, baseLabels, nil, nil); err != nil { - return err - } - } - return nil } @@ -306,33 +279,6 @@ func (r *Reconciler) ReconcileRouterReplicaEndpoint(ctx context.Context, owner m } } - // If no service type is explicitly enabled, auto-select based on cluster capabilities - if (endpoint.LoadBalancer == nil || !endpoint.LoadBalancer.Enabled) && - (endpoint.NodePort == nil || !endpoint.NodePort.Enabled) && - (endpoint.ClusterIP == nil || !endpoint.ClusterIP.Enabled) && - (endpoint.Ingress == nil || !endpoint.Ingress.Enabled) && - (endpoint.Route == nil || !endpoint.Route.Enabled) { - - // Auto-select networking type based on cluster capabilities - if r.RouteAvailable { - // OpenShift cluster - use Route - if err := r.createRouteForEndpoint(ctx, owner, servicePort.Name, servicePort.Port, endpoint, baseLabels); err != nil { - return err - } - } else if r.IngressAvailable { - // Standard K8s cluster - use Ingress - if err := r.createIngressForEndpoint(ctx, owner, servicePort.Name, servicePort.Port, endpoint, baseLabels); err != nil { - return err - } - } - - // Always create ClusterIP service (needed by Route/Ingress, or as standalone fallback) - if err := r.createService(ctx, owner, servicePort, "", corev1.ServiceTypeClusterIP, - podSelector, baseLabels, nil, nil); err != nil { - return err - } - } - return nil } diff --git a/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go b/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go index 08f00e41..7ad187bf 100644 --- a/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go +++ b/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go @@ -129,7 +129,7 @@ func (r *JumpstarterReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // Apply runtime-computed defaults (endpoints based on baseDomain and cluster capabilities) - // Static defaults are handled by the mutating webhook via kubebuilder annotations + // Static defaults are handled by kubebuilder annotations in the CRD schema r.EndpointReconciler.ApplyDefaults(&jumpstarter.Spec) // Reconcile RBAC resources first From f16765b203f3385af1dbe07a1523eb76b720ccdb Mon Sep 17 00:00:00 2001 From: Evgeni Vakhonin Date: Wed, 17 Dec 2025 17:44:20 +0200 Subject: [PATCH 3/3] lift the hard requirement --- deploy/operator/api/v1alpha1/jumpstarter_types.go | 6 ++---- .../jumpstarter-operator.clusterserviceversion.yaml | 2 +- .../manifests/operator.jumpstarter.dev_jumpstarters.yaml | 4 ---- .../crd/bases/operator.jumpstarter.dev_jumpstarters.yaml | 4 ---- deploy/operator/dist/install.yaml | 4 ---- 5 files changed, 3 insertions(+), 17 deletions(-) diff --git a/deploy/operator/api/v1alpha1/jumpstarter_types.go b/deploy/operator/api/v1alpha1/jumpstarter_types.go index d16c9de4..be463579 100644 --- a/deploy/operator/api/v1alpha1/jumpstarter_types.go +++ b/deploy/operator/api/v1alpha1/jumpstarter_types.go @@ -131,9 +131,8 @@ type JumpstarterSpec struct { // Base domain used to construct FQDNs for all service endpoints. // This domain will be used to generate the default hostnames for Routes, Ingresses, and certificates. // Example: "example.com" will generate endpoints like "grpc.example.com", "router.example.com" - // +kubebuilder:validation:Required // +kubebuilder:validation:Pattern=^[a-z0-9]([a-z0-9\-\.]*[a-z0-9])?$ - BaseDomain string `json:"baseDomain"` + BaseDomain string `json:"baseDomain,omitempty"` // Enable automatic TLS certificate management using cert-manager. // When enabled, jumpstarter will interact with cert-manager to automatically provision @@ -530,8 +529,7 @@ type Jumpstarter struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - // +kubebuilder:validation:Required - Spec JumpstarterSpec `json:"spec"` + Spec JumpstarterSpec `json:"spec,omitempty"` Status JumpstarterStatus `json:"status,omitempty"` } diff --git a/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml b/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml index a545303a..1fbea466 100644 --- a/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml +++ b/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml @@ -18,7 +18,7 @@ metadata: } ] capabilities: Basic Install - createdAt: "2025-12-16T10:55:06Z" + createdAt: "2025-12-17T15:29:25Z" operators.operatorframework.io/builder: operator-sdk-v1.41.1 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 name: jumpstarter-operator.v0.8.0 diff --git a/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml b/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml index c786863e..50443c42 100644 --- a/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml +++ b/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml @@ -1898,8 +1898,6 @@ spec: When enabled, jumpstarter will interact with cert-manager to automatically provision and renew TLS certificates for all endpoints. Requires cert-manager to be installed in the cluster. type: boolean - required: - - baseDomain type: object status: description: |- @@ -1907,8 +1905,6 @@ spec: This field is currently empty but can be extended to include status information such as deployment status, endpoint URLs, and health information. type: object - required: - - spec type: object served: true storage: true diff --git a/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml b/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml index faf258e0..c6f7c6d8 100644 --- a/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml +++ b/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml @@ -1898,8 +1898,6 @@ spec: When enabled, jumpstarter will interact with cert-manager to automatically provision and renew TLS certificates for all endpoints. Requires cert-manager to be installed in the cluster. type: boolean - required: - - baseDomain type: object status: description: |- @@ -1907,8 +1905,6 @@ spec: This field is currently empty but can be extended to include status information such as deployment status, endpoint URLs, and health information. type: object - required: - - spec type: object served: true storage: true diff --git a/deploy/operator/dist/install.yaml b/deploy/operator/dist/install.yaml index eb956f08..9ea70a33 100644 --- a/deploy/operator/dist/install.yaml +++ b/deploy/operator/dist/install.yaml @@ -2301,8 +2301,6 @@ spec: When enabled, jumpstarter will interact with cert-manager to automatically provision and renew TLS certificates for all endpoints. Requires cert-manager to be installed in the cluster. type: boolean - required: - - baseDomain type: object status: description: |- @@ -2310,8 +2308,6 @@ spec: This field is currently empty but can be extended to include status information such as deployment status, endpoint URLs, and health information. type: object - required: - - spec type: object served: true storage: true