diff --git a/deploy/operator/api/v1alpha1/jumpstarter_types.go b/deploy/operator/api/v1alpha1/jumpstarter_types.go index db9149d..be46357 100644 --- a/deploy/operator/api/v1alpha1/jumpstarter_types.go +++ b/deploy/operator/api/v1alpha1/jumpstarter_types.go @@ -142,10 +142,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 +160,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 +195,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. diff --git a/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml b/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml index e34de0a..1fbea46 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-11-25T08:56:27Z" + 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 7b28cb8..50443c4 100644 --- a/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml +++ b/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml @@ -430,6 +430,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 +1084,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 +1370,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 +1629,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 dbb182c..c6f7c6d 100644 --- a/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml +++ b/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml @@ -430,6 +430,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 +1084,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 +1370,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 +1629,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 7e93f93..8ac4834 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 2216c69..9ea70a3 100644 --- a/deploy/operator/dist/install.yaml +++ b/deploy/operator/dist/install.yaml @@ -833,6 +833,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 +1487,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 +1773,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 +2032,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 0000000..830eb71 --- /dev/null +++ b/deploy/operator/internal/controller/jumpstarter/endpoints/defaults.go @@ -0,0 +1,85 @@ +/* +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" +) + +// 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 == "" { + 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), + } + 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 + 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), + } + 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 44f7403..15db7e0 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,20 +201,6 @@ func (r *Reconciler) ReconcileControllerEndpoint(ctx context.Context, owner meta } } - // If no service type is explicitly enabled, create a default ClusterIP service - 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 - if err := r.createService(ctx, owner, servicePort, "", corev1.ServiceTypeClusterIP, - podSelector, baseLabels, nil, nil); err != nil { - return err - } - } - return nil } @@ -287,20 +279,6 @@ func (r *Reconciler) ReconcileRouterReplicaEndpoint(ctx context.Context, owner m } } - // If no service type is explicitly enabled, create a default ClusterIP service - 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) { - 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 ced6c27..7ad187b 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 kubebuilder annotations in the CRD schema + r.EndpointReconciler.ApplyDefaults(&jumpstarter.Spec) + // Reconcile RBAC resources first if err := r.reconcileRBAC(ctx, &jumpstarter); err != nil { log.Error(err, "Failed to reconcile RBAC")