Skip to content

Commit dc23c84

Browse files
committed
chore: add missing files
1 parent 228ee65 commit dc23c84

File tree

3 files changed

+302
-0
lines changed

3 files changed

+302
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import axios from 'axios';
2+
3+
export class HealthCheckService {
4+
private readonly timeout = 5000; // 5 second timeout
5+
6+
/**
7+
* Check if a URI is healthy by testing its whois endpoint
8+
*/
9+
async isUriHealthy(uri: string): Promise<boolean> {
10+
try {
11+
// Parse the URI to extract IP:PORT
12+
const { ip, port } = this.parseUri(uri);
13+
14+
if (!ip || !port) {
15+
console.log(`Invalid URI format: ${uri}`);
16+
return false;
17+
}
18+
19+
// Construct the whois endpoint URL
20+
const whoisUrl = `http://${ip}:${port}/whois`;
21+
22+
console.log(`Health checking: ${whoisUrl}`);
23+
24+
// Make a request to the whois endpoint with timeout
25+
const response = await axios.get(whoisUrl, {
26+
timeout: this.timeout,
27+
validateStatus: (status) => status < 500 // Accept any status < 500 as "reachable"
28+
});
29+
30+
console.log(`Health check passed for ${uri}: ${response.status}`);
31+
return true;
32+
33+
} catch (error) {
34+
console.log(`Health check failed for ${uri}:`, error instanceof Error ? error.message : 'Unknown error');
35+
return false;
36+
}
37+
}
38+
39+
/**
40+
* Parse URI to extract IP and PORT
41+
* Supports formats: IP:PORT, http://IP:PORT, https://IP:PORT
42+
*/
43+
private parseUri(uri: string): { ip: string | null; port: string | null } {
44+
try {
45+
// Remove protocol if present
46+
const cleanUri = uri.replace(/^https?:\/\//, '');
47+
48+
// Split by colon to get IP and PORT
49+
const parts = cleanUri.split(':');
50+
51+
if (parts.length === 2) {
52+
const ip = parts[0];
53+
const port = parts[1];
54+
55+
// Basic validation
56+
if (this.isValidIp(ip) && this.isValidPort(port)) {
57+
return { ip, port };
58+
}
59+
}
60+
61+
return { ip: null, port: null };
62+
} catch (error) {
63+
console.error(`Error parsing URI ${uri}:`, error);
64+
return { ip: null, port: null };
65+
}
66+
}
67+
68+
/**
69+
* Basic IP validation
70+
*/
71+
private isValidIp(ip: string): boolean {
72+
// Simple regex for IP validation
73+
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
74+
return ipRegex.test(ip);
75+
}
76+
77+
/**
78+
* Basic port validation
79+
*/
80+
private isValidPort(port: string): boolean {
81+
const portNum = parseInt(port, 10);
82+
return portNum >= 1 && portNum <= 65535;
83+
}
84+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import * as k8s from '@kubernetes/client-node';
2+
3+
export class KubernetesService {
4+
private kc: k8s.KubeConfig;
5+
private coreV1Api: k8s.CoreV1Api;
6+
7+
constructor() {
8+
this.kc = new k8s.KubeConfig();
9+
this.kc.loadFromDefault(); // This will load from .kube/config
10+
11+
// Get the current context
12+
const currentContext = this.kc.getCurrentContext();
13+
console.log(`Using Kubernetes context: ${currentContext}`);
14+
15+
// Create API client
16+
this.coreV1Api = this.kc.makeApiClient(k8s.CoreV1Api);
17+
}
18+
19+
/**
20+
* Get a working external IP from Kubernetes services
21+
* This will look for services with LoadBalancer type or NodePort with external IPs
22+
*/
23+
async getWorkingExternalIp(): Promise<string | null> {
24+
try {
25+
console.log('Querying Kubernetes for external IPs...');
26+
27+
// Get all services across all namespaces
28+
const servicesResponse = await this.coreV1Api.listServiceForAllNamespaces();
29+
30+
const externalIps: string[] = [];
31+
32+
for (const service of servicesResponse.body.items) {
33+
// Check for LoadBalancer services with external IPs
34+
if (service.spec?.type === 'LoadBalancer' && service.status?.loadBalancer?.ingress) {
35+
for (const ingress of service.status.loadBalancer.ingress) {
36+
if (ingress.ip) {
37+
externalIps.push(ingress.ip);
38+
console.log(`Found LoadBalancer external IP: ${ingress.ip}`);
39+
}
40+
}
41+
}
42+
43+
// Check for services with external IPs
44+
if (service.spec?.externalIPs) {
45+
for (const externalIp of service.spec.externalIPs) {
46+
externalIps.push(externalIp);
47+
console.log(`Found service external IP: ${externalIp}`);
48+
}
49+
}
50+
}
51+
52+
// Remove duplicates
53+
const uniqueIps = [...new Set(externalIps)];
54+
55+
if (uniqueIps.length === 0) {
56+
console.log('No external IPs found in Kubernetes');
57+
return null;
58+
}
59+
60+
// Test each IP to find a working one
61+
for (const ip of uniqueIps) {
62+
if (await this.testIpConnectivity(ip)) {
63+
console.log(`Found working external IP: ${ip}`);
64+
return ip;
65+
}
66+
}
67+
68+
console.log('No working external IPs found');
69+
return null;
70+
71+
} catch (error) {
72+
console.error('Error querying Kubernetes:', error);
73+
return null;
74+
}
75+
}
76+
77+
/**
78+
* Test if an IP is reachable
79+
*/
80+
private async testIpConnectivity(ip: string): Promise<boolean> {
81+
try {
82+
// Try to connect to port 80 (HTTP) as a basic connectivity test
83+
const response = await fetch(`http://${ip}:80`, {
84+
method: 'HEAD',
85+
signal: AbortSignal.timeout(3000) // 3 second timeout
86+
});
87+
return true;
88+
} catch (error) {
89+
console.log(`IP ${ip} is not reachable:`, error instanceof Error ? error.message : 'Unknown error');
90+
return false;
91+
}
92+
}
93+
94+
/**
95+
* Get external IPs for a specific service
96+
*/
97+
async getServiceExternalIps(serviceName: string, namespace: string = 'default'): Promise<string[]> {
98+
try {
99+
const service = await this.coreV1Api.readNamespacedService(serviceName, namespace);
100+
101+
const externalIps: string[] = [];
102+
103+
// Check LoadBalancer ingress
104+
if (service.body.spec?.type === 'LoadBalancer' && service.body.status?.loadBalancer?.ingress) {
105+
for (const ingress of service.body.status.loadBalancer.ingress) {
106+
if (ingress.ip) {
107+
externalIps.push(ingress.ip);
108+
}
109+
}
110+
}
111+
112+
// Check external IPs
113+
if (service.body.spec?.externalIPs) {
114+
externalIps.push(...service.body.spec.externalIPs);
115+
}
116+
117+
return externalIps;
118+
119+
} catch (error) {
120+
console.error(`Error getting service ${serviceName} in namespace ${namespace}:`, error);
121+
return [];
122+
}
123+
}
124+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { HealthCheckService } from './HealthCheckService';
2+
import { KubernetesService } from './KubernetesService';
3+
4+
export class UriResolutionService {
5+
private healthCheckService: HealthCheckService;
6+
private kubernetesService: KubernetesService;
7+
8+
constructor() {
9+
this.healthCheckService = new HealthCheckService();
10+
this.kubernetesService = new KubernetesService();
11+
}
12+
13+
/**
14+
* Resolve a URI with health check and Kubernetes fallback
15+
*/
16+
async resolveUri(originalUri: string): Promise<string> {
17+
console.log(`Resolving URI: ${originalUri}`);
18+
19+
// First, check if the original URI is healthy
20+
const isHealthy = await this.healthCheckService.isUriHealthy(originalUri);
21+
22+
if (isHealthy) {
23+
console.log(`URI ${originalUri} is healthy, returning original`);
24+
return originalUri;
25+
}
26+
27+
console.log(`URI ${originalUri} is unhealthy, attempting Kubernetes fallback`);
28+
29+
// URI is unhealthy, try to get a working external IP from Kubernetes
30+
const workingIp = await this.kubernetesService.getWorkingExternalIp();
31+
32+
if (!workingIp) {
33+
console.log('No working external IP found in Kubernetes, returning original URI');
34+
return originalUri; // Fallback to original if no working IP found
35+
}
36+
37+
// Extract port from original URI and construct new URI with working IP
38+
const { port } = this.parseUri(originalUri);
39+
40+
if (!port) {
41+
console.log('Could not extract port from original URI, returning original');
42+
return originalUri;
43+
}
44+
45+
const newUri = `${workingIp}:${port}`;
46+
console.log(`Substituted ${originalUri} with ${newUri} using working IP from Kubernetes`);
47+
48+
return newUri;
49+
}
50+
51+
/**
52+
* Parse URI to extract IP and PORT (duplicated from HealthCheckService for convenience)
53+
*/
54+
private parseUri(uri: string): { ip: string | null; port: string | null } {
55+
try {
56+
// Remove protocol if present
57+
const cleanUri = uri.replace(/^https?:\/\//, '');
58+
59+
// Split by colon to get IP and PORT
60+
const parts = cleanUri.split(':');
61+
62+
if (parts.length === 2) {
63+
const ip = parts[0];
64+
const port = parts[1];
65+
66+
// Basic validation
67+
if (this.isValidIp(ip) && this.isValidPort(port)) {
68+
return { ip, port };
69+
}
70+
}
71+
72+
return { ip: null, port: null };
73+
} catch (error) {
74+
console.error(`Error parsing URI ${uri}:`, error);
75+
return { ip: null, port: null };
76+
}
77+
}
78+
79+
/**
80+
* Basic IP validation
81+
*/
82+
private isValidIp(ip: string): boolean {
83+
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
84+
return ipRegex.test(ip);
85+
}
86+
87+
/**
88+
* Basic port validation
89+
*/
90+
private isValidPort(port: string): boolean {
91+
const portNum = parseInt(port, 10);
92+
return portNum >= 1 && portNum <= 65535;
93+
}
94+
}

0 commit comments

Comments
 (0)