Files
3cloud-web/src/services/xcloudifyApi.ts
T
2025-12-01 23:29:52 +00:00

673 lines
20 KiB
TypeScript

const DEFAULT_API_BASE_URL = 'https://api.xcloudify.tech/api';
// Read API base URL from env with safe normalization and fallback
const RAW_API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string | undefined;
const normalizeBaseUrl = (url: string) => url ? url.replace(/\/+$/, '') : '';
export const API_BASE_URL = normalizeBaseUrl(RAW_API_BASE_URL || DEFAULT_API_BASE_URL);
export interface XCloudifyWorkload {
cloudflare_tunnel: any;
containers: Array<{
container_id: string;
container_name: string;
is_sidecar?: boolean;
launch_params: {
container_name: string;
docker_image: string;
networks: string;
ports: Array<{
external: number;
internal: number;
use_dns: boolean;
}>;
};
status: string;
}>;
nscontroller_workload_id: string;
pod_id: string;
pod_name: string;
created_at?: string;
port_forwardings: Array<{
container_id: string;
container_name: string;
dns_record_hostname: string | null;
dns_record_id: string | null;
external_port: number;
internal_port: number;
external_ip_address: string;
internal_ip_address: string;
protocol: string;
}>;
region_id: string;
vdc_id: string;
virtual_data_center_id: string;
workload_host_id: string;
}
export interface ApiPod {
id: string;
name: string;
status: string;
created?: string;
namespace?: string;
node?: string;
ip?: string;
total_cpu?: number;
total_memory?: string;
virtual_data_center_id?: string;
containers: Array<{
id: string;
name: string;
image: string;
status: string;
type: string;
restarts?: number;
created?: string;
cpu_usage?: number;
memory_usage?: string;
sidecar_type?: string;
port_forwarding?: Array<{
internal: number;
external: number;
protocol: string;
use_dns?: boolean;
dns_name?: string;
external_ip_address?: string;
status?: string;
}>;
}>;
createdAt?: string;
updatedAt?: string;
}
export interface ApiResponse<T> {
code: number;
data: T;
message: string;
request_id: string;
success: boolean;
version: number;
}
export interface VirtualDataCenter {
id: string;
name: string;
description: string | null;
status: string | null;
project_id: string;
region_id: string;
created_at: string;
updated_at: string;
created_by: string;
deleted: boolean;
deleted_at: string | null;
visible: boolean;
}
// Transform XCloudify workload data to our Pod format
export function transformWorkloadToPod(workload: XCloudifyWorkload): ApiPod {
// Map port forwardings by container_id for easy lookup
const portForwardingMap = workload.port_forwardings.reduce((acc, pf) => {
if (!acc[pf.container_id]) {
acc[pf.container_id] = [];
}
acc[pf.container_id].push({
internal: pf.internal_port,
external: pf.external_port,
protocol: pf.protocol,
use_dns: !!pf.dns_record_hostname,
dns_name: pf.dns_record_hostname || undefined,
external_ip_address: pf.external_ip_address,
status: "active"
});
return acc;
}, {} as Record<string, any[]>);
return {
id: workload.pod_id,
name: workload.pod_name,
status: workload.containers.every(c => c.status === "running") ? "Running" : "Pending",
created: workload.created_at,
ip: workload.port_forwardings[0]?.external_ip_address,
virtual_data_center_id: workload.virtual_data_center_id,
containers: workload.containers.map(container => ({
id: container.container_id,
name: container.container_name,
image: container.launch_params.docker_image,
status: container.status,
type: container.is_sidecar ? "sidecar" : "user",
port_forwarding: portForwardingMap[container.container_id] || []
}))
};
}
class XCloudifyApiError extends Error {
constructor(message: string, public status?: number) {
super(message);
this.name = 'XCloudifyApiError';
}
}
// Global callback for API monitoring
let apiMonitorCallback: ((call: any) => void) | null = null;
export function setApiMonitorCallback(callback: (call: any) => void) {
apiMonitorCallback = callback;
}
export async function apiRequest<T>(endpoint: string, options: RequestInit & { responseType?: 'json' | 'blob' } = {}): Promise<T> {
const url = endpoint.startsWith('http')
? endpoint
: `${API_BASE_URL}${endpoint.startsWith('/') ? '' : '/'}${endpoint}`;
const startTime = Date.now();
const callData = {
method: options.method || 'GET',
url,
requestHeaders: options.headers,
requestBody: options.body ? JSON.parse(options.body as string) : undefined,
};
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
const duration = Date.now() - startTime;
let responseBody;
try {
responseBody = options.responseType === 'blob' ? await response.blob() : await response.json();
} catch (parseError) {
// If response body parsing fails, capture the raw text
responseBody = await response.text();
}
if (!response.ok) {
const error = `API request failed: ${response.statusText}`;
// Always log failed requests
if (apiMonitorCallback) {
apiMonitorCallback({
...callData,
responseStatus: response.status,
responseHeaders: Object.fromEntries(response.headers.entries()),
responseBody,
duration,
error,
});
}
throw new XCloudifyApiError(error, response.status);
}
// Monitor successful API calls
if (apiMonitorCallback) {
apiMonitorCallback({
...callData,
responseStatus: response.status,
responseHeaders: Object.fromEntries(response.headers.entries()),
responseBody,
duration,
});
}
return responseBody;
} catch (error) {
const duration = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
// Only log network errors, not already logged XCloudifyApiError instances
if (apiMonitorCallback && !(error instanceof XCloudifyApiError)) {
apiMonitorCallback({
...callData,
responseStatus: undefined,
duration,
error: errorMessage,
});
}
if (error instanceof XCloudifyApiError) {
throw error;
}
throw new XCloudifyApiError(`Network error: ${errorMessage}`);
}
}
export const xcloudifyApi = {
// Fetch all pods
async getPods(): Promise<ApiPod[]> {
const response = await apiRequest<ApiResponse<XCloudifyWorkload[]>>('/workloads/pods');
// Transform the workloads array to pods
return response.data.map(transformWorkloadToPod);
},
// Create a new container
async createContainer(containerData: any): Promise<any> {
return apiRequest('/workloads/containers', {
method: 'POST',
body: JSON.stringify(containerData),
});
},
// Get container details
async getContainer(containerId: string): Promise<any> {
const response = await apiRequest<ApiResponse<any>>(`/workloads/containers/${containerId}`);
return response.data;
},
// Container lifecycle actions (start, stop, restart)
async containerLifecycleAction(containerId: string, action: 'start' | 'stop' | 'restart'): Promise<any> {
return apiRequest(`/workloads/containers/${containerId}/lifecycle/${action}`, {
method: 'POST',
});
},
// Delete container
async deleteContainer(containerId: string): Promise<any> {
return apiRequest(`/workloads/containers/${containerId}`, {
method: 'DELETE',
});
},
// Delete pod
async deletePod(podId: string): Promise<any> {
return apiRequest(`/workloads/pods/${podId}`, {
method: 'DELETE',
});
},
// Get workload hosts
async getWorkloadHosts(regionId?: string): Promise<any[]> {
const url = regionId ? `/workload_hosts?region_id=${regionId}` : '/workload_hosts';
const response = await apiRequest<ApiResponse<any[]>>(url);
return response.data || [];
},
// Get workload host details
async getWorkloadHost(hostId: string): Promise<any> {
return apiRequest(`/workload_hosts/${hostId}`);
},
// Update workload host
async updateWorkloadHost(hostId: string, data: any): Promise<any> {
return apiRequest(`/workload_hosts/${hostId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
// Delete workload host
async deleteWorkloadHost(hostId: string): Promise<any> {
return apiRequest(`/workload_hosts/${hostId}`, {
method: 'DELETE',
});
},
// Get audit logs for a container
async getContainerAuditLogs(containerId: string, params?: {
limit?: number;
offset?: number;
action?: string;
}): Promise<any> {
const queryParams = new URLSearchParams();
if (params?.limit) queryParams.append('limit', params.limit.toString());
if (params?.offset) queryParams.append('offset', params.offset.toString());
if (params?.action) queryParams.append('action', params.action);
const url = `/audit/container/${containerId}${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
return apiRequest(url);
},
// Get audit action types
async getAuditActionTypes(): Promise<any> {
return apiRequest('/audit/actions');
},
// Get audit logs for a pod
async getPodAuditLogs(podId: string, params?: {
limit?: number;
offset?: number;
action?: string;
}): Promise<any> {
const queryParams = new URLSearchParams();
if (params?.limit) queryParams.append('limit', params.limit.toString());
if (params?.offset) queryParams.append('offset', params.offset.toString());
if (params?.action) queryParams.append('action', params.action);
const url = `/audit/pod/${podId}${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
return apiRequest(url);
},
// Get labels for a workload host
async getLabelsForHost(hostId: string): Promise<any[]> {
const response = await apiRequest<ApiResponse<any[]>>(`/workload_hosts/${hostId}/labels`);
return response.data || [];
},
// Add label to workload host
async addLabel(hostId: string, key: string, value: string, adminOnlyView = false, adminOnlySet = false): Promise<any> {
return apiRequest(`/workload_hosts/${hostId}/labels`, {
method: 'POST',
body: JSON.stringify({
label_key: key,
label_value: value,
admin_only_view: adminOnlyView,
admin_only_set: adminOnlySet,
}),
});
},
// Remove label from workload host
async removeLabel(hostId: string, key: string): Promise<any> {
return apiRequest(`/workload_hosts/${hostId}/labels`, {
method: 'DELETE',
body: JSON.stringify({
label_key: key,
}),
});
},
// Get OVS bridges for host
async getOVSBridgesForHost(hostId: string): Promise<any[]> {
return apiRequest(`/workload_hosts/${hostId}/ovs_bridges`);
},
// Region management endpoints
async getRegions(): Promise<any[]> {
const response = await apiRequest<ApiResponse<any[]>>('/regions');
return response.data || [];
},
async getRegion(regionId: string): Promise<any> {
return apiRequest(`/regions/${regionId}`);
},
async createRegion(data: any): Promise<any> {
return apiRequest('/regions', {
method: 'POST',
body: JSON.stringify(data),
});
},
async updateRegion(regionId: string, data: any): Promise<any> {
return apiRequest(`/regions/${regionId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
async deleteRegion(regionId: string): Promise<any> {
return apiRequest(`/regions/${regionId}`, {
method: 'DELETE',
});
},
// Virtual Data Center management endpoints
async getVirtualDataCenters(): Promise<VirtualDataCenter[]> {
const response = await apiRequest<ApiResponse<VirtualDataCenter[]>>('/virtual_data_centers');
return response.data;
},
async getVirtualDataCenter(vdcId: string): Promise<any> {
return apiRequest(`/virtual_data_centers/${vdcId}`);
},
async createVirtualDataCenter(data: any): Promise<any> {
return apiRequest('/virtual_data_centers', {
method: 'POST',
body: JSON.stringify(data),
});
},
async updateVirtualDataCenter(vdcId: string, data: any): Promise<any> {
return apiRequest(`/virtual_data_centers/${vdcId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
async deleteVirtualDataCenter(vdcId: string): Promise<any> {
return apiRequest(`/virtual_data_centers/${vdcId}`, {
method: 'DELETE',
});
},
// Project management endpoints (needed for VDC creation)
async getProjects(): Promise<any[]> {
const response = await apiRequest<ApiResponse<any[]>>('/projects');
return response.data || [];
},
// Image management endpoints
async getImages(): Promise<any[]> {
const response = await apiRequest<{data: any[]}>('/images');
return response.data || [];
},
async getImage(imageId: string): Promise<any> {
return apiRequest(`/images/${imageId}`);
},
async createImage(data: any): Promise<any> {
return apiRequest('/images', {
method: 'POST',
body: JSON.stringify(data),
});
},
async updateImage(imageId: string, data: any): Promise<any> {
return apiRequest(`/images/${imageId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
async deleteImage(imageId: string): Promise<any> {
return apiRequest(`/images/${imageId}`, {
method: 'DELETE',
});
},
// Project management endpoints
async getProject(projectId: string): Promise<any> {
return apiRequest(`/projects/${projectId}`);
},
async createProject(data: any): Promise<any> {
return apiRequest('/projects', {
method: 'POST',
body: JSON.stringify(data),
});
},
async updateProject(projectId: string, data: any): Promise<any> {
return apiRequest(`/projects/${projectId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
async deleteProject(projectId: string): Promise<any> {
return apiRequest(`/projects/${projectId}`, {
method: 'DELETE',
});
},
// Universe management endpoints (needed for project creation)
async getUniverses(): Promise<any[]> {
const response = await apiRequest<ApiResponse<any[]>>('/universes');
return response.data || [];
},
// Get workloads for a specific workload host
async getWorkloadsForHost(workloadHostId: string): Promise<any[]> {
const response = await apiRequest<ApiResponse<any[]>>(`/workload_hosts/${workloadHostId}/active_workloads`);
return response.data || [];
},
// Virtual Machine management endpoints
async getVirtualMachines(): Promise<any[]> {
try {
const response = await apiRequest('/workloads/virtual_machines');
// Ensure we always return an array
return Array.isArray(response) ? response : [];
} catch (error) {
console.error('Error fetching virtual machines:', error);
return [];
}
},
async getVirtualMachine(vmId: string): Promise<any> {
return apiRequest(`/workloads/virtual_machines/${vmId}`);
},
async createVirtualMachine(data: any): Promise<any> {
return apiRequest('/workloads/virtual_machines', {
method: 'POST',
body: JSON.stringify(data),
});
},
async deleteVirtualMachine(vmId: string): Promise<any> {
return apiRequest(`/workloads/virtual_machines/${vmId}`, {
method: 'DELETE',
});
},
async updateVirtualMachine(vmId: string, data: any): Promise<any> {
return apiRequest(`/workloads/virtual_machines/status_update/${vmId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
// Network management endpoints
async getNetworks(): Promise<any[]> {
return apiRequest('/networks');
},
// Get networks by VDC
async getNetworksByVDC(vdcId: string): Promise<any[]> {
const response = await apiRequest<ApiResponse<any[]>>(`/networks?virtual_datacenter_id=${vdcId}`);
return response.data || [];
},
// Create network
async createNetwork(data: any): Promise<any> {
return apiRequest('/networks', {
method: 'POST',
body: JSON.stringify(data),
});
},
// Volume management
getVolumes(params?: { vdc_id?: string; type?: string; boot?: boolean }): Promise<any> {
const searchParams = new URLSearchParams();
if (params?.vdc_id) searchParams.append('vdc_id', params.vdc_id);
if (params?.type) searchParams.append('type', params.type);
if (params?.boot !== undefined) searchParams.append('boot', params.boot.toString());
const queryString = searchParams.toString();
return apiRequest<any>(`/volumes${queryString ? `?${queryString}` : ''}`);
},
// Get volumes for a specific virtual data center
getVolumesForVirtualDataCenter(vdcId: string): Promise<any> {
return apiRequest<any>(`/virtual_data_centers/${vdcId}/volumes`);
},
createVolume(volumeData: any): Promise<any> {
return apiRequest<any>('/volumes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(volumeData),
});
},
getVolume(volumeId: string): Promise<any> {
return apiRequest<any>(`/volumes/${volumeId}`);
},
updateVolume(volumeId: string, volumeData: any): Promise<any> {
return apiRequest<any>(`/volumes/${volumeId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(volumeData),
});
},
deleteVolume(volumeId: string): Promise<any> {
return apiRequest<any>(`/volumes/${volumeId}`, {
method: 'DELETE',
});
},
attachVolumeToWorkload(volumeId: string, workloadId: string): Promise<any> {
return apiRequest<any>(`/volumes/${volumeId}/attach`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ workload_id: workloadId }),
});
},
detachVolumeFromWorkload(volumeId: string, workloadId: string): Promise<any> {
return apiRequest<any>(`/volumes/${volumeId}/detach/${workloadId}`, {
method: 'DELETE',
});
},
getVolumesForWorkload(workloadId: string): Promise<any> {
return apiRequest<any>(`/workloads/${workloadId}/volumes`);
},
getWorkloadsForVolume(volumeId: string): Promise<any> {
return apiRequest<any>(`/volumes/${volumeId}/workloads`);
},
getVirtualDataCentersByProject(projectId: string): Promise<VirtualDataCenter[]> {
return apiRequest<ApiResponse<VirtualDataCenter[]>>(`/virtual_data_centers?project_id=${projectId}`).then(response => response.data || []);
},
// Available ports for workload hosts
getAvailablePortsForHost(hostId: string): Promise<any[]> {
return apiRequest<ApiResponse<any[]>>(`/workload_hosts/${hostId}/available_ports`).then(response => response.data || []);
},
addAvailablePortsForHost(hostId: string, portData: { start_port: number; end_port: number; protocol: string }): Promise<any> {
return apiRequest<any>(`/workload_hosts/${hostId}/available_ports`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(portData)
});
},
removeAvailablePortsForHost(hostId: string, portId: string): Promise<any> {
return apiRequest<any>(`/workload_hosts/${hostId}/available_ports/${portId}`, {
method: 'DELETE'
});
},
getVirtualDataCentersByRegion(regionId: string): Promise<VirtualDataCenter[]> {
return apiRequest<{ data: VirtualDataCenter[] }>(`/virtual_data_centers?region_id=${regionId}`)
.then(response => response.data);
},
deleteNetwork(networkId: string): Promise<any> {
return apiRequest(`/api/networks/${networkId}`, {
method: 'DELETE',
});
},
};