package dns import ( "context" "crypto/tls" "fmt" "net" "net/http" "time" ) // HealthResult contains the result of health checks type HealthResult struct { SSHReady bool HTTPSReady bool SSHError error HTTPSError error } // HealthChecker performs health checks on VMs type HealthChecker struct { SSHTimeout time.Duration HTTPSTimeout time.Duration } // NewHealthChecker creates a new health checker func NewHealthChecker() *HealthChecker { return &HealthChecker{ SSHTimeout: 10 * time.Second, HTTPSTimeout: 10 * time.Second, } } // CheckSSH checks if SSH is accessible func (h *HealthChecker) CheckSSH(ctx context.Context, ip string, port int) error { if port == 0 { port = 22 } address := fmt.Sprintf("%s:%d", ip, port) dialer := &net.Dialer{ Timeout: h.SSHTimeout, } conn, err := dialer.DialContext(ctx, "tcp", address) if err != nil { return fmt.Errorf("SSH not accessible: %w", err) } conn.Close() return nil } // CheckHTTPS checks if HTTPS is accessible func (h *HealthChecker) CheckHTTPS(ctx context.Context, ip string, port int) error { if port == 0 { port = 443 } // Use IP directly with insecure TLS (we just want to verify the port is open) client := &http.Client{ Timeout: h.HTTPSTimeout, Transport: &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, }, } url := fmt.Sprintf("https://%s:%d/", ip, port) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return err } resp, err := client.Do(req) if err != nil { // Connection refused or timeout is a failure // But TLS errors mean the port is open (which is what we want) if _, ok := err.(*tls.CertificateVerificationError); ok { return nil // Port is open, TLS is working } // For other TLS errors, the port is still open if netErr, ok := err.(net.Error); ok && !netErr.Timeout() { // Non-timeout network error might still mean port is open // Check if we can at least connect conn, connErr := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, port), h.HTTPSTimeout) if connErr == nil { conn.Close() return nil } } return fmt.Errorf("HTTPS not accessible: %w", err) } defer resp.Body.Close() return nil } // CheckAll performs all health checks func (h *HealthChecker) CheckAll(ctx context.Context, ip string) *HealthResult { result := &HealthResult{} result.SSHError = h.CheckSSH(ctx, ip, 22) result.SSHReady = result.SSHError == nil result.HTTPSError = h.CheckHTTPS(ctx, ip, 443) result.HTTPSReady = result.HTTPSError == nil return result } // WaitForReady waits for all health checks to pass func (h *HealthChecker) WaitForReady(ctx context.Context, ip string, timeout time.Duration) error { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { result := h.CheckAll(ctx, ip) if result.SSHReady { return nil // SSH is the minimum requirement } select { case <-ctx.Done(): return ctx.Err() case <-time.After(5 * time.Second): // Continue checking } } return fmt.Errorf("timeout waiting for VM to be ready") }