diff --git a/internal/central/pkg/externaldns/externaldns.go b/internal/central/pkg/externaldns/externaldns.go index 78ad2131d0..6a5813b62e 100644 --- a/internal/central/pkg/externaldns/externaldns.go +++ b/internal/central/pkg/externaldns/externaldns.go @@ -5,5 +5,8 @@ import "github.com/stackrox/acs-fleet-manager/internal/central/pkg/api/private" // IsEnabled checks if the external DNS feature is enabled for the given managed central. func IsEnabled(managedCentral private.ManagedCentral) bool { isEnabled, ok := managedCentral.Spec.TenantResourcesValues["externalDnsEnabled"].(bool) - return ok && isEnabled + if !ok { + return true // By default, external DNS is enabled + } + return isEnabled } diff --git a/internal/central/pkg/workers/centralmgrs/centrals_routes_cname_mgr.go b/internal/central/pkg/workers/centralmgrs/centrals_routes_cname_mgr.go index 047e74ebe9..1f9450c6d0 100644 --- a/internal/central/pkg/workers/centralmgrs/centrals_routes_cname_mgr.go +++ b/internal/central/pkg/workers/centralmgrs/centrals_routes_cname_mgr.go @@ -1,6 +1,8 @@ package centralmgrs import ( + "context" + "github.com/golang/glog" "github.com/google/uuid" "github.com/pkg/errors" @@ -20,6 +22,7 @@ type CentralRoutesCNAMEManager struct { centralService services.CentralService centralConfig *config.CentralConfig managedCentralPresenter *presenters.ManagedCentralPresenter + uiReachabilityChecker UIReachabilityChecker } var _ workers.Worker = &CentralRoutesCNAMEManager{} @@ -36,6 +39,7 @@ func NewCentralCNAMEManager(centralService services.CentralService, centralConfi centralService: centralService, centralConfig: centralConfig, managedCentralPresenter: managedCentralPresenter, + uiReachabilityChecker: NewHTTPUIReachabilityChecker(), } } @@ -67,35 +71,52 @@ func (k *CentralRoutesCNAMEManager) Reconcile() []error { errs = append(errs, errors.Wrapf(err, "failed to present managed central for central %s", central.ID)) continue } - if k.centralConfig.EnableCentralExternalDomain && !externaldns.IsEnabled(managedCentral) { - if central.RoutesCreationID == "" { - glog.Infof("creating CNAME records for central %s", central.ID) - - changeOutput, err := k.centralService.ChangeCentralCNAMErecords(central, services.CentralRoutesActionUpsert) - - if err != nil { - errs = append(errs, err) - continue + if k.centralConfig.EnableCentralExternalDomain { + if !externaldns.IsEnabled(managedCentral) { + if central.RoutesCreationID == "" { + glog.Infof("creating CNAME records for central %s", central.ID) + + changeOutput, err := k.centralService.ChangeCentralCNAMErecords(central, services.CentralRoutesActionUpsert) + + if err != nil { + errs = append(errs, err) + continue + } + + switch { + case changeOutput == nil: + glog.Infof("creating CNAME records failed with nil result") + continue + case changeOutput.ChangeInfo == nil || changeOutput.ChangeInfo.Id == nil || changeOutput.ChangeInfo.Status == "": + glog.Infof("creating CNAME records failed with nil info") + continue + } + + central.RoutesCreationID = *changeOutput.ChangeInfo.Id + central.RoutesCreated = changeOutput.ChangeInfo.Status == "INSYNC" + } else { + recordStatus, err := k.centralService.GetCNAMERecordStatus(central) + if err != nil { + errs = append(errs, err) + continue + } + central.RoutesCreated = *recordStatus.Status == "INSYNC" } - - switch { - case changeOutput == nil: - glog.Infof("creating CNAME records failed with nil result") - continue - case changeOutput.ChangeInfo == nil || changeOutput.ChangeInfo.Id == nil || changeOutput.ChangeInfo.Status == "": - glog.Infof("creating CNAME records failed with nil info") - continue - } - - central.RoutesCreationID = *changeOutput.ChangeInfo.Id - central.RoutesCreated = changeOutput.ChangeInfo.Status == "INSYNC" } else { - recordStatus, err := k.centralService.GetCNAMERecordStatus(central) - if err != nil { - errs = append(errs, err) - continue + // External DNS is enabled for this central (managed by external-dns operator) + ctx := context.Background() + uiReachable, checkErr := k.uiReachabilityChecker.IsReachable(ctx, managedCentral.Spec.UiHost) + if checkErr != nil { + glog.Warningf("Failed to check UI reachability for central %s at %s: %v", + central.ID, managedCentral.Spec.UiHost, checkErr) + } else if !uiReachable { + glog.Infof("Central %s UI at %s is not yet reachable from internet", + central.ID, managedCentral.Spec.UiHost) + } else { + glog.Infof("Central %s UI at %s is reachable from internet", + central.ID, managedCentral.Spec.UiHost) + central.RoutesCreated = true } - central.RoutesCreated = *recordStatus.Status == "INSYNC" } } else { glog.Infof("external certificate is disabled, skip CNAME creation for Central %s", central.ID) diff --git a/internal/central/pkg/workers/centralmgrs/ui_reachability_checker.go b/internal/central/pkg/workers/centralmgrs/ui_reachability_checker.go new file mode 100644 index 0000000000..2d33837742 --- /dev/null +++ b/internal/central/pkg/workers/centralmgrs/ui_reachability_checker.go @@ -0,0 +1,75 @@ +package centralmgrs + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/golang/glog" + "github.com/pkg/errors" +) + +const ( + httpCheckTimeout = 10 * time.Second +) + +// UIReachabilityChecker checks if a Central UI is reachable from the internet +type UIReachabilityChecker interface { + IsReachable(ctx context.Context, uiHost string) (bool, error) +} + +// HTTPUIReachabilityChecker is the default implementation that performs actual HTTP checks +type HTTPUIReachabilityChecker struct { + httpClient *http.Client +} + +// NewHTTPUIReachabilityChecker creates a new HTTP-based reachability checker +func NewHTTPUIReachabilityChecker() *HTTPUIReachabilityChecker { + return &HTTPUIReachabilityChecker{ + httpClient: &http.Client{ + Timeout: httpCheckTimeout, + // Don't follow redirects automatically, we want to check the exact host + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + }, + } +} + +// IsReachable performs an HTTP HEAD request to verify if the Central UI host is reachable +func (c *HTTPUIReachabilityChecker) IsReachable(ctx context.Context, uiHost string) (bool, error) { + if uiHost == "" { + return false, errors.New("UI host is empty") + } + + // Construct the URL with https scheme + url := fmt.Sprintf("https://%s", uiHost) + + // Create request with context + req, err := http.NewRequestWithContext(ctx, "HEAD", url, nil) + if err != nil { + return false, errors.Wrapf(err, "creating HTTP request for %s", url) + } + + // Perform the request + resp, err := c.httpClient.Do(req) + if err != nil { + // Network errors mean the host is not reachable + return false, nil + } + defer func() { + _ = resp.Body.Close() + }() + + // Accept any response status code in the 2xx or 3xx range as "reachable" + // This indicates the DNS resolved and the server responded + isSuccess := resp.StatusCode >= 200 && resp.StatusCode < 400 + if !isSuccess { + glog.Infof("UI reachability check failed for host %q with status code %d", uiHost, resp.StatusCode) + } else { + glog.Infof("UI reachability check succeeded for host %q with status code %d", uiHost, resp.StatusCode) + } + + return isSuccess, nil +}