diff --git a/fleetshard/pkg/central/reconciler/reconciler.go b/fleetshard/pkg/central/reconciler/reconciler.go index 083f982e7f..505009e20f 100644 --- a/fleetshard/pkg/central/reconciler/reconciler.go +++ b/fleetshard/pkg/central/reconciler/reconciler.go @@ -131,6 +131,7 @@ type CentralReconciler struct { environment string auditLogging config.AuditLogging encryptionKeyGenerator cipher.KeyGenerator + uiReachabilityChecker CentralUIReachabilityChecker managedDbReconciler *managedDbReconciler managedDBEnabled bool @@ -245,6 +246,20 @@ func (r *CentralReconciler) Reconcile(ctx context.Context, remoteCentral private return installingStatus(), nil } + if r.useRoutes && !isRemoteCentralReady(&remoteCentral) { + // Check whether central UI host is reachable over HTTP. + centralUIReachable, err := r.uiReachabilityChecker.IsCentralUIHostReachable(ctx, remoteCentral.Spec.UiHost) + if err != nil { + return nil, err + } + if !centralUIReachable { + if isRemoteCentralProvisioning(remoteCentral) && !needsReconcile { // no changes detected, wait until central UI becomes reachable + return nil, ErrCentralNotChanged + } + return installingStatus(), nil + } + } + status, err := r.collectReconciliationStatus(ctx, &remoteCentral) if err != nil { return nil, err @@ -1064,6 +1079,7 @@ func NewCentralReconciler(k8sClient ctrlClient.Client, fleetmanagerClient *fleet environment: opts.Environment, auditLogging: opts.AuditLogging, encryptionKeyGenerator: encryptionKeyGenerator, + uiReachabilityChecker: NewHTTPCentralUIReachabilityChecker(), managedDbReconciler: dbReconciler, managedDBEnabled: opts.ManagedDBEnabled, diff --git a/fleetshard/pkg/central/reconciler/reconciler_test.go b/fleetshard/pkg/central/reconciler/reconciler_test.go index 41fd330d8a..09b7ef0c8d 100644 --- a/fleetshard/pkg/central/reconciler/reconciler_test.go +++ b/fleetshard/pkg/central/reconciler/reconciler_test.go @@ -137,6 +137,8 @@ func getClientTrackerAndReconciler( cipher.AES256KeyGenerator{}, reconcilerOptions, ) + // Override with mock for testing - always return reachable + reconciler.uiReachabilityChecker = NewMockCentralUIReachabilityChecker(true, nil) return fakeClient, tracker, reconciler } diff --git a/fleetshard/pkg/central/reconciler/ui_reachability_checker.go b/fleetshard/pkg/central/reconciler/ui_reachability_checker.go new file mode 100644 index 0000000000..dc82187853 --- /dev/null +++ b/fleetshard/pkg/central/reconciler/ui_reachability_checker.go @@ -0,0 +1,62 @@ +package reconciler + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/pkg/errors" +) + +const ( + httpCheckTimeout = 10 * time.Second +) + +// CentralUIReachabilityChecker checks if a Central UI is reachable +type CentralUIReachabilityChecker interface { + IsCentralUIHostReachable(ctx context.Context, uiHost string) (bool, error) +} + +// HTTPCentralUIReachabilityChecker is the default implementation that performs actual HTTP checks +type HTTPCentralUIReachabilityChecker struct { + httpClient *http.Client +} + +// NewHTTPCentralUIReachabilityChecker creates a new HTTP-based reachability checker +func NewHTTPCentralUIReachabilityChecker() *HTTPCentralUIReachabilityChecker { + return &HTTPCentralUIReachabilityChecker{ + httpClient: &http.Client{ + Timeout: httpCheckTimeout, + }, + } +} + +// IsCentralUIHostReachable performs an HTTP check to verify if the Central UI host is reachable +func (c *HTTPCentralUIReachabilityChecker) IsCentralUIHostReachable(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 { + return false, errors.Wrapf(err, "HTTP request failed for %s", url) + } + defer func() { + _ = resp.Body.Close() + }() + + // Accept any response status code in the 2xx or 3xx range as reachable + // This allows for redirects and successful responses + return resp.StatusCode >= 200 && resp.StatusCode < 400, nil +} \ No newline at end of file diff --git a/fleetshard/pkg/central/reconciler/ui_reachability_checker_mock.go b/fleetshard/pkg/central/reconciler/ui_reachability_checker_mock.go new file mode 100644 index 0000000000..55853db529 --- /dev/null +++ b/fleetshard/pkg/central/reconciler/ui_reachability_checker_mock.go @@ -0,0 +1,34 @@ +package reconciler + +import ( + "context" +) + +// MockCentralUIReachabilityChecker is a mock implementation for testing +type MockCentralUIReachabilityChecker struct { + reachable bool + err error +} + +// NewMockCentralUIReachabilityChecker creates a new mock checker +func NewMockCentralUIReachabilityChecker(reachable bool, err error) *MockCentralUIReachabilityChecker { + return &MockCentralUIReachabilityChecker{ + reachable: reachable, + err: err, + } +} + +// IsCentralUIHostReachable returns the mocked reachability status +func (m *MockCentralUIReachabilityChecker) IsCentralUIHostReachable(_ context.Context, _ string) (bool, error) { + return m.reachable, m.err +} + +// SetReachable sets the reachability status for the mock +func (m *MockCentralUIReachabilityChecker) SetReachable(reachable bool) { + m.reachable = reachable +} + +// SetError sets the error for the mock +func (m *MockCentralUIReachabilityChecker) SetError(err error) { + m.err = err +} \ No newline at end of file