diff --git a/deploy/operator/internal/controller/jumpstarter/endpoints/endpoints.go b/deploy/operator/internal/controller/jumpstarter/endpoints/endpoints.go index aed06ee3..0f7f5e33 100644 --- a/deploy/operator/internal/controller/jumpstarter/endpoints/endpoints.go +++ b/deploy/operator/internal/controller/jumpstarter/endpoints/endpoints.go @@ -28,6 +28,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" operatorv1alpha1 "github.com/jumpstarter-dev/jumpstarter-controller/deploy/operator/api/v1alpha1" + "github.com/jumpstarter-dev/jumpstarter-controller/deploy/operator/internal/utils" ) // Reconciler provides endpoint reconciliation functionality @@ -127,6 +128,15 @@ func (r *Reconciler) ReconcileControllerEndpoint(ctx context.Context, owner meta // Note: ClusterIP uses no suffix (most common for in-cluster communication) // LoadBalancer uses "-lb" suffix, NodePort uses "-np" suffix + // Ingress resource (uses ClusterIP service) + if endpoint.Ingress != nil && endpoint.Ingress.Enabled { + serviceName := servicePort.Name + // Create the Ingress resource pointing to the ClusterIP service + if err := r.createIngressForEndpoint(ctx, owner, serviceName, servicePort.Port, endpoint, baseLabels); err != nil { + return err + } + } + // LoadBalancer service if endpoint.LoadBalancer != nil && endpoint.LoadBalancer.Enabled { if err := r.createService(ctx, owner, servicePort, "-lb", corev1.ServiceTypeLoadBalancer, @@ -144,9 +154,18 @@ func (r *Reconciler) ReconcileControllerEndpoint(ctx context.Context, owner meta } // ClusterIP service (no suffix for cleaner in-cluster service names) - if endpoint.ClusterIP != nil && endpoint.ClusterIP.Enabled { + // Create ClusterIP if explicitly enabled OR if Ingress/Route need it + if (endpoint.ClusterIP != nil && endpoint.ClusterIP.Enabled) || + (endpoint.Ingress != nil && endpoint.Ingress.Enabled) || + (endpoint.Route != nil && endpoint.Route.Enabled) { + // Merge annotations and labels from ClusterIP config if present + var annotations, labels map[string]string + if endpoint.ClusterIP != nil { + annotations = endpoint.ClusterIP.Annotations + labels = endpoint.ClusterIP.Labels + } if err := r.createService(ctx, owner, servicePort, "", corev1.ServiceTypeClusterIP, - podSelector, baseLabels, endpoint.ClusterIP.Annotations, endpoint.ClusterIP.Labels); err != nil { + podSelector, baseLabels, annotations, labels); err != nil { return err } } @@ -154,7 +173,9 @@ 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.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, @@ -190,10 +211,11 @@ func (r *Reconciler) ReconcileRouterReplicaEndpoint(ctx context.Context, owner m // Note: ClusterIP uses no suffix (most common for in-cluster communication) // LoadBalancer uses "-lb" suffix, NodePort uses "-np" suffix - // Ingress service + // Ingress resource (uses ClusterIP service) if endpoint.Ingress != nil && endpoint.Ingress.Enabled { - if err := r.createService(ctx, owner, servicePort, "-ing", corev1.ServiceTypeClusterIP, - podSelector, baseLabels, endpoint.Ingress.Annotations, endpoint.Ingress.Labels); err != nil { + serviceName := servicePort.Name + // Create the Ingress resource pointing to the ClusterIP service + if err := r.createIngressForEndpoint(ctx, owner, serviceName, servicePort.Port, endpoint, baseLabels); err != nil { return err } } @@ -223,9 +245,18 @@ func (r *Reconciler) ReconcileRouterReplicaEndpoint(ctx context.Context, owner m } // ClusterIP service (no suffix for cleaner in-cluster service names) - if endpoint.ClusterIP != nil && endpoint.ClusterIP.Enabled { + // Create ClusterIP if explicitly enabled OR if Ingress/Route need it + if (endpoint.ClusterIP != nil && endpoint.ClusterIP.Enabled) || + (endpoint.Ingress != nil && endpoint.Ingress.Enabled) || + (endpoint.Route != nil && endpoint.Route.Enabled) { + // Merge annotations and labels from ClusterIP config if present + var annotations, labels map[string]string + if endpoint.ClusterIP != nil { + annotations = endpoint.ClusterIP.Annotations + labels = endpoint.ClusterIP.Labels + } if err := r.createService(ctx, owner, servicePort, "", corev1.ServiceTypeClusterIP, - podSelector, baseLabels, endpoint.ClusterIP.Annotations, endpoint.ClusterIP.Labels); err != nil { + podSelector, baseLabels, annotations, labels); err != nil { return err } } @@ -242,8 +273,7 @@ func (r *Reconciler) ReconcileRouterReplicaEndpoint(ctx context.Context, owner m } } - // TODO: Create ingress/route resources here instead of calling the deprecated ReconcileEndpoint - // For now, ingress and route are handled by creating ClusterIP services above + // Note: Ingress resources are now created above. Route resources still need to be implemented. return nil } @@ -257,14 +287,8 @@ func (r *Reconciler) createService(ctx context.Context, owner metav1.Object, ser // Build service name with suffix to avoid conflicts serviceName := servicePort.Name + nameSuffix - // Merge labels - serviceLabels := make(map[string]string) - for k, v := range baseLabels { - serviceLabels[k] = v - } - for k, v := range extraLabels { - serviceLabels[k] = v - } + // Merge labels (extra labels take precedence) + serviceLabels := utils.MergeMaps(baseLabels, extraLabels) // Ensure annotations map is initialized if annotations == nil { diff --git a/deploy/operator/internal/controller/jumpstarter/endpoints/endpoints_test.go b/deploy/operator/internal/controller/jumpstarter/endpoints/endpoints_test.go index 12f57d1c..1de1d1e7 100644 --- a/deploy/operator/internal/controller/jumpstarter/endpoints/endpoints_test.go +++ b/deploy/operator/internal/controller/jumpstarter/endpoints/endpoints_test.go @@ -22,6 +22,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -254,9 +255,189 @@ var _ = Describe("Endpoints Reconciler", func() { }) }) + Context("with Ingress enabled", func() { + It("should create a ClusterIP service and Ingress with default nginx annotations", func() { + endpoint := &operatorv1alpha1.Endpoint{ + Address: "grpc.example.com:443", + Ingress: &operatorv1alpha1.IngressConfig{ + Enabled: true, + Class: "nginx", + }, + } + + svcPort := corev1.ServicePort{ + Name: "controller-grpc", + Port: 8082, + TargetPort: intstr.FromInt(8082), + Protocol: corev1.ProtocolTCP, + } + + err := reconciler.ReconcileControllerEndpoint(ctx, owner, endpoint, svcPort) + Expect(err).NotTo(HaveOccurred()) + + // Verify the ClusterIP service was created (used by ingress) + service := &corev1.Service{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: "controller-grpc", + Namespace: namespace, + }, service) + Expect(err).NotTo(HaveOccurred()) + Expect(service.Spec.Type).To(Equal(corev1.ServiceTypeClusterIP)) + Expect(service.Spec.Selector["app"]).To(Equal("jumpstarter-controller")) + + // Verify the Ingress was created + ingress := &networkingv1.Ingress{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: "controller-grpc-ing", + Namespace: namespace, + }, ingress) + Expect(err).NotTo(HaveOccurred()) + + // Verify ingress class + Expect(ingress.Spec.IngressClassName).NotTo(BeNil()) + Expect(*ingress.Spec.IngressClassName).To(Equal("nginx")) + + // Verify default nginx annotations for TLS passthrough + Expect(ingress.Annotations["nginx.ingress.kubernetes.io/ssl-redirect"]).To(Equal("true")) + Expect(ingress.Annotations["nginx.ingress.kubernetes.io/backend-protocol"]).To(Equal("GRPC")) + Expect(ingress.Annotations["nginx.ingress.kubernetes.io/proxy-read-timeout"]).To(Equal("300")) + Expect(ingress.Annotations["nginx.ingress.kubernetes.io/proxy-send-timeout"]).To(Equal("300")) + Expect(ingress.Annotations["nginx.ingress.kubernetes.io/ssl-passthrough"]).To(Equal("true")) + + // Verify ingress rules + Expect(ingress.Spec.Rules).To(HaveLen(1)) + Expect(ingress.Spec.Rules[0].Host).To(Equal("grpc.example.com")) + Expect(ingress.Spec.Rules[0].HTTP.Paths).To(HaveLen(1)) + Expect(ingress.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name).To(Equal("controller-grpc")) + Expect(ingress.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Port.Number).To(Equal(int32(8082))) + + // Verify TLS config + Expect(ingress.Spec.TLS).To(HaveLen(1)) + Expect(ingress.Spec.TLS[0].Hosts).To(ContainElement("grpc.example.com")) + }) + + It("should merge user annotations with defaults (user takes precedence)", func() { + endpoint := &operatorv1alpha1.Endpoint{ + Address: "grpc.example.com", + Ingress: &operatorv1alpha1.IngressConfig{ + Enabled: true, + Class: "nginx", + Annotations: map[string]string{ + "nginx.ingress.kubernetes.io/ssl-redirect": "false", // override default + "custom.annotation/key": "custom-value", + }, + Labels: map[string]string{ + "environment": "production", + }, + }, + } + + svcPort := corev1.ServicePort{ + Name: "controller-grpc", + Port: 8082, + TargetPort: intstr.FromInt(8082), + Protocol: corev1.ProtocolTCP, + } + + err := reconciler.ReconcileControllerEndpoint(ctx, owner, endpoint, svcPort) + Expect(err).NotTo(HaveOccurred()) + + // Verify the Ingress was created + ingress := &networkingv1.Ingress{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: "controller-grpc-ing", + Namespace: namespace, + }, ingress) + Expect(err).NotTo(HaveOccurred()) + + // User annotation should override default + Expect(ingress.Annotations["nginx.ingress.kubernetes.io/ssl-redirect"]).To(Equal("false")) + // Custom annotation should be present + Expect(ingress.Annotations["custom.annotation/key"]).To(Equal("custom-value")) + // Other defaults should still be present + Expect(ingress.Annotations["nginx.ingress.kubernetes.io/backend-protocol"]).To(Equal("GRPC")) + + // User labels should be present + Expect(ingress.Labels["environment"]).To(Equal("production")) + }) + + It("should extract hostname from various address formats", func() { + testCases := []struct { + address string + expectedHost string + }{ + {"grpc.example.com", "grpc.example.com"}, + {"grpc.example.com:443", "grpc.example.com"}, + {"grpc.example.com:8080", "grpc.example.com"}, + } + + for _, tc := range testCases { + endpoint := &operatorv1alpha1.Endpoint{ + Address: tc.address, + Ingress: &operatorv1alpha1.IngressConfig{ + Enabled: true, + }, + } + + svcPort := corev1.ServicePort{ + Name: "test-svc", + Port: 8082, + TargetPort: intstr.FromInt(8082), + Protocol: corev1.ProtocolTCP, + } + + err := reconciler.ReconcileControllerEndpoint(ctx, owner, endpoint, svcPort) + Expect(err).NotTo(HaveOccurred()) + + // Verify ingress was created with correct hostname + ingress := &networkingv1.Ingress{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: "test-svc-ing", + Namespace: namespace, + }, ingress) + Expect(err).NotTo(HaveOccurred()) + Expect(ingress.Spec.Rules[0].Host).To(Equal(tc.expectedHost)) + + // Clean up + _ = k8sClient.Delete(ctx, ingress) + svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test-svc", Namespace: namespace}} + _ = k8sClient.Delete(ctx, svc) + } + }) + + It("should not set ingress class when not specified", func() { + endpoint := &operatorv1alpha1.Endpoint{ + Address: "grpc.example.com", + Ingress: &operatorv1alpha1.IngressConfig{ + Enabled: true, + // Class not specified - will use cluster default + }, + } + + svcPort := corev1.ServicePort{ + Name: "controller-grpc", + Port: 8082, + TargetPort: intstr.FromInt(8082), + Protocol: corev1.ProtocolTCP, + } + + err := reconciler.ReconcileControllerEndpoint(ctx, owner, endpoint, svcPort) + Expect(err).NotTo(HaveOccurred()) + + // Verify ingress class is nil (will use cluster default IngressClass) + ingress := &networkingv1.Ingress{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: "controller-grpc-ing", + Namespace: namespace, + }, ingress) + Expect(err).NotTo(HaveOccurred()) + Expect(ingress.Spec.IngressClassName).To(BeNil()) + }) + }) + AfterEach(func() { // Clean up created services - services := []string{"controller", "controller-lb", "controller-np"} + services := []string{"controller", "controller-lb", "controller-np", "controller-grpc", "test-svc"} for _, svcName := range services { service := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -267,6 +448,18 @@ var _ = Describe("Endpoints Reconciler", func() { _ = k8sClient.Delete(ctx, service) } + // Clean up ingresses + ingresses := []string{"controller-grpc-ing", "test-svc-ing"} + for _, ingName := range ingresses { + ingress := &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: ingName, + Namespace: namespace, + }, + } + _ = k8sClient.Delete(ctx, ingress) + } + // Clean up owner _ = k8sClient.Delete(ctx, owner) }) @@ -344,9 +537,61 @@ var _ = Describe("Endpoints Reconciler", func() { }) }) + Context("with Ingress enabled for router", func() { + It("should create a ClusterIP service and Ingress for router replica", func() { + endpoint := &operatorv1alpha1.Endpoint{ + Address: "router-0.example.com", + Ingress: &operatorv1alpha1.IngressConfig{ + Enabled: true, + Class: "nginx", + Annotations: map[string]string{ + "router.annotation": "value", + }, + }, + } + + svcPort := corev1.ServicePort{ + Name: "router-grpc", + Port: 8083, + TargetPort: intstr.FromInt(8083), + Protocol: corev1.ProtocolTCP, + } + + err := reconciler.ReconcileRouterReplicaEndpoint(ctx, owner, replicaIdx, endpointIdx, endpoint, svcPort) + Expect(err).NotTo(HaveOccurred()) + + // Verify the ClusterIP service was created + service := &corev1.Service{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: "router-grpc", + Namespace: namespace, + }, service) + Expect(err).NotTo(HaveOccurred()) + Expect(service.Spec.Type).To(Equal(corev1.ServiceTypeClusterIP)) + Expect(service.Spec.Selector["app"]).To(Equal("test-router-router-0")) + + // Verify the Ingress was created + ingress := &networkingv1.Ingress{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: "router-grpc-ing", + Namespace: namespace, + }, ingress) + Expect(err).NotTo(HaveOccurred()) + + // Verify ingress configuration + Expect(*ingress.Spec.IngressClassName).To(Equal("nginx")) + Expect(ingress.Spec.Rules[0].Host).To(Equal("router-0.example.com")) + Expect(ingress.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name).To(Equal("router-grpc")) + + // Verify user and default annotations + Expect(ingress.Annotations["router.annotation"]).To(Equal("value")) + Expect(ingress.Annotations["nginx.ingress.kubernetes.io/ssl-passthrough"]).To(Equal("true")) + }) + }) + AfterEach(func() { // Clean up created services - services := []string{"router", "router-lb", "router-np", "router-ing", "router-route"} + services := []string{"router", "router-lb", "router-np", "router-ing", "router-route", "router-grpc"} for _, svcName := range services { service := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -357,6 +602,18 @@ var _ = Describe("Endpoints Reconciler", func() { _ = k8sClient.Delete(ctx, service) } + // Clean up ingresses + ingresses := []string{"router-grpc-ing"} + for _, ingName := range ingresses { + ingress := &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: ingName, + Namespace: namespace, + }, + } + _ = k8sClient.Delete(ctx, ingress) + } + // Clean up owner _ = k8sClient.Delete(ctx, owner) }) diff --git a/deploy/operator/internal/controller/jumpstarter/endpoints/ingress.go b/deploy/operator/internal/controller/jumpstarter/endpoints/ingress.go new file mode 100644 index 00000000..6b2ce655 --- /dev/null +++ b/deploy/operator/internal/controller/jumpstarter/endpoints/ingress.go @@ -0,0 +1,183 @@ +/* +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 ( + "context" + "errors" + "strings" + + networkingv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + operatorv1alpha1 "github.com/jumpstarter-dev/jumpstarter-controller/deploy/operator/api/v1alpha1" + "github.com/jumpstarter-dev/jumpstarter-controller/deploy/operator/internal/utils" +) + +// createOrUpdateIngress creates or updates an ingress with proper handling of mutable fields +// and owner references. This follows the same pattern as createOrUpdateService. +func (r *Reconciler) createOrUpdateIngress(ctx context.Context, ingress *networkingv1.Ingress, owner metav1.Object) error { + log := logf.FromContext(ctx) + + existingIngress := &networkingv1.Ingress{} + existingIngress.Name = ingress.Name + existingIngress.Namespace = ingress.Namespace + + op, err := controllerutil.CreateOrUpdate(ctx, r.Client, existingIngress, func() error { + // Update all mutable fields + existingIngress.Labels = ingress.Labels + existingIngress.Annotations = ingress.Annotations + existingIngress.Spec.IngressClassName = ingress.Spec.IngressClassName + existingIngress.Spec.Rules = ingress.Spec.Rules + existingIngress.Spec.TLS = ingress.Spec.TLS + + return controllerutil.SetControllerReference(owner, existingIngress, r.Scheme) + }) + + if err != nil { + log.Error(err, "Failed to reconcile ingress", + "name", ingress.Name, + "namespace", ingress.Namespace) + return err + } + + log.Info("Ingress reconciled", + "name", ingress.Name, + "namespace", ingress.Namespace, + "operation", op) + + return nil +} + +// extractHostname extracts the hostname from an endpoint address. +// It handles formats like: "hostname", "hostname:port", "IPv4:port", "[IPv6]", "[IPv6]:port" +func extractHostname(address string) string { + // Handle IPv6 addresses in brackets + if strings.HasPrefix(address, "[") { + // Find the closing bracket + if idx := strings.Index(address, "]"); idx != -1 { + return address[1:idx] + } + return address + } + + // For hostname or IPv4, strip port if present + if idx := strings.LastIndex(address, ":"); idx != -1 { + // Check if this is part of an IPv6 address (no brackets) + // Count colons - if more than one, likely IPv6 + if strings.Count(address, ":") > 1 { + return address + } + return address[:idx] + } + + return address +} + +// createIngressForEndpoint creates an ingress for a specific endpoint. +// The ingress points to the ClusterIP service (serviceName with no suffix). +func (r *Reconciler) createIngressForEndpoint(ctx context.Context, owner metav1.Object, serviceName string, servicePort int32, + endpoint *operatorv1alpha1.Endpoint, baseLabels map[string]string) error { + + // Extract hostname from address + hostname := extractHostname(endpoint.Address) + if hostname == "" { + log := logf.FromContext(ctx) + log.Info("Skipping ingress creation: no hostname in endpoint address", + "address", endpoint.Address) + return nil + } + + if errs := validation.IsDNS1123Subdomain(hostname); errs != nil { + log := logf.FromContext(ctx) + log.Error(errors.New(strings.Join(errs, ", ")), "Skipping ingress creation: invalid hostname", + "address", endpoint.Address, + "hostname", hostname) + // TODO: propagate error to status conditions + return nil + } + + // Build default annotations for TLS passthrough with GRPC with nginx ingress + defaultAnnotations := map[string]string{ + "nginx.ingress.kubernetes.io/ssl-redirect": "true", + "nginx.ingress.kubernetes.io/backend-protocol": "GRPC", + "nginx.ingress.kubernetes.io/proxy-read-timeout": "300", + "nginx.ingress.kubernetes.io/proxy-send-timeout": "300", + "nginx.ingress.kubernetes.io/ssl-passthrough": "true", + } + + // Merge with user-provided annotations (user annotations take precedence) + annotations := utils.MergeMaps(defaultAnnotations, endpoint.Ingress.Annotations) + + // Merge labels (user labels take precedence) + ingressLabels := utils.MergeMaps(baseLabels, endpoint.Ingress.Labels) + + // Set ingress class name (only if specified, cannot be empty string) + var ingressClassName *string + if endpoint.Ingress.Class != "" { + ingressClassName = &endpoint.Ingress.Class + } + + // Build path type + pathTypePrefix := networkingv1.PathTypePrefix + + ingress := &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName + "-ing", + Namespace: owner.GetNamespace(), + Labels: ingressLabels, + Annotations: annotations, + }, + Spec: networkingv1.IngressSpec{ + IngressClassName: ingressClassName, + Rules: []networkingv1.IngressRule{ + { + Host: hostname, + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: "/", + PathType: &pathTypePrefix, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: serviceName, + Port: networkingv1.ServiceBackendPort{ + Number: servicePort, + }, + }, + }, + }, + }, + }, + }, + }, + }, + TLS: []networkingv1.IngressTLS{ + { + Hosts: []string{hostname}, + // No SecretName - passthrough mode handles TLS at the backend + }, + }, + }, + } + + return r.createOrUpdateIngress(ctx, ingress, owner) +} diff --git a/deploy/operator/internal/utils/utils.go b/deploy/operator/internal/utils/utils.go new file mode 100644 index 00000000..9796da5d --- /dev/null +++ b/deploy/operator/internal/utils/utils.go @@ -0,0 +1,19 @@ +package utils + +// MergeMaps merges two string maps, with values from the second map taking precedence. +// This is useful for merging labels, annotations, or any other string key-value pairs. +func MergeMaps(base, overrides map[string]string) map[string]string { + merged := make(map[string]string) + + // Add base map first + for k, v := range base { + merged[k] = v + } + + // Override with values from second map + for k, v := range overrides { + merged[k] = v + } + + return merged +} diff --git a/hack/deploy_vars b/hack/deploy_vars index e54ca915..0d3b8926 100755 --- a/hack/deploy_vars +++ b/hack/deploy_vars @@ -8,13 +8,11 @@ BASEDOMAIN="jumpstarter.${IP}.nip.io" IMG=${IMG:-quay.io/jumpstarter-dev/jumpstarter-controller:latest} OPERATOR_IMG=${OPERATOR_IMG:-quay.io/jumpstarter-dev/jumpstarter-operator:latest} -# Determine networking mode and endpoints based on INGRESS_ENABLED -if [ "${INGRESS_ENABLED}" == "true" ]; then - NETWORKING_MODE="ingress" - GRPC_ENDPOINT="grpc.${BASEDOMAIN}:5080" - GRPC_ROUTER_ENDPOINT="router.${BASEDOMAIN}:5080" +# Determine endpoints based on NETWORKING_MODE +if [ "${NETWORKING_MODE}" == "ingress" ]; then + GRPC_ENDPOINT="grpc.${BASEDOMAIN}:5443" + GRPC_ROUTER_ENDPOINT="router.${BASEDOMAIN}:5443" else - NETWORKING_MODE="nodeport" GRPC_ENDPOINT="grpc.${BASEDOMAIN}:8082" GRPC_ROUTER_ENDPOINT="router.${BASEDOMAIN}:8083" fi diff --git a/hack/deploy_with_helm.sh b/hack/deploy_with_helm.sh index 5fec8f26..abdada4b 100755 --- a/hack/deploy_with_helm.sh +++ b/hack/deploy_with_helm.sh @@ -13,7 +13,7 @@ METHOD=install kubectl config use-context kind-jumpstarter # Install nginx ingress if in ingress mode -if [ "${INGRESS_ENABLED}" == "true" ]; then +if [ "${NETWORKING_MODE}" == "ingress" ]; then install_nginx_ingress else echo -e "${GREEN}Deploying with nodeport ...${NC}" diff --git a/hack/deploy_with_operator.sh b/hack/deploy_with_operator.sh index ccd0d0c6..cb553989 100755 --- a/hack/deploy_with_operator.sh +++ b/hack/deploy_with_operator.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -eo pipefail +set -exo pipefail SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" # Source common utilities @@ -55,14 +55,14 @@ if [ "${NETWORKING_MODE}" == "ingress" ]; then - address: grpc.${BASEDOMAIN}:443 ingress: enabled: true - class: "" + class: "nginx" END ) ROUTER_ENDPOINT_CONFIG=$(cat <<-END - address: router.${BASEDOMAIN}:443 ingress: enabled: true - class: "" + class: "nginx" END ) else diff --git a/hack/utils b/hack/utils index 50fd0882..7b558297 100755 --- a/hack/utils +++ b/hack/utils @@ -12,7 +12,7 @@ get_script_dir() { # Environment variable defaults export KIND=${KIND:-bin/kind} export GRPCURL=${GRPCURL:-bin/grpcurl} -export INGRESS_ENABLED=${INGRESS_ENABLED:-false} +export NETWORKING_MODE=${NETWORKING_MODE:-nodeport} # Color codes for terminal output export GREEN='\033[0;32m'