feat: Lightsail + svg animation
This commit is contained in:
@@ -25,7 +25,9 @@ export const apiEndpointKeys = {
|
||||
invoke: "clouds.services.resources.invoke",
|
||||
start: "clouds.services.resources.start",
|
||||
stop: "clouds.services.resources.stop",
|
||||
reboot: "clouds.services.resources.reboot",
|
||||
reboot: "clouds.services.resources.reboot",
|
||||
deployLightsailStaticFiles: "clouds.services.resources.lightsail.static-files.deploy",
|
||||
deployLightsailNodeApplication: "clouds.services.resources.lightsail.node-application.deploy",
|
||||
publish: "clouds.services.resources.notifications.publish",
|
||||
createSnsSubscription: "clouds.services.resources.notifications.subscription.create",
|
||||
listSnsSubscriptions: "clouds.services.resources.notifications.subscriptions.list",
|
||||
@@ -228,6 +230,7 @@ export const apiEndpointKeys = {
|
||||
securityGroups: "aws.ec2.instances.securityGroups",
|
||||
createImage: "aws.ec2.instances.create-image",
|
||||
console: "aws.ec2.instances.console",
|
||||
checkSsh: "aws.ec2.instances.ssh.check",
|
||||
},
|
||||
amis: {
|
||||
list: "aws.ec2.amis.list",
|
||||
@@ -643,6 +646,20 @@ export const endpointRegistry: EndpointRegistry = new Map([
|
||||
method: "PATCH",
|
||||
},
|
||||
],
|
||||
[
|
||||
apiEndpointKeys.clouds.resources.deployLightsailStaticFiles,
|
||||
{
|
||||
path: "/clouds/aws/services/lightsail/resources/:id/static-files",
|
||||
method: "POST",
|
||||
},
|
||||
],
|
||||
[
|
||||
apiEndpointKeys.clouds.resources.deployLightsailNodeApplication,
|
||||
{
|
||||
path: "/clouds/aws/services/lightsail/resources/:id/node-application",
|
||||
method: "POST",
|
||||
},
|
||||
],
|
||||
[
|
||||
apiEndpointKeys.clouds.resources.delete,
|
||||
{
|
||||
@@ -1129,6 +1146,7 @@ export const endpointRegistry: EndpointRegistry = new Map([
|
||||
[apiEndpointKeys.aws.ec2.instances.securityGroups, { path: "/ec2/instances/:instanceId/security-groups", method: "PUT" }],
|
||||
[apiEndpointKeys.aws.ec2.instances.createImage, { path: "/ec2/instances/:instanceId/image", method: "POST" }],
|
||||
[apiEndpointKeys.aws.ec2.instances.console, { path: "/ec2/instances/:instanceId/console", method: "GET" }],
|
||||
[apiEndpointKeys.aws.ec2.instances.checkSsh, { path: "/ec2/instances/:instanceId/ssh/check", method: "POST" }],
|
||||
[apiEndpointKeys.aws.ec2.amis.list, { path: "/ec2/amis", method: "GET" }],
|
||||
[apiEndpointKeys.aws.ec2.amis.deregister, { path: "/ec2/amis/:imageId/deregister", method: "POST" }],
|
||||
[apiEndpointKeys.aws.ec2.keyPairs.list, { path: "/ec2/key-pairs", method: "GET" }],
|
||||
|
||||
@@ -2,9 +2,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
authorizeEc2SecurityGroupEgress,
|
||||
authorizeEc2SecurityGroupIngress,
|
||||
checkEc2InstanceSsh,
|
||||
createEc2SecurityGroup,
|
||||
revokeEc2SecurityGroupEgress,
|
||||
revokeEc2SecurityGroupIngress,
|
||||
updateEc2InstanceTags,
|
||||
updateEc2InstanceSecurityGroups,
|
||||
} from "./ec2.api";
|
||||
|
||||
@@ -50,6 +52,37 @@ describe("createEc2SecurityGroup", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("checkEc2InstanceSsh", () => {
|
||||
const fetchMock = vi.fn<typeof fetch>();
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.mockResolvedValue(new Response(JSON.stringify({
|
||||
ok: true,
|
||||
banner: "SSH-2.0-OpenSSH_9.6",
|
||||
checkedFrom: "UI backend",
|
||||
checkedAt: "2026-10-09T00:00:00Z",
|
||||
target: { host: "host.docker.internal", port: 2200 },
|
||||
}), {status: 200, headers: {"Content-Type": "application/json"}}));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
it("checks SSH through the UI backend", async () => {
|
||||
await expect(checkEc2InstanceSsh("i-test")).resolves.toMatchObject({
|
||||
ok: true,
|
||||
banner: "SSH-2.0-OpenSSH_9.6",
|
||||
});
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(String(url)).toContain("/api/ec2/instances/i-test/ssh/check");
|
||||
expect(init?.method).toBe("POST");
|
||||
});
|
||||
});
|
||||
|
||||
describe("EC2 security-group rule requests", () => {
|
||||
const fetchMock = vi.fn<typeof fetch>();
|
||||
|
||||
@@ -111,3 +144,32 @@ describe("updateEc2InstanceSecurityGroups", () => {
|
||||
expect(JSON.parse(String(init?.body))).toEqual({groupIds: ["sg-default", "sg-web"]});
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateEc2InstanceTags", () => {
|
||||
const fetchMock = vi.fn<typeof fetch>();
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.mockResolvedValue(new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
it("sends tags as one JSON object", async () => {
|
||||
await updateEc2InstanceTags("i-test", [{ key: "Name", value: "web-01" }], ["old-tag"]);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(String(url)).toContain("/api/ec2/instances/i-test/tags");
|
||||
expect(init?.method).toBe("PUT");
|
||||
expect(JSON.parse(String(init?.body))).toEqual({
|
||||
toAdd: [{ key: "Name", value: "web-01" }],
|
||||
toRemove: ["old-tag"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,6 +83,35 @@ export type Ec2InstanceConnection = {
|
||||
sshPort?: number;
|
||||
containerId?: string;
|
||||
containerStatus: 'running' | 'stopped' | 'unavailable';
|
||||
state?: string;
|
||||
launchStatus?: string;
|
||||
launchEvents?: Ec2LaunchEvent[];
|
||||
selectedImageType?: string;
|
||||
selectedImageReference?: string;
|
||||
sshUser?: string;
|
||||
sshPreinstalled?: boolean;
|
||||
initType?: string;
|
||||
sshServiceName?: string;
|
||||
sshStatus?: string;
|
||||
sshStatusMessage?: string;
|
||||
keyInjectionStatus?: string;
|
||||
publishedPorts?: Record<string, number>;
|
||||
failure?: { code?: string; message?: string } | string;
|
||||
};
|
||||
|
||||
export type Ec2LaunchEvent = {
|
||||
stage?: string;
|
||||
status?: string;
|
||||
timestamp?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export type Ec2SshCheckResult = {
|
||||
ok: true;
|
||||
banner: string;
|
||||
checkedFrom: 'UI backend';
|
||||
checkedAt: string;
|
||||
target: { host: string; port: number };
|
||||
};
|
||||
|
||||
export type Ec2KeyPair = {
|
||||
@@ -323,6 +352,15 @@ export async function terminateEc2Instance(instanceId: string): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
export async function checkEc2InstanceSsh(instanceId: string): Promise<Ec2SshCheckResult> {
|
||||
const res = await apiClient.call<Ec2SshCheckResult>(
|
||||
apiEndpointKeys.aws.ec2.instances.checkSsh,
|
||||
{ body: {} },
|
||||
{ instanceId },
|
||||
);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function updateEc2InstanceTags(
|
||||
instanceId: string,
|
||||
toAdd: Ec2Tag[],
|
||||
@@ -330,7 +368,7 @@ export async function updateEc2InstanceTags(
|
||||
): Promise<void> {
|
||||
await apiClient.call(
|
||||
apiEndpointKeys.aws.ec2.instances.tags,
|
||||
{ body: JSON.stringify({ toAdd, toRemove }), headers: { "content-type": "application/json" } },
|
||||
{ body: { toAdd, toRemove } },
|
||||
{ instanceId },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import { getAccountId } from "@/lib/accountStore";
|
||||
type CloudPathParams = Record<string, string>;
|
||||
|
||||
const DATABASE_MUTATION_TIMEOUT_MS = 5 * 60_000;
|
||||
const LIGHTSAIL_CREATION_TIMEOUT_MS = 3 * 60_000;
|
||||
const SQL_DATA_TIMEOUT_MS = 45_000;
|
||||
/** Floci completes Insights queries instantly by default, but bounded server-side polling can take longer. */
|
||||
const LOGS_QUERY_TIMEOUT_MS = 20_000;
|
||||
@@ -164,10 +165,12 @@ export async function createCloudResource(
|
||||
values: Record<string, unknown>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CloudResource> {
|
||||
const timeout =
|
||||
(cloud === "azure" || cloud === "aws") && service === "database"
|
||||
? DATABASE_MUTATION_TIMEOUT_MS
|
||||
: undefined;
|
||||
const timeout =
|
||||
(cloud === "azure" || cloud === "aws") && service === "database"
|
||||
? DATABASE_MUTATION_TIMEOUT_MS
|
||||
: cloud === "aws" && service === "lightsail"
|
||||
? LIGHTSAIL_CREATION_TIMEOUT_MS
|
||||
: undefined;
|
||||
const res = await apiClient.call<CloudResource, Record<string, unknown>>(
|
||||
apiEndpointKeys.clouds.resources.create,
|
||||
requestOptions(cloud, service, { signal, body: values, timeout }),
|
||||
@@ -1104,6 +1107,36 @@ export async function uploadStorageObject(
|
||||
);
|
||||
}
|
||||
|
||||
export type LightsailStaticDeployment = {status: 'deployed'; files: number}
|
||||
export type LightsailNodeDeployment = {status: 'deployed'; files: number; port: number; releaseId: string}
|
||||
|
||||
export async function deployLightsailStaticFiles(instanceId: string, files: File[]): Promise<LightsailStaticDeployment> {
|
||||
const form = new FormData()
|
||||
for (const file of files) {
|
||||
const path = file.webkitRelativePath || file.name
|
||||
form.append('files', file, path)
|
||||
}
|
||||
const res = await apiClient.call<LightsailStaticDeployment>(
|
||||
apiEndpointKeys.clouds.resources.deployLightsailStaticFiles,
|
||||
requestOptions('aws', 'lightsail', {rawBody: form}),
|
||||
{id: instanceId},
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function deployLightsailNodeApplication(instanceId: string, files: File[], startCommand: string, port: number): Promise<LightsailNodeDeployment> {
|
||||
const form = new FormData()
|
||||
for (const file of files) form.append('files', file, file.webkitRelativePath || file.name)
|
||||
if (startCommand.trim()) form.append('startCommand', startCommand.trim())
|
||||
form.append('port', String(port))
|
||||
const res = await apiClient.call<LightsailNodeDeployment>(
|
||||
apiEndpointKeys.clouds.resources.deployLightsailNodeApplication,
|
||||
requestOptions('aws', 'lightsail', {rawBody: form, timeout: 370_000}),
|
||||
{id: instanceId},
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export function storageObjectDownloadUrl(
|
||||
cloud: CloudProvider,
|
||||
resourceId: string,
|
||||
|
||||
Reference in New Issue
Block a user