Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions packages/grid_client/scripts/multiple_gateway_backends.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { FilterOptions, GatewayNameModel } from "../src";
import { getClient } from "./client_loader";
import { log } from "./utils";

async function deploy(client, gw) {
const res = await client.gateway.deploy_name(gw);
log("================= Deploying NAME gateway with multiple backends =================");
log(res);
log("================= Deploying NAME gateway with multiple backends =================");
}

async function getDeployment(client, gw) {
const res = await client.gateway.getObj(gw);
log("================= Getting deployment information =================");
log(res);
log("================= Getting deployment information =================");
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function cancel(client, gw) {
const res = await client.gateway.delete_name(gw);
log("================= Canceling the deployment =================");
log(res);
log("================= Canceling the deployment =================");
}

async function main() {
const grid3 = await getClient();

const gatewayQueryOptions: FilterOptions = {
gateway: true,
farmId: 1,
};

const gw: GatewayNameModel = {
name: "multibackends",
node_id: +(await grid3.capacity.filterNodes(gatewayQueryOptions))[0].nodeId,
tls_passthrough: false,
// the backends have to be in this format `http://ip:port` or `https://ip:port`, and the `ip` pingable from the node so using the ygg ip or public ip if available.
backends: ["http://185.206.122.17:8000", "http://185.206.122.22:8000", "http://185.206.122.35:8000"],
};

//Deploy Gateway
await deploy(grid3, gw);

//Get the deployment
await getDeployment(grid3, gw.name);

//Uncomment the line below to cancel the deployment
// await cancel(grid3, { name: gw.name });

grid3.disconnect();
}

main();
166 changes: 143 additions & 23 deletions packages/playground/src/components/manage_gateway_dialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,21 @@
</template>

<template #[`item.backends`]="{ item }">
{{ (Array.isArray(item.backends) ? item.backends[0] : item.backends) ?? "-" }}
<v-tooltip v-if="Array.isArray(item.backends) && item.backends.length > 1" location="top">
<template #activator="{ props }">
<v-chip v-bind="props" size="small" color="info" variant="outlined" class="cursor-pointer">
{{ item.backends.length }} backends
</v-chip>
</template>
<div class="d-flex flex-column gap-1">
<div v-for="(backend, index) in item.backends" :key="index">
<span class="text-caption">{{ backend }}</span>
</div>
</div>
</v-tooltip>
<span v-else>
{{ (Array.isArray(item.backends) ? item.backends[0] : item.backends) ?? "-" }}
</span>
</template>

<template #[`item.status`]="{ item }">
Expand Down Expand Up @@ -134,6 +148,82 @@
color="primary"
/>
</input-tooltip>

<input-tooltip
tooltip="Enable multiple backend configuration for load balancing and failover"
:align-center="true"
>
<v-switch
v-model="multipleBackends"
label="Multiple Backends"
hide-details
inset
density="compact"
variant="tonal"
color="primary"
/>
</input-tooltip>

<template v-if="multipleBackends">
<div class="mt-4">
<v-card class="pa-4" outlined>
<v-card-title class="text-subtitle-1 mb-3">Backend Configuration</v-card-title>

<div v-for="(backend, index) in managedBackends" :key="index" class="mb-3">
<v-row>
<v-col cols="12" md="5">
<input-validator
:value="backend.ip"
:rules="[validators.required('IP is required.'), validators.isIP('IP is not valid.')]"
#="{ props }"
>
<v-text-field
v-model="backend.ip"
:label="`Backend ${index + 1} IP`"
density="compact"
v-bind="props"
/>
</input-validator>
</v-col>

<v-col cols="12" md="4">
<input-validator :value="backend.port" :rules="portRules" #="{ props }">
<v-text-field
v-model.number="backend.port"
:label="`Backend ${index + 1} Port`"
type="number"
density="compact"
v-bind="props"
/>
</input-validator>
</v-col>

<v-col cols="12" md="3" class="d-flex align-center">
<v-btn
v-if="managedBackends.length > 1"
icon="mdi-delete"
variant="outlined"
color="error"
size="small"
@click="removeManagedBackend(index)"
/>
</v-col>
</v-row>
</div>

<v-btn
variant="outlined"
color="primary"
prepend-icon="mdi-plus"
size="small"
class="mt-2"
@click="addManagedBackend"
>
Add Backend
</v-btn>
</v-card>
</div>
</template>
<div style="margin-top: -15px">
<TfSelectionDetails
v-model="selectionDetails"
Expand Down Expand Up @@ -212,6 +302,7 @@ import type { NetworkFeatures, SelectionDetails } from "../types/nodeSelector";
import {
type DeployGatewayConfig,
deployGatewayName,
deployGatewayNameMultiBackend,
extractDomainIP,
getDeploymentIps,
type GridGateway,
Expand Down Expand Up @@ -259,8 +350,10 @@ export default {
const subdomain = ref("");
const port = ref(props.vm ? 80 : 443);
const passThrough = ref(false);
const multipleBackends = ref(false);
const valid = ref(false);
const selectionDetails = ref<SelectionDetails>();
const managedBackends = ref([{ ip: "", port: props.vm ? 80 : 443 }]);
const networks = ref<VMNetwork[]>([]);
const selectedIPAddress = ref<string>();
const networkName = props.vm
Expand Down Expand Up @@ -399,38 +492,51 @@ export default {
async function deployGateway() {
layout.value.setStatus("deploy");
try {
const IP = selectedIPAddress.value as string;

const gwConfig: DeployGatewayConfig = {
subdomain: subdomain.value,
ip: IP,
port: port.value,
tlsPassthrough: passThrough.value,
};

if (isWireGuard.value) {
gwConfig.network = networkName;
const [x, y] = IP.split(".");
const data = {
name: networkName,
ipRange: `${x}.${y}.0.0/16`,
nodeId: selectionDetails.value!.domain!.selectedDomain!.nodeId,
mycelium: false,
if (multipleBackends.value) {
const multiConfig = {
subdomain: subdomain.value,
backends: managedBackends.value.map(backend => ({
ip: backend.ip,
port: backend.port,
})),
tlsPassthrough: passThrough.value,
};

const hasNode = await grid.networks.hasNode(data);
await deployGatewayNameMultiBackend(grid, selectionDetails.value!.domain, multiConfig);
} else {
const IP = selectedIPAddress.value as string;
const gwConfig: DeployGatewayConfig = {
subdomain: subdomain.value,
ip: IP,
port: port.value,
tlsPassthrough: passThrough.value,
};

if (!hasNode) {
await grid.networks.addNode(data);
if (isWireGuard.value) {
gwConfig.network = networkName;
const [x, y] = IP.split(".");
const data = {
name: networkName,
ipRange: `${x}.${y}.0.0/16`,
nodeId: selectionDetails.value!.domain!.selectedDomain!.nodeId,
mycelium: false,
};

const hasNode = await grid.networks.hasNode(data);

if (!hasNode) {
await grid.networks.addNode(data);
}
}

await deployGatewayName(grid, selectionDetails.value!.domain, gwConfig);
}

await deployGatewayName(grid, selectionDetails.value!.domain, gwConfig);
suggestName();
// get gateway url
const gatewayUrl = selectionDetails.value!.domain!.useFQDN
? `https://${selectionDetails.value!.domain!.customDomain}`
: `https://${gwConfig.subdomain}.${selectionDetails.value!.domain!.selectedDomain!.publicConfig.domain}`;
: `https://${subdomain.value}.${selectionDetails.value!.domain!.selectedDomain!.publicConfig.domain}`;
layout.value.setStatus("success", `Successfully deployed gateway at ${gatewayUrl}`);
} catch (error) {
errorMessage.value = "Failed to add domain";
Expand Down Expand Up @@ -509,6 +615,16 @@ export default {
loadGateways();
}

function addManagedBackend() {
managedBackends.value.push({ ip: "", port: props.vm ? 80 : 443 });
}

function removeManagedBackend(index: number) {
if (managedBackends.value.length > 1) {
managedBackends.value.splice(index, 1);
}
}

function suggestName() {
if (props.k8s) {
oldPrefix.value = props.k8s.projectName.toLowerCase().includes(ProjectName.Fullvm.toLowerCase())
Expand Down Expand Up @@ -545,6 +661,8 @@ export default {
subdomain,
port,
passThrough,
multipleBackends,
managedBackends,
valid,
selectionDetails,
networks,
Expand Down Expand Up @@ -575,6 +693,8 @@ export default {
availableK8SNodesNames,
selectedK8SNodeName,
errorMessage,
addManagedBackend,
removeManagedBackend,
};
},
};
Expand Down
14 changes: 14 additions & 0 deletions packages/playground/src/components/vm_deployment_table.vue
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,20 @@
{{ item.name }}
</template>

<template #[`item.backends`]="{ item }">
<div class="d-flex flex-wrap gap-1">
<v-chip
v-for="(backend, index) in item[0].workloads[0].data.backends"
:key="index"
size="small"
color="primary"
variant="outlined"
>
{{ backend }}
</v-chip>
</div>
</template>

<template #[`item.ipv4`]="{ item }">
{{ item.publicIP?.ip?.split("/")?.[0] || item.publicIP?.ip || "-" }}
</template>
Expand Down
53 changes: 53 additions & 0 deletions packages/playground/src/utils/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ export interface DeployGatewayConfig {
tlsPassthrough?: boolean;
}

export interface DeployGatewayMultiBackendConfig {
subdomain: string;
backends: Array<{
ip: string;
port: number;
}>;
network?: string;
tlsPassthrough?: boolean;
}

export async function deployGatewayName(
grid: GridClient | null,
domain: DomainInfo | undefined,
Expand Down Expand Up @@ -76,6 +86,49 @@ export async function deployGatewayName(
return grid.gateway.deploy_name(gw);
}

export async function deployGatewayNameMultiBackend(
grid: GridClient | null,
domain: DomainInfo | undefined,
config: DeployGatewayMultiBackendConfig,
) {
if (!grid) {
throw new Error("Fetch Grid Failed");
}

if (!domain || !domain.selectedDomain) {
throw new Error("Please provide a valid domain name data.");
}

if (!config.backends || config.backends.length === 0) {
throw new Error("Please provide at least one backend.");
}

await grid.gateway.getObj(config.subdomain);

const id = process.env.INTERNAL_SOLUTION_PROVIDER_ID;

const gw = new GatewayNameModel();
gw.name = config.subdomain;
gw.node_id = domain.selectedDomain.nodeId;
gw.tls_passthrough = config.tlsPassthrough || false;
gw.network = config.network;
gw.solutionProviderId = id ? +id : undefined;
gw.backends = config.backends.map(backend => {
if (validator.isIP(backend.ip, "6")) {
return `${config.tlsPassthrough ? "" : "http://"}[${backend.ip}]:${backend.port}`;
} else {
return `${config.tlsPassthrough ? "" : "http://"}${backend.ip}:${backend.port}`;
}
});

if (domain.useFQDN) {
(gw as GatewayFQDNModel).fqdn = domain.customDomain;
return grid.gateway.deploy_fqdn(gw as GatewayFQDNModel);
}

return grid.gateway.deploy_name(gw);
}

export async function rollbackDeployment(grid: GridClient, name: string) {
const result = await grid.machines.delete({ name });

Expand Down
Loading