From 9e98c125151ccc0bf1a89eb6d3eb03abae2f27ff Mon Sep 17 00:00:00 2001 From: Miguel Angel Ajo Pelayo Date: Tue, 28 Oct 2025 16:25:27 +0000 Subject: [PATCH] operator: auto detect base domain --- deploy/operator/config/rbac/role.yaml | 8 ++ .../jumpstarter/endpoints/discovery.go | 51 ++++++++ .../jumpstarter/endpoints/discovery_test.go | 121 ++++++++++++++++++ .../jumpstarter/jumpstarter_controller.go | 15 +++ 4 files changed, 195 insertions(+) create mode 100644 deploy/operator/internal/controller/jumpstarter/endpoints/discovery_test.go diff --git a/deploy/operator/config/rbac/role.yaml b/deploy/operator/config/rbac/role.yaml index d9a8394..5ddc274 100644 --- a/deploy/operator/config/rbac/role.yaml +++ b/deploy/operator/config/rbac/role.yaml @@ -54,6 +54,14 @@ rules: - get - patch - update +- apiGroups: + - config.openshift.io + resources: + - dnses + verbs: + - get + - list + - watch - apiGroups: - coordination.k8s.io resources: diff --git a/deploy/operator/internal/controller/jumpstarter/endpoints/discovery.go b/deploy/operator/internal/controller/jumpstarter/endpoints/discovery.go index 7e17198..d2ee380 100644 --- a/deploy/operator/internal/controller/jumpstarter/endpoints/discovery.go +++ b/deploy/operator/internal/controller/jumpstarter/endpoints/discovery.go @@ -17,8 +17,14 @@ limitations under the License. package endpoints import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -48,3 +54,48 @@ func discoverAPIResource(config *rest.Config, groupVersion, kind string) bool { return false } + +// DiscoverBaseDomain attempts to auto-detect the baseDomain from OpenShift DNS cluster config +// It returns the detected baseDomain in the format "namespace.apps.baseDomain" for +// OpenShift clusters, or an error if it cannot be determined. +func DiscoverBaseDomain(ctx context.Context, c client.Client, namespace string) (string, error) { + logger := log.FromContext(ctx) + + // Try to fetch the OpenShift DNS cluster configuration + dns := &unstructured.Unstructured{} + dns.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "config.openshift.io", + Version: "v1", + Kind: "DNS", + }) + + err := c.Get(ctx, client.ObjectKey{Name: "cluster"}, dns) + if err != nil { + logger.Error(err, "Failed to get OpenShift DNS cluster config - baseDomain cannot be auto-detected") + return "", fmt.Errorf("failed to auto-detect baseDomain from OpenShift DNS cluster config: %w", err) + } + + // Extract spec.baseDomain from the DNS object + spec, found, err := unstructured.NestedMap(dns.Object, "spec") + if err != nil || !found { + logger.Error(err, "Failed to get spec from OpenShift DNS cluster config") + return "", fmt.Errorf("failed to get spec from OpenShift DNS cluster config: spec not found") + } + + openShiftBaseDomain, found, err := unstructured.NestedString(spec, "baseDomain") + if err != nil || !found || openShiftBaseDomain == "" { + logger.Error(err, "Failed to get baseDomain from OpenShift DNS cluster config") + return "", fmt.Errorf("failed to get baseDomain from OpenShift DNS cluster config: baseDomain not found or empty") + } + + // Format the baseDomain as "namespace.apps.openShiftBaseDomain" + // This matches the Helm template behavior when .noNs is false + detectedBaseDomain := fmt.Sprintf("%s.apps.%s", namespace, openShiftBaseDomain) + + logger.Info("Auto-detected baseDomain from OpenShift DNS cluster config", + "openShiftBaseDomain", openShiftBaseDomain, + "detectedBaseDomain", detectedBaseDomain, + "namespace", namespace) + + return detectedBaseDomain, nil +} diff --git a/deploy/operator/internal/controller/jumpstarter/endpoints/discovery_test.go b/deploy/operator/internal/controller/jumpstarter/endpoints/discovery_test.go new file mode 100644 index 0000000..a65241a --- /dev/null +++ b/deploy/operator/internal/controller/jumpstarter/endpoints/discovery_test.go @@ -0,0 +1,121 @@ +/* +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 ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var _ = Describe("DiscoverBaseDomain", func() { + // Note: These tests require OpenShift CRDs to be available in the test environment. + // They will be skipped if the CRDs are not present, which is expected in non-OpenShift environments. + + Context("when OpenShift is available", func() { + BeforeEach(func() { + // Check if OpenShift CRDs are available + dns := &unstructured.Unstructured{} + dns.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "config.openshift.io", + Version: "v1", + Kind: "DNS", + }) + dns.SetName("cluster") + dns.Object["spec"] = map[string]interface{}{ + "baseDomain": "test-check.com", + } + + // Try to create a test DNS object to check if the CRD is available + err := k8sClient.Create(ctx, dns) + if err != nil { + Skip("Skipping OpenShift baseDomain auto-detection tests: OpenShift CRDs not available in test environment") + } + // Clean up test object + _ = k8sClient.Delete(ctx, dns) + }) + + Context("when OpenShift DNS cluster config exists", func() { + It("should successfully auto-detect baseDomain", func() { + // Create a mock OpenShift DNS cluster config + dns := &unstructured.Unstructured{} + dns.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "config.openshift.io", + Version: "v1", + Kind: "DNS", + }) + dns.SetName("cluster") + dns.Object["spec"] = map[string]interface{}{ + "baseDomain": "example.com", + } + + // Create the DNS object in the cluster + err := k8sClient.Create(ctx, dns) + Expect(err).NotTo(HaveOccurred()) + + // Test auto-detection + detectedBaseDomain, err := DiscoverBaseDomain(ctx, k8sClient, "test-namespace") + Expect(err).NotTo(HaveOccurred()) + Expect(detectedBaseDomain).To(Equal("test-namespace.apps.example.com")) + + // Cleanup + err = k8sClient.Delete(ctx, dns) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when OpenShift DNS cluster config has empty baseDomain", func() { + It("should return an error", func() { + // Create a mock OpenShift DNS cluster config with empty baseDomain + dns := &unstructured.Unstructured{} + dns.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "config.openshift.io", + Version: "v1", + Kind: "DNS", + }) + dns.SetName("cluster") + dns.Object["spec"] = map[string]interface{}{ + "baseDomain": "", + } + + // Create the DNS object in the cluster + err := k8sClient.Create(ctx, dns) + Expect(err).NotTo(HaveOccurred()) + + // Test auto-detection with empty baseDomain + _, err = DiscoverBaseDomain(ctx, k8sClient, "test-namespace") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("baseDomain not found or empty")) + + // Cleanup + err = k8sClient.Delete(ctx, dns) + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) + + Context("when OpenShift DNS cluster config does not exist", func() { + It("should return an error", func() { + // Try to auto-detect when no DNS config exists + // This test will work even without OpenShift CRDs because it just checks error handling + _, err := DiscoverBaseDomain(ctx, k8sClient, "test-namespace") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to auto-detect baseDomain")) + }) + }) +}) diff --git a/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go b/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go index d5a0dd0..bb80f7e 100644 --- a/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go +++ b/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go @@ -85,6 +85,9 @@ type JumpstarterReconciler struct { // +kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=route.openshift.io,resources=routes/status,verbs=get;update;patch +// OpenShift config resources (for baseDomain auto-detection) +// +kubebuilder:rbac:groups=config.openshift.io,resources=dnses,verbs=get;list;watch + // Monitoring resources // +kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors,verbs=get;list;watch;create;update;patch;delete @@ -127,6 +130,18 @@ func (r *JumpstarterReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{}, nil } + // Auto-detect baseDomain if not provided + if jumpstarter.Spec.BaseDomain == "" { + log.Info("BaseDomain not provided, attempting auto-detection from OpenShift DNS cluster config") + detectedBaseDomain, err := endpoints.DiscoverBaseDomain(ctx, r.Client, jumpstarter.Namespace) + if err != nil { + log.Error(err, "Failed to auto-detect baseDomain - baseDomain is required but was not provided and could not be auto-detected") + return ctrl.Result{}, fmt.Errorf("baseDomain is required but was not provided and could not be auto-detected: %w", err) + } + jumpstarter.Spec.BaseDomain = detectedBaseDomain + log.Info("Successfully auto-detected baseDomain", "baseDomain", detectedBaseDomain) + } + // Reconcile RBAC resources first if err := r.reconcileRBAC(ctx, &jumpstarter); err != nil { log.Error(err, "Failed to reconcile RBAC")