- Add GET /auth/status endpoint (returns needsSetup when no admin exists) - Add POST /auth/setup endpoint (public first-admin registration) - Add IsAdminExists + SetupInitialAdmin methods to AuthService - Frontend: detect needsSetup on load, show setup page with admin registration - Frontend: fall back to login page when setup is already complete - Docker compose: env_file already optional (required: false), no changes needed - Bootstrap: auto-detect BOOTSTRAP_CLUSTERS without separate enable flag
416 lines
18 KiB
TypeScript
416 lines
18 KiB
TypeScript
/**
|
|
* API Client entry point
|
|
* Export configured API client, generated functions, and friendly aliases.
|
|
*/
|
|
|
|
type AxiosOptions<T extends (...args: never[]) => unknown> = Parameters<T>[2];
|
|
|
|
import {
|
|
deleteClustersClusterId,
|
|
deleteClustersClusterIdInstancesInstanceId,
|
|
deleteRegistriesRegistryId,
|
|
getClusters,
|
|
getClustersClusterId,
|
|
getClustersClusterIdHealth,
|
|
getClustersClusterIdInstances,
|
|
getClustersClusterIdInstancesInstanceId,
|
|
getClustersClusterIdInstancesInstanceIdEntries,
|
|
getMonitoringClusters,
|
|
getMonitoringClustersClusterId,
|
|
getMonitoringClustersClusterIdNodes,
|
|
getMonitoringSummary,
|
|
getRegistries,
|
|
getRegistriesRegistryId,
|
|
getRegistriesRegistryIdHealth,
|
|
getRegistriesRegistryIdRepositories,
|
|
getRegistriesRegistryIdRepositoriesRepositoryNameArtifacts,
|
|
getRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReference,
|
|
getRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchema,
|
|
postAuthLogin,
|
|
postAuthRefresh,
|
|
postAuthRegister,
|
|
postClusters,
|
|
postClustersClusterIdInstances,
|
|
postRegistries,
|
|
putClustersClusterId,
|
|
putClustersClusterIdInstancesInstanceId,
|
|
putRegistriesRegistryId,
|
|
} from './generated-orval/api';
|
|
|
|
import type {
|
|
DeleteClustersClusterIdInstancesInstanceIdPathParameters,
|
|
DeleteClustersClusterIdPathParameters,
|
|
DeleteRegistriesRegistryIdPathParameters,
|
|
GetClustersClusterIdInstancesInstanceIdPathParameters,
|
|
GetClustersClusterIdInstancesPathParameters,
|
|
GetRegistriesRegistryIdHealthPathParameters,
|
|
GetRegistriesRegistryIdPathParameters,
|
|
GetRegistriesRegistryIdRepositoriesPathParameters,
|
|
GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsPathParameters,
|
|
GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferencePathParameters,
|
|
GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchemaPathParameters,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoArtifactResponse as GeneratedArtifactResponse,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoAuthResponse as GeneratedAuthResponse,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoClusterMetricsResponse as GeneratedClusterMetricsResponse,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoClusterResponse as GeneratedClusterResponse,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateClusterRequest as GeneratedCreateClusterRequest,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateInstanceRequest as GeneratedCreateInstanceRequest,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateRegistryRequest as GeneratedCreateRegistryRequest,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceEntryResponse as GeneratedInstanceEntry,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponse as GeneratedInstanceResponse,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoLoginRequest as GeneratedLoginRequest,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoMonitoringSummaryResponse as GeneratedMonitoringSummary,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoNodeMetricsResponse as GeneratedNodeMetricsResponse,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRefreshTokenRequest as GeneratedRefreshTokenRequest,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRegisterRequest as GeneratedRegisterRequest,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRegistryHealthResponse as GeneratedRegistryHealthResponse,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRegistryResponse as GeneratedRegistryResponse,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRepositoryListResponse as GeneratedRepositoryListResponse,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoTagResponse as GeneratedTagResponse,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateClusterRequest as GeneratedUpdateClusterRequest,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateInstanceRequest as GeneratedUpdateInstanceRequest,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateRegistryRequest as GeneratedUpdateRegistryRequest,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUserResponse as GeneratedUserResponse,
|
|
PutClustersClusterIdInstancesInstanceIdPathParameters,
|
|
PutClustersClusterIdPathParameters,
|
|
PutRegistriesRegistryIdPathParameters,
|
|
} from './generated-orval/api.schemas';
|
|
|
|
import { AXIOS_INSTANCE, customAxiosInstance } from './axios-mutator';
|
|
|
|
import {
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseLastOperation as GeneratedInstanceLastOperationEnum,
|
|
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseStatus as GeneratedInstanceStatusEnum,
|
|
} from './generated-orval/api.schemas';
|
|
|
|
export { AXIOS_INSTANCE, customAxiosInstance, setAuthToken } from './axios-mutator';
|
|
export { keysToCamel, keysToSnake, snakeToCamel, camelToSnake } from '@/shared/utils/case-converter';
|
|
|
|
// Re-export raw generated APIs/types for advanced usages
|
|
export * from './generated-orval/api';
|
|
export type * from './generated-orval/api.schemas';
|
|
|
|
// ---------- Friendly type aliases ----------
|
|
export type AuthResponse = GeneratedAuthResponse;
|
|
export type RegisterBody = GeneratedRegisterRequest;
|
|
export type AdminCreateUserRequest = RegisterBody & {
|
|
role?: string;
|
|
workspaceId?: string;
|
|
namespace?: string;
|
|
defaultClusterId?: string;
|
|
quotaCpu?: string;
|
|
quotaMemory?: string;
|
|
quotaGpu?: string;
|
|
quotaGpuMemory?: string;
|
|
isActive?: boolean;
|
|
mustChangePassword?: boolean;
|
|
};
|
|
export type LoginBody = GeneratedLoginRequest;
|
|
export type RefreshTokenBody = GeneratedRefreshTokenRequest;
|
|
export type UserResponse = GeneratedUserResponse & {
|
|
role?: string;
|
|
workspaceId?: string;
|
|
workspaceName?: string;
|
|
namespace?: string;
|
|
defaultClusterId?: string;
|
|
quotaCpu?: string;
|
|
quotaMemory?: string;
|
|
quotaGpu?: string;
|
|
quotaGpuMemory?: string;
|
|
isActive?: boolean;
|
|
mustChangePassword?: boolean;
|
|
};
|
|
export type UpdateUserRequest = {
|
|
role?: string;
|
|
workspaceId?: string;
|
|
namespace?: string;
|
|
defaultClusterId?: string;
|
|
quotaCpu?: string;
|
|
quotaMemory?: string;
|
|
quotaGpu?: string;
|
|
quotaGpuMemory?: string;
|
|
isActive?: boolean;
|
|
mustChangePassword?: boolean;
|
|
};
|
|
export type ValuesYamlResponse = { valuesYaml: string };
|
|
|
|
export type ClusterResponse = GeneratedClusterResponse;
|
|
export type CreateClusterRequest = GeneratedCreateClusterRequest;
|
|
export type UpdateClusterRequest = GeneratedUpdateClusterRequest;
|
|
|
|
export type RegistryResponse = GeneratedRegistryResponse;
|
|
export type CreateRegistryRequest = GeneratedCreateRegistryRequest;
|
|
export type UpdateRegistryRequest = GeneratedUpdateRegistryRequest;
|
|
export type RegistryHealthResponse = GeneratedRegistryHealthResponse;
|
|
|
|
export type InstanceResponse = GeneratedInstanceResponse & {
|
|
ownerId?: string;
|
|
ownerUsername?: string;
|
|
};
|
|
export type CreateInstanceRequest = GeneratedCreateInstanceRequest;
|
|
export type UpdateInstanceRequest = GeneratedUpdateInstanceRequest;
|
|
export type InstanceEntry = GeneratedInstanceEntry;
|
|
export type InstanceDiagnosticsResponse = {
|
|
instanceName?: string;
|
|
namespace?: string;
|
|
collectedAt?: string;
|
|
pods?: Array<{
|
|
name?: string;
|
|
namespace?: string;
|
|
phase?: string;
|
|
nodeName?: string;
|
|
podIp?: string;
|
|
hostIp?: string;
|
|
restartCount?: number;
|
|
containers?: Array<{
|
|
name?: string;
|
|
image?: string;
|
|
ready?: boolean;
|
|
restartCount?: number;
|
|
state?: string;
|
|
reason?: string;
|
|
message?: string;
|
|
}>;
|
|
conditions?: Array<{ type?: string; status?: string; reason?: string; message?: string }>;
|
|
creationTimestamp?: string;
|
|
}>;
|
|
services?: Array<{
|
|
name?: string;
|
|
namespace?: string;
|
|
type?: string;
|
|
clusterIP?: string;
|
|
ports?: Array<{ name?: string; protocol?: string; port?: number; targetPort?: string; nodePort?: number }>;
|
|
}>;
|
|
events?: Array<{
|
|
type?: string;
|
|
reason?: string;
|
|
message?: string;
|
|
involvedKind?: string;
|
|
involvedName?: string;
|
|
count?: number;
|
|
firstTimestamp?: string;
|
|
lastTimestamp?: string;
|
|
}>;
|
|
logs?: Array<{ pod?: string; container?: string; tailLines?: number; log?: string; error?: string }>;
|
|
};
|
|
export const INSTANCE_STATUS = GeneratedInstanceStatusEnum;
|
|
export type InstanceStatus = NonNullable<InstanceResponse['status']>;
|
|
export const INSTANCE_LAST_OPERATION = GeneratedInstanceLastOperationEnum;
|
|
export type InstanceLastOperation = NonNullable<InstanceResponse['lastOperation']>;
|
|
|
|
export type ArtifactResponse = GeneratedArtifactResponse;
|
|
export type ArtifactListItem = GeneratedTagResponse;
|
|
export type ListRepositories200Item =
|
|
| {
|
|
name?: string;
|
|
artifact_count?: number;
|
|
artifactCount?: number;
|
|
}
|
|
| string;
|
|
export type RepositoryListResponse = GeneratedRepositoryListResponse;
|
|
export type ListArtifactsFilter = 'all' | 'chart' | 'image' | 'other';
|
|
|
|
export type ClusterMonitoring = GeneratedClusterMetricsResponse;
|
|
export type ClusterMonitoringStatus = ClusterMonitoring['status'];
|
|
export type MonitoringSummary = GeneratedMonitoringSummary;
|
|
export type NodeMetricsResponse = GeneratedNodeMetricsResponse;
|
|
|
|
// ---------- Friendly function aliases ----------
|
|
export const login = postAuthLogin;
|
|
export const register = postAuthRegister;
|
|
export const refreshAuth = postAuthRefresh;
|
|
export const fetchAuthStatus = () =>
|
|
AXIOS_INSTANCE.get<{ needsSetup: boolean; hasUsers: boolean }>("/auth/status").then((r) => r.data);
|
|
export const setupInitialAdmin = (data: { username: string; password: string; email?: string }) =>
|
|
AXIOS_INSTANCE.post<{ accessToken: string; refreshToken: string }>("/auth/setup", data).then((r) => r.data);
|
|
export const listUsers = () => customAxiosInstance<UserResponse[]>({ url: "/users", method: "GET" });
|
|
export const createUser = (data: AdminCreateUserRequest) =>
|
|
customAxiosInstance<UserResponse>({ url: "/users", method: "POST", data });
|
|
export const updateUser = (userId: string, data: UpdateUserRequest) =>
|
|
customAxiosInstance<UserResponse>({ url: `/users/${encodeURIComponent(userId)}`, method: "PUT", data });
|
|
export const deleteUser = (userId: string) =>
|
|
customAxiosInstance<void>({ url: `/users/${encodeURIComponent(userId)}`, method: "DELETE" });
|
|
|
|
export const listClusters = getClusters;
|
|
export const createCluster = postClusters;
|
|
export const getCluster = getClustersClusterId;
|
|
export const updateCluster = putClustersClusterId;
|
|
export const deleteCluster = deleteClustersClusterId;
|
|
export const getClusterHealth = getClustersClusterIdHealth;
|
|
|
|
export const listInstances = getClustersClusterIdInstances;
|
|
export const createInstance = postClustersClusterIdInstances;
|
|
export const getInstance = getClustersClusterIdInstancesInstanceId;
|
|
export const updateInstance = putClustersClusterIdInstancesInstanceId;
|
|
export const deleteInstance = deleteClustersClusterIdInstancesInstanceId;
|
|
export const listInstanceEntries = getClustersClusterIdInstancesInstanceIdEntries;
|
|
export const scaleInstance = (
|
|
clusterId: string,
|
|
instanceId: string,
|
|
body: { replicas: number; workload?: string },
|
|
) => {
|
|
return customAxiosInstance<{ instance: InstanceResponse; replicas: number; message: string }>({
|
|
url: `/clusters/${encodeURIComponent(clusterId)}/instances/${encodeURIComponent(instanceId)}/scale`,
|
|
method: "POST",
|
|
data: body,
|
|
});
|
|
};
|
|
export const getInstanceValuesDiff = (
|
|
clusterId: string,
|
|
instanceId: string,
|
|
) => {
|
|
return customAxiosInstance<{ current: Record<string, unknown>; defaults: Record<string, unknown> }>({
|
|
url: `/clusters/${encodeURIComponent(clusterId)}/instances/${encodeURIComponent(instanceId)}/values-diff`,
|
|
method: "GET",
|
|
});
|
|
};
|
|
export const getInstanceDiagnostics = (
|
|
params: { clusterId: string; instanceId: string },
|
|
options?: { tailLines?: number },
|
|
) =>
|
|
customAxiosInstance<InstanceDiagnosticsResponse>({
|
|
url: `/clusters/${encodeURIComponent(params.clusterId)}/instances/${encodeURIComponent(params.instanceId)}/diagnostics`,
|
|
method: "GET",
|
|
params: options?.tailLines ? { tailLines: options.tailLines } : undefined,
|
|
});
|
|
|
|
/**
|
|
* Stream pod logs via SSE from the backend.
|
|
* Returns an AbortController to cancel the stream at any time.
|
|
*/
|
|
export function streamInstanceLogs(
|
|
clusterId: string,
|
|
instanceId: string,
|
|
pod: string,
|
|
container: string,
|
|
tailLines: number = 200,
|
|
onLine: (line: string) => void,
|
|
onDone: () => void,
|
|
onError: (err: Error) => void,
|
|
): AbortController {
|
|
const controller = new AbortController();
|
|
const baseUrl = AXIOS_INSTANCE.defaults.baseURL ?? "/api/v1";
|
|
const authHeader = AXIOS_INSTANCE.defaults.headers.common["Authorization"] as string | undefined;
|
|
|
|
const params = new URLSearchParams({ pod, container, tailLines: String(tailLines) });
|
|
const url = `${baseUrl}/clusters/${encodeURIComponent(clusterId)}/instances/${encodeURIComponent(instanceId)}/logs/stream?${params}`;
|
|
|
|
const headers: Record<string, string> = { Accept: "text/event-stream" };
|
|
if (authHeader) {
|
|
headers["Authorization"] = authHeader;
|
|
}
|
|
|
|
fetch(url, { headers, signal: controller.signal })
|
|
.then(async (response) => {
|
|
if (!response.ok) {
|
|
const text = await response.text().catch(() => response.statusText);
|
|
onError(new Error(`HTTP ${response.status}: ${text}`));
|
|
return;
|
|
}
|
|
const reader = response.body?.getReader();
|
|
if (!reader) {
|
|
onError(new Error("ReadableStream not supported"));
|
|
return;
|
|
}
|
|
const decoder = new TextDecoder();
|
|
let buffer = "";
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
buffer += decoder.decode(value, { stream: true });
|
|
const lines = buffer.split("\n");
|
|
// Keep the last potentially-incomplete line in the buffer
|
|
buffer = lines.pop() ?? "";
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || !trimmed.startsWith("data:")) continue;
|
|
const data = trimmed.slice(5).trim();
|
|
if (data === "[DONE]") {
|
|
onDone();
|
|
return;
|
|
}
|
|
if (data.startsWith("[ERROR]")) {
|
|
onError(new Error(data.slice(7).trim()));
|
|
continue;
|
|
}
|
|
onLine(data);
|
|
}
|
|
}
|
|
} catch (err: unknown) {
|
|
if (err instanceof DOMException && err.name === "AbortError") {
|
|
// Stream was intentionally cancelled - not an error
|
|
return;
|
|
}
|
|
onError(err instanceof Error ? err : new Error(String(err)));
|
|
}
|
|
onDone();
|
|
})
|
|
.catch((err: unknown) => {
|
|
if (err instanceof DOMException && err.name === "AbortError") {
|
|
return;
|
|
}
|
|
onError(err instanceof Error ? err : new Error(String(err)));
|
|
});
|
|
|
|
return controller;
|
|
}
|
|
|
|
export const listRegistries = getRegistries;
|
|
export const createRegistry = postRegistries;
|
|
export const getRegistry = getRegistriesRegistryId;
|
|
export const updateRegistry = putRegistriesRegistryId;
|
|
export const deleteRegistry = deleteRegistriesRegistryId;
|
|
export const checkRegistryHealth = getRegistriesRegistryIdHealth;
|
|
|
|
export const listRepositories = (
|
|
params: GetRegistriesRegistryIdRepositoriesPathParameters,
|
|
options?: { artifactType?: 'chart' | 'all' },
|
|
) =>
|
|
getRegistriesRegistryIdRepositories(params, {
|
|
params: options?.artifactType ? { artifact_type: options.artifactType } : undefined,
|
|
});
|
|
type ListArtifactsRequestOptions = AxiosOptions<typeof getRegistriesRegistryIdRepositoriesRepositoryNameArtifacts>;
|
|
|
|
export const listArtifacts = (
|
|
params: GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsPathParameters,
|
|
options?: { filter?: ListArtifactsFilter },
|
|
axiosOptions?: ListArtifactsRequestOptions,
|
|
) => {
|
|
const query =
|
|
options?.filter && options.filter !== 'all'
|
|
? { media_type: options.filter }
|
|
: undefined;
|
|
return getRegistriesRegistryIdRepositoriesRepositoryNameArtifacts(params, query, axiosOptions);
|
|
};
|
|
|
|
export const getArtifact = getRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReference;
|
|
export const getValuesSchema = getRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchema;
|
|
export const getValuesYaml = (params: GetValuesSchemaPathParameters) =>
|
|
customAxiosInstance<ValuesYamlResponse>({
|
|
url: `/registries/${encodeURIComponent(params.registryId)}/repositories/${encodeURIComponent(params.repositoryName)}/artifacts/${encodeURIComponent(params.reference)}/values-yaml`,
|
|
method: "GET",
|
|
});
|
|
|
|
export const listClusterMonitoring = getMonitoringClusters;
|
|
export const getClusterMonitoring = getMonitoringClustersClusterId;
|
|
export const getClusterNodeMetrics = getMonitoringClustersClusterIdNodes;
|
|
export const getMonitoringSummaryData = getMonitoringSummary;
|
|
|
|
// Re-export parameter types with friendly names for caller convenience
|
|
export type DeleteClusterPathParameters = DeleteClustersClusterIdPathParameters;
|
|
export type UpdateClusterPathParameters = PutClustersClusterIdPathParameters;
|
|
export type ClusterInstancesPathParameters = GetClustersClusterIdInstancesPathParameters;
|
|
export type InstancePathParameters = GetClustersClusterIdInstancesInstanceIdPathParameters;
|
|
export type UpdateInstancePathParameters = PutClustersClusterIdInstancesInstanceIdPathParameters;
|
|
export type DeleteInstancePathParameters = DeleteClustersClusterIdInstancesInstanceIdPathParameters;
|
|
export type RegistryPathParameters = GetRegistriesRegistryIdPathParameters;
|
|
export type UpdateRegistryPathParameters = PutRegistriesRegistryIdPathParameters;
|
|
export type DeleteRegistryPathParameters = DeleteRegistriesRegistryIdPathParameters;
|
|
export type RegistryHealthPathParameters = GetRegistriesRegistryIdHealthPathParameters;
|
|
export type ListRepositoriesPathParameters = GetRegistriesRegistryIdRepositoriesPathParameters;
|
|
export type ListArtifactsPathParameters = GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsPathParameters;
|
|
export type ListArtifactsParams = { filter?: ListArtifactsFilter };
|
|
export type GetArtifactPathParameters = GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferencePathParameters;
|
|
export type GetValuesSchemaPathParameters = GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchemaPathParameters;
|