Compare commits

...

1 Commits

Author SHA1 Message Date
65b6f61b53 dedicated-instance-deploy 2025-02-25 16:21:20 +09:00
30 changed files with 4151 additions and 9 deletions

2186
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -117,6 +117,7 @@
"vitest": "^1.2.2"
},
"dependencies": {
"@aws-sdk/client-cloudformation": "^3.750.0",
"@aws-sdk/client-elasticache": "^3.637.0",
"@aws-sdk/client-iam": "^3.525.0",
"@aws-sdk/client-kms": "^3.609.0",
@ -161,6 +162,7 @@
"@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0",
"argon2": "^0.31.2",
"aws-cdk-lib": "^2.180.0",
"aws-sdk": "^2.1553.0",
"axios": "^1.6.7",
"axios-retry": "^4.0.0",

View File

@ -19,6 +19,7 @@ import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/ser
import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
import { TKmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service";
import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service";
import { TDedicatedInstanceServiceFactory } from "@app/ee/services/dedicated-instance/dedicated-instance-service";
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
@ -228,6 +229,7 @@ declare module "fastify" {
secretSync: TSecretSyncServiceFactory;
kmip: TKmipServiceFactory;
kmipOperation: TKmipOperationServiceFactory;
dedicatedInstance: TDedicatedInstanceServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

View File

@ -930,5 +930,10 @@ declare module "knex/types/tables" {
TKmipClientCertificatesInsert,
TKmipClientCertificatesUpdate
>;
[TableName.DedicatedInstances]: KnexOriginal.CompositeTableType<
TDedicatedInstances,
TDedicatedInstancesInsert,
TDedicatedInstancesUpdate
>;
}
}

View File

@ -0,0 +1,56 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const isTablePresent = await knex.schema.hasTable(TableName.DedicatedInstances);
if (!isTablePresent) {
await knex.schema.createTable(TableName.DedicatedInstances, (t) => {
t.uuid("id").primary().defaultTo(knex.fn.uuid());
t.uuid("orgId").notNullable();
t.string("instanceName").notNullable();
t.string("subdomain").notNullable().unique();
t.enum("status", ["RUNNING", "UPGRADING", "PROVISIONING", "FAILED"]).notNullable();
t.string("rdsInstanceType").notNullable();
t.string("elasticCacheType").notNullable();
t.integer("elasticContainerMemory").notNullable();
t.integer("elasticContainerCpu").notNullable();
t.string("region").notNullable();
t.string("version").notNullable();
t.integer("backupRetentionDays").defaultTo(7);
t.timestamp("lastBackupTime").nullable();
t.timestamp("lastUpgradeTime").nullable();
t.boolean("publiclyAccessible").defaultTo(false);
t.string("vpcId").nullable();
t.specificType("subnetIds", "text[]").nullable();
t.jsonb("tags").nullable();
t.boolean("multiAz").defaultTo(true);
t.integer("rdsAllocatedStorage").defaultTo(50);
t.integer("rdsBackupRetentionDays").defaultTo(7);
t.integer("redisNumCacheNodes").defaultTo(1);
t.integer("desiredContainerCount").defaultTo(1);
t.string("stackName").nullable();
t.text("rdsInstanceId").nullable();
t.text("redisClusterId").nullable();
t.text("ecsClusterArn").nullable();
t.text("ecsServiceArn").nullable();
t.specificType("securityGroupIds", "text[]").nullable();
t.text("error").nullable();
t.timestamps(true, true, true);
t.foreign("orgId")
.references("id")
.inTable(TableName.Organization)
.onDelete("CASCADE");
t.unique(["orgId", "instanceName"]);
});
}
await createOnUpdateTrigger(knex, TableName.DedicatedInstances);
}
export async function down(knex: Knex): Promise<void> {
await dropOnUpdateTrigger(knex, TableName.DedicatedInstances);
await knex.schema.dropTableIfExists(TableName.DedicatedInstances);
}

View File

@ -0,0 +1,16 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
// First drop the existing constraint
await knex.raw(`ALTER TABLE ${TableName.DedicatedInstances} DROP CONSTRAINT IF EXISTS dedicated_instances_status_check`);
// Add the new constraint with updated enum values
await knex.raw(`ALTER TABLE ${TableName.DedicatedInstances} ADD CONSTRAINT dedicated_instances_status_check CHECK (status IN ('RUNNING', 'UPGRADING', 'PROVISIONING', 'FAILED'))`);
}
export async function down(knex: Knex): Promise<void> {
// Revert back to original constraint
await knex.raw(`ALTER TABLE ${TableName.DedicatedInstances} DROP CONSTRAINT IF EXISTS dedicated_instances_status_check`);
await knex.raw(`ALTER TABLE ${TableName.DedicatedInstances} ADD CONSTRAINT dedicated_instances_status_check CHECK (status IN ('RUNNING', 'UPGRADING', 'PROVISIONING'))`);
}

View File

@ -0,0 +1,34 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const DedicatedInstancesSchema = z.object({
id: z.string().uuid(),
orgId: z.string().uuid(),
instanceName: z.string(),
status: z.string(),
rdsInstanceType: z.string(),
elasticCacheType: z.string(),
elasticContainerMemory: z.number(),
elasticContainerCpu: z.number(),
region: z.string(),
version: z.string(),
backupRetentionDays: z.number().default(7).nullable().optional(),
lastBackupTime: z.date().nullable().optional(),
lastUpgradeTime: z.date().nullable().optional(),
publiclyAccessible: z.boolean().default(false).nullable().optional(),
vpcId: z.string().nullable().optional(),
subnetIds: z.string().array().nullable().optional(),
tags: z.unknown().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TDedicatedInstances = z.infer<typeof DedicatedInstancesSchema>;
export type TDedicatedInstancesInsert = Omit<z.input<typeof DedicatedInstancesSchema>, TImmutableDBKeys>;
export type TDedicatedInstancesUpdate = Partial<Omit<z.input<typeof DedicatedInstancesSchema>, TImmutableDBKeys>>;

View File

@ -2,6 +2,7 @@ import { z } from "zod";
export enum TableName {
Users = "users",
Organization = "organizations",
SshCertificateAuthority = "ssh_certificate_authorities",
SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets",
SshCertificateTemplate = "ssh_certificate_templates",
@ -29,7 +30,6 @@ export enum TableName {
AuthTokens = "auth_tokens",
AuthTokenSession = "auth_token_sessions",
BackupPrivateKey = "backup_private_key",
Organization = "organizations",
OrgMembership = "org_memberships",
OrgRoles = "org_roles",
OrgBot = "org_bots",
@ -136,7 +136,8 @@ export enum TableName {
KmipClient = "kmip_clients",
KmipOrgConfig = "kmip_org_configs",
KmipOrgServerCertificates = "kmip_org_server_certificates",
KmipClientCertificates = "kmip_client_certificates"
KmipClientCertificates = "kmip_client_certificates",
DedicatedInstances = "dedicated_instances"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,141 @@
import { z } from "zod";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const DedicatedInstanceSchema = z.object({
id: z.string().uuid(),
orgId: z.string().uuid(),
instanceName: z.string().min(1),
subdomain: z.string().min(1),
status: z.enum(["RUNNING", "UPGRADING", "PROVISIONING", "FAILED"]),
rdsInstanceType: z.string(),
elasticCacheType: z.string(),
elasticContainerMemory: z.number(),
elasticContainerCpu: z.number(),
region: z.string(),
version: z.string(),
backupRetentionDays: z.number(),
lastBackupTime: z.date().nullable(),
lastUpgradeTime: z.date().nullable(),
publiclyAccessible: z.boolean(),
vpcId: z.string().nullable(),
subnetIds: z.array(z.string()).nullable(),
tags: z.record(z.string()).nullable(),
createdAt: z.date(),
updatedAt: z.date()
});
const CreateDedicatedInstanceSchema = z.object({
instanceName: z.string().min(1),
subdomain: z.string().min(1),
provider: z.literal('aws'), // Only allow 'aws' as provider
region: z.string(),
publiclyAccessible: z.boolean().default(false)
});
const DedicatedInstanceDetailsSchema = DedicatedInstanceSchema.extend({
stackStatus: z.string().optional(),
stackStatusReason: z.string().optional(),
error: z.string().nullable(),
events: z.array(
z.object({
timestamp: z.date().optional(),
logicalResourceId: z.string().optional(),
resourceType: z.string().optional(),
resourceStatus: z.string().optional(),
resourceStatusReason: z.string().optional()
})
).optional()
});
export const registerDedicatedInstanceRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:organizationId/dedicated-instances",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
organizationId: z.string().uuid()
}),
response: {
200: z.object({
instances: DedicatedInstanceSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const instances = await server.services.dedicatedInstance.listInstances({
orgId: req.params.organizationId
});
return { instances };
}
});
server.route({
method: "POST",
url: "/:organizationId/dedicated-instances",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
organizationId: z.string().uuid()
}),
body: CreateDedicatedInstanceSchema
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { organizationId } = req.params;
const { instanceName, subdomain, region, publiclyAccessible, provider} = req.body;
const instance = await server.services.dedicatedInstance.createInstance({
orgId: organizationId,
instanceName,
subdomain,
region,
publiclyAccessible,
provider: provider,
dryRun: false,
});
return instance;
}
});
server.route({
method: "GET",
url: "/:organizationId/dedicated-instances/:instanceId",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
organizationId: z.string().uuid(),
instanceId: z.string().uuid()
}),
response: {
200: DedicatedInstanceDetailsSchema
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { organizationId, instanceId } = req.params;
const { instance, stackStatus, stackStatusReason, events } = await server.services.dedicatedInstance.getInstance({
orgId: organizationId,
instanceId
});
return {
...instance,
stackStatus,
stackStatusReason,
events
};
}
});
};

View File

@ -4,6 +4,7 @@ import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-rou
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
import { registerDedicatedInstanceRouter } from "./dedicated-instance-router";
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerExternalKmsRouter } from "./external-kms-router";
@ -38,6 +39,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
// org role starts with organization
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
await server.register(registerLicenseRouter, { prefix: "/organizations" });
await server.register(registerDedicatedInstanceRouter, { prefix: "/organizations" });
await server.register(
async (projectRouter) => {
await projectRouter.register(registerProjectRoleRouter);

View File

@ -0,0 +1,28 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TDedicatedInstanceDALFactory = ReturnType<typeof dedicatedInstanceDALFactory>;
export const dedicatedInstanceDALFactory = (db: TDbClient) => {
const dedicatedInstanceOrm = ormify(db, TableName.DedicatedInstances);
const findInstancesByOrgId = async (orgId: string, tx?: Knex) => {
try {
const instances = await (tx || db.replicaNode())(TableName.DedicatedInstances)
.where({ orgId })
.select("*");
return instances;
} catch (error) {
throw new DatabaseError({ error, name: "Find instances by org ID" });
}
};
return {
...dedicatedInstanceOrm,
findInstancesByOrgId
};
};

View File

@ -0,0 +1,470 @@
import { ForbiddenError } from "@casl/ability";
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as elasticache from 'aws-cdk-lib/aws-elasticache';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as iam from 'aws-cdk-lib/aws-iam';
import { randomBytes } from 'crypto';
import { CloudFormationClient, CreateStackCommand, DescribeStacksCommand, DescribeStackEventsCommand } from "@aws-sdk/client-cloudformation";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TDedicatedInstanceDALFactory } from "./dedicated-instance-dal";
type TDedicatedInstanceServiceFactoryDep = {
dedicatedInstanceDAL: TDedicatedInstanceDALFactory;
permissionService: TPermissionServiceFactory;
};
interface CreateInstanceParams {
orgId: string;
instanceName: string;
subdomain: string;
region: string;
provider: 'aws';
publiclyAccessible: boolean;
clusterSize: 'small' | 'medium' | 'large';
dryRun?: boolean;
}
interface GetInstanceParams {
orgId: string;
instanceId: string;
}
interface StackResource {
resourceType: string;
resourceStatus: string;
resourceStatusReason?: string;
}
interface StackEvent {
timestamp?: Date;
logicalResourceId?: string;
resourceType?: string;
resourceStatus?: string;
resourceStatusReason?: string;
}
interface InstanceDetails {
instance: Awaited<ReturnType<TDedicatedInstanceDALFactory["findById"]>>;
stackStatus?: string;
stackStatusReason?: string;
resources?: StackResource[];
events?: StackEvent[];
}
export type TDedicatedInstanceServiceFactory = ReturnType<typeof dedicatedInstanceServiceFactory>;
const CLUSTER_SIZES = {
small: {
containerCpu: 1024, // 1 vCPU
containerMemory: 2048, // 2GB
rdsInstanceType: 'db.t3.small',
elasticCacheType: 'cache.t3.micro',
desiredContainerCount: 1,
displayName: '1 vCPU, 2GB RAM'
},
medium: {
containerCpu: 2048, // 2 vCPU
containerMemory: 4096, // 4GB
rdsInstanceType: 'db.t3.medium',
elasticCacheType: 'cache.t3.small',
desiredContainerCount: 2,
displayName: '2 vCPU, 4GB RAM'
},
large: {
containerCpu: 4096, // 4 vCPU
containerMemory: 8192, // 8GB
rdsInstanceType: 'db.t3.large',
elasticCacheType: 'cache.t3.medium',
desiredContainerCount: 4,
displayName: '4 vCPU, 8GB RAM'
}
};
export const dedicatedInstanceServiceFactory = ({
dedicatedInstanceDAL,
permissionService
}: TDedicatedInstanceServiceFactoryDep) => {
const listInstances = async ({
orgId
}: {
orgId: string;
}) => {
const instances = await dedicatedInstanceDAL.findInstancesByOrgId(orgId);
return instances;
};
const createInstance = async (params: CreateInstanceParams) => {
const { orgId, instanceName, subdomain, region, publiclyAccessible, dryRun = false, clusterSize = 'small' } = params;
if (params.provider !== 'aws') {
throw new BadRequestError({ message: 'Only AWS provider is supported' });
}
const clusterConfig = CLUSTER_SIZES[clusterSize];
// Configure AWS SDK with environment variables
const awsConfig = {
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
},
region: region,
};
if (!awsConfig.credentials.accessKeyId || !awsConfig.credentials.secretAccessKey) {
throw new Error('AWS credentials not found in environment variables');
}
const internalTags = {
'managed-by': 'infisical',
'organization-id': orgId,
'instance-name': instanceName
};
// Create the instance record with expanded configuration
const instance = await dedicatedInstanceDAL.create({
orgId,
instanceName,
subdomain,
status: "PROVISIONING",
region,
rdsInstanceType: clusterConfig.rdsInstanceType,
elasticCacheType: clusterConfig.elasticCacheType,
elasticContainerMemory: clusterConfig.containerMemory,
elasticContainerCpu: clusterConfig.containerCpu,
publiclyAccessible,
tags: internalTags,
version: "1.0.0",
multiAz: true,
rdsAllocatedStorage: 50,
rdsBackupRetentionDays: 7,
redisNumCacheNodes: 1,
desiredContainerCount: clusterConfig.desiredContainerCount,
subnetIds: [],
securityGroupIds: []
});
// Generate unique names for resources
const stackName = `infisical-dedicated-${instance.id}`;
const dbPassword = randomBytes(32).toString('hex');
// Create CDK app and stack
const app = new cdk.App();
const stack = new cdk.Stack(app, stackName, {
env: { region },
tags: internalTags,
synthesizer: new cdk.DefaultStackSynthesizer({
generateBootstrapVersionRule: false,
})
});
// Create VPC
const vpc = new ec2.Vpc(stack, `${orgId}-${instanceName}-vpc`, {
maxAzs: 2,
natGateways: 1,
subnetConfiguration: [
{
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
cidrMask: 24,
},
{
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
cidrMask: 24,
},
],
});
// Create RDS instance
const dbSecurityGroup = new ec2.SecurityGroup(stack, `${orgId}-${instanceName}-db-sg`, {
vpc,
description: `Security group for ${instanceName} RDS instance`,
});
const db = new rds.DatabaseInstance(stack, `${orgId}-${instanceName}-db`, {
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_14,
}),
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
securityGroups: [dbSecurityGroup],
credentials: rds.Credentials.fromPassword(
'postgres',
cdk.SecretValue.unsafePlainText(dbPassword)
),
multiAz: true,
allocatedStorage: 50,
backupRetention: cdk.Duration.days(7),
});
// Create Redis cluster
const redisSecurityGroup = new ec2.SecurityGroup(stack, `${orgId}-${instanceName}-redis-sg`, {
vpc,
description: `Security group for ${instanceName} Redis cluster`,
});
const redisSubnetGroup = new elasticache.CfnSubnetGroup(stack, `${orgId}-${instanceName}-redis-subnet`, {
subnetIds: vpc.privateSubnets.map((subnet: ec2.ISubnet) => subnet.subnetId),
description: `Subnet group for ${instanceName} Redis cluster`,
});
const redis = new elasticache.CfnCacheCluster(stack, `${orgId}-${instanceName}-redis`, {
engine: 'redis',
cacheNodeType: 'cache.t3.micro',
numCacheNodes: 1,
vpcSecurityGroupIds: [redisSecurityGroup.securityGroupId],
cacheSubnetGroupName: redisSubnetGroup.ref,
});
// Create ECS Fargate cluster and service
const cluster = new ecs.Cluster(stack, `${orgId}-${instanceName}-cluster`, { vpc });
// Create task execution role with permissions to read from Parameter Store
const executionRole = new iam.Role(stack, `${orgId}-${instanceName}-execution-role`, {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
});
executionRole.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy')
);
// Create ECS task definition and service
const taskDefinition = new ecs.FargateTaskDefinition(stack, `${orgId}-${instanceName}-task-def`, {
memoryLimitMiB: clusterConfig.containerMemory,
cpu: clusterConfig.containerCpu,
executionRole,
});
taskDefinition.addContainer('infisical', {
image: ecs.ContainerImage.fromRegistry('infisical/infisical:latest-postgres'),
environment: {
NODE_ENV: 'production',
ENCRYPTION_KEY: randomBytes(16).toString('hex'),
AUTH_SECRET: randomBytes(32).toString('base64'),
DB_CONNECTION_URI: `postgresql://postgres:${dbPassword}@${db.instanceEndpoint.hostname}:5432/postgres?sslmode=no-verify`,
REDIS_URL: `redis://${redis.attrRedisEndpointAddress}:${redis.attrRedisEndpointPort}`,
},
logging: ecs.LogDrivers.awsLogs({ streamPrefix: stackName }),
});
const service = new ecs.FargateService(stack, `${orgId}-${instanceName}-service`, {
cluster,
taskDefinition,
desiredCount: clusterConfig.desiredContainerCount,
assignPublicIp: publiclyAccessible,
vpcSubnets: {
subnetType: publiclyAccessible ? ec2.SubnetType.PUBLIC : ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
});
// Create security group for ECS service
const ecsSecurityGroup = new ec2.SecurityGroup(stack, `${orgId}-${instanceName}-ecs-sg`, {
vpc,
description: `Security group for ${instanceName} ECS service`,
allowAllOutbound: true,
});
// Update service to use the security group
service.connections.addSecurityGroup(ecsSecurityGroup);
// Configure security group rules for RDS access
dbSecurityGroup.addIngressRule(
ecsSecurityGroup,
ec2.Port.tcp(5432),
'Allow ECS tasks to connect to PostgreSQL'
);
// Configure security group rules for Redis access
redisSecurityGroup.addIngressRule(
ecsSecurityGroup,
ec2.Port.tcp(6379),
'Allow ECS tasks to connect to Redis'
);
// Add outputs for resource IDs
new cdk.CfnOutput(stack, 'vpcid', { value: vpc.vpcId });
new cdk.CfnOutput(stack, 'publicsubnet1id', { value: vpc.publicSubnets[0].subnetId });
new cdk.CfnOutput(stack, 'publicsubnet2id', { value: vpc.publicSubnets[1].subnetId });
new cdk.CfnOutput(stack, 'privatesubnet1id', { value: vpc.privateSubnets[0].subnetId });
new cdk.CfnOutput(stack, 'privatesubnet2id', { value: vpc.privateSubnets[1].subnetId });
new cdk.CfnOutput(stack, 'rdsinstanceid', { value: db.instanceIdentifier });
new cdk.CfnOutput(stack, 'redisclusterid', { value: redis.ref });
new cdk.CfnOutput(stack, 'ecsclusterarn', { value: cluster.clusterArn });
new cdk.CfnOutput(stack, 'ecsservicearn', { value: service.serviceArn });
new cdk.CfnOutput(stack, 'dbsecuritygroupid', { value: dbSecurityGroup.securityGroupId });
new cdk.CfnOutput(stack, 'redissecuritygroupid', { value: redisSecurityGroup.securityGroupId });
// After VPC creation, store subnet IDs
const subnetIds = [
...vpc.publicSubnets.map(subnet => subnet.subnetId),
...vpc.privateSubnets.map(subnet => subnet.subnetId)
];
// After security group creation, store security group IDs
const securityGroupIds = [
dbSecurityGroup.securityGroupId,
redisSecurityGroup.securityGroupId
];
// Update instance with all infrastructure details
await dedicatedInstanceDAL.updateById(instance.id, {
stackName,
status: "PROVISIONING",
// Remove the token values and update them after stack creation
rdsInstanceId: null,
redisClusterId: null,
ecsClusterArn: null,
ecsServiceArn: null,
vpcId: null,
subnetIds: null,
securityGroupIds: null
});
// Deploy the stack
const deployment = app.synth();
if (dryRun) {
console.log('Dry run - would create stack with template:', JSON.stringify(deployment.getStackArtifact(stackName).template, null, 2));
return instance;
}
// Deploy the CloudFormation stack
try {
const cfnClient = new CloudFormationClient(awsConfig);
const command = new CreateStackCommand({
StackName: stackName,
TemplateBody: JSON.stringify(deployment.getStackArtifact(stackName).template),
Capabilities: ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"],
Tags: Object.entries(internalTags).map(([Key, Value]) => ({ Key, Value }))
});
await cfnClient.send(command);
} catch (error) {
await dedicatedInstanceDAL.updateById(instance.id, {
status: "FAILED",
error: error instanceof Error ? error.message : 'Unknown error occurred'
});
throw error;
}
return instance;
};
const getInstance = async ({ orgId, instanceId }: GetInstanceParams): Promise<InstanceDetails> => {
const instance = await dedicatedInstanceDAL.findById(instanceId);
if (!instance) {
throw new NotFoundError({ message: "Instance not found" });
}
if (instance.orgId !== orgId) {
throw new BadRequestError({ message: "Not authorized to access this instance" });
}
// Get CloudFormation stack status
try {
const awsConfig = {
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
},
region: instance.region,
};
if (!awsConfig.credentials.accessKeyId || !awsConfig.credentials.secretAccessKey) {
throw new Error('AWS credentials not found in environment variables');
}
const cfnClient = new CloudFormationClient(awsConfig);
// Get stack status
const stackResponse = await cfnClient.send(new DescribeStacksCommand({
StackName: instance.stackName
}));
const stack = stackResponse.Stacks?.[0];
if (!stack) {
return { instance };
}
// Get stack events for progress tracking
const eventsResponse = await cfnClient.send(new DescribeStackEventsCommand({
StackName: instance.stackName
}));
const events = eventsResponse.StackEvents?.map(event => ({
timestamp: event.Timestamp,
logicalResourceId: event.LogicalResourceId,
resourceType: event.ResourceType,
resourceStatus: event.ResourceStatus,
resourceStatusReason: event.ResourceStatusReason
})).sort((a, b) => (b.timestamp?.getTime() || 0) - (a.timestamp?.getTime() || 0)) || [];
const stackStatus = stack.StackStatus || '';
let updates: Record<string, any> = {};
// Process outputs when stack is complete
if (stackStatus === 'CREATE_COMPLETE') {
const outputs = stack.Outputs || [];
const getOutput = (key: string) => outputs.find(o => o.OutputKey?.toLowerCase() === key.toLowerCase())?.OutputValue;
updates = {
status: 'RUNNING',
vpcId: getOutput('vpcid'),
subnetIds: [
getOutput('publicsubnet1id'),
getOutput('publicsubnet2id'),
getOutput('privatesubnet1id'),
getOutput('privatesubnet2id')
].filter(Boolean) as string[],
rdsInstanceId: getOutput('rdsinstanceid'),
redisClusterId: getOutput('redisclusterid'),
ecsClusterArn: getOutput('ecsclusterarn'),
ecsServiceArn: getOutput('ecsservicearn'),
securityGroupIds: [
getOutput('dbsecuritygroupid'),
getOutput('redissecuritygroupid')
].filter(Boolean) as string[]
};
} else if (stackStatus.includes('FAILED')) {
updates = {
status: 'FAILED',
error: stack.StackStatusReason
};
}
// Update instance if we have changes
if (Object.keys(updates).length > 0) {
await dedicatedInstanceDAL.updateById(instance.id, updates);
}
return {
instance: {
...instance,
...updates
},
stackStatus: stack.StackStatus,
stackStatusReason: stack.StackStatusReason,
events
};
} catch (error) {
// Log the error but don't throw - we still want to return the instance details
console.error('Error fetching CloudFormation stack status:', error);
}
return { instance };
};
return {
listInstances,
createInstance,
getInstance
};
};

View File

@ -12,7 +12,8 @@ export enum OrgPermissionActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete"
Delete = "delete",
Update = "update"
}
export enum OrgPermissionAppConnectionActions {
@ -50,7 +51,8 @@ export enum OrgPermissionSubjects {
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates",
AppConnections = "app-connections",
Kmip = "kmip"
Kmip = "kmip",
DedicatedInstance = "dedicatedInstance"
}
export type AppConnectionSubjectFields = {
@ -81,7 +83,8 @@ export type OrgPermissionSet =
)
]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip];
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
| [OrgPermissionActions, OrgPermissionSubjects.DedicatedInstance];
const AppConnectionConditionSchema = z
.object({
@ -180,6 +183,10 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(OrgPermissionSubjects.DedicatedInstance).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
})
]);
@ -271,6 +278,11 @@ const buildAdminPermission = () => {
// the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI
can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
can(OrgPermissionActions.Create, OrgPermissionSubjects.DedicatedInstance);
can(OrgPermissionActions.Read, OrgPermissionSubjects.DedicatedInstance);
can(OrgPermissionActions.Update, OrgPermissionSubjects.DedicatedInstance);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.DedicatedInstance);
return rules;
};
@ -301,6 +313,8 @@ const buildMemberPermission = () => {
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
can(OrgPermissionActions.Read, OrgPermissionSubjects.DedicatedInstance);
return rules;
};

View File

@ -232,6 +232,8 @@ import { webhookDALFactory } from "@app/services/webhook/webhook-dal";
import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
import { workflowIntegrationDALFactory } from "@app/services/workflow-integration/workflow-integration-dal";
import { workflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
import { dedicatedInstanceDALFactory } from "@app/ee/services/dedicated-instance/dedicated-instance-dal";
import { dedicatedInstanceServiceFactory } from "@app/ee/services/dedicated-instance/dedicated-instance-service";
import { injectAuditLogInfo } from "../plugins/audit-log";
import { injectIdentity } from "../plugins/auth/inject-identity";
@ -1457,6 +1459,12 @@ export const registerRoutes = async (
permissionService
});
const dedicatedInstanceDAL = dedicatedInstanceDALFactory(db);
const dedicatedInstanceService = dedicatedInstanceServiceFactory({
dedicatedInstanceDAL,
permissionService
});
await superAdminService.initServerCfg();
// setup the communication with license key server
@ -1557,7 +1565,8 @@ export const registerRoutes = async (
appConnection: appConnectionService,
secretSync: secretSyncService,
kmip: kmipService,
kmipOperation: kmipOperationService
kmipOperation: kmipOperationService,
dedicatedInstance: dedicatedInstanceService
});
const cronJobs: CronJob[] = [];

View File

@ -74,6 +74,13 @@ export const OrganizationLayout = () => {
</MenuItem>
)}
</Link>
<Link to="/organization/dedicated-instances">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="moving-block">
Dedicated Instances
</MenuItem>
)}
</Link>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://eu.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && (

View File

@ -13,7 +13,8 @@ import {
faPlug,
faSignOut,
faUser,
faUsers
faUsers,
faServer
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
@ -332,6 +333,11 @@ export const MinimizedOrgSidebar = () => {
App Connections
</DropdownMenuItem>
</Link>
<Link to="/organization/dedicated-instances">
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faServer} />}>
Dedicated Instances
</DropdownMenuItem>
</Link>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://eu.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && (

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstanceDetailsPage } from '@app/pages/organization/DedicatedInstancesPage';
export const Route = createFileRoute('/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId')({
component: DedicatedInstanceDetailsPage,
});

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstancesPage } from '@app/pages/organization/DedicatedInstancesPage';
export const Route = createFileRoute('/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances')({
component: DedicatedInstancesPage,
});

View File

@ -0,0 +1,371 @@
import { useState } from "react";
import {
Button,
Card,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem,
Table,
TableContainer,
TBody,
Th,
THead,
Tr,
Td,
IconButton,
EmptyState,
Badge,
Tooltip,
Spinner
} from "@app/components/v2";
import {
faServer,
faBookOpen,
faArrowUpRightFromSquare,
faHome,
faTerminal,
faKey,
faLock,
faRotateRight,
faExclamationTriangle,
faChevronRight
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useParams } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { useOrganization } from "@app/context/OrganizationContext";
import { apiRequest } from "@app/config/request";
interface DedicatedInstanceDetails {
id: string;
orgId: string;
instanceName: string;
subdomain: string;
status: "RUNNING" | "UPGRADING" | "PROVISIONING" | "FAILED";
version: string;
versionUpgrades: "Automatic" | "Manual";
clusterId: string;
clusterTier: "Development" | "Production";
clusterSize: string;
highAvailability: boolean;
createdAt: string;
region: string;
provider: string;
publicAccess: boolean;
ipAllowlist: "Enabled" | "Disabled";
}
const fetchInstanceDetails = async (organizationId: string, instanceId: string) => {
const { data } = await apiRequest.get<DedicatedInstanceDetails>(
`/api/v1/organizations/${organizationId}/dedicated-instances/${instanceId}`
);
return data;
};
export const DedicatedInstanceDetailsPage = () => {
const { currentOrg } = useOrganization();
const { instanceId } = useParams({
from: "/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId"
});
const [isAPILockModalOpen, setIsAPILockModalOpen] = useState(false);
const [isRevokeTokensModalOpen, setIsRevokeTokensModalOpen] = useState(false);
const { data: instance, isLoading, error } = useQuery({
queryKey: ["dedicatedInstance", instanceId],
queryFn: () => fetchInstanceDetails(currentOrg?.id || "", instanceId || ""),
enabled: Boolean(currentOrg?.id && instanceId)
});
if (isLoading) {
return (
<div className="flex h-full items-center justify-center">
<Spinner size="lg" />
</div>
);
}
if (error || !instance) {
return (
<div className="flex h-full items-center justify-center">
<EmptyState
title="Error loading instance details"
icon={faServer}
>
<p className="text-sm text-bunker-400">
{error instanceof Error ? error.message : "Failed to load instance details"}
</p>
</EmptyState>
</div>
);
}
return (
<div className="flex h-full flex-col space-y-4 p-4">
<div className="mb-6">
<div className="flex items-center gap-2 text-sm text-mineshaft-300">
<Link to="/" className="flex items-center gap-1 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faHome} />
<span>Home</span>
</Link>
<FontAwesomeIcon icon={faChevronRight} className="text-xs" />
<Link to="/organization/dedicated-instances" className="hover:text-mineshaft-200">
Dedicated Instances
</Link>
<FontAwesomeIcon icon={faChevronRight} className="text-xs" />
<span className="text-mineshaft-200">{instance.instanceName}</span>
</div>
<div className="mt-6 flex items-center justify-between">
<div>
<h1 className="text-3xl font-semibold text-mineshaft-100">
{instance.instanceName} <span className="text-mineshaft-300">({instance.region})</span>
</h1>
</div>
<div>
<Button
variant="solid"
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faArrowUpRightFromSquare} />}
onClick={() => window.open(`https://${instance.subdomain}.infisical.com`, "_blank")}
>
Launch Web UI
</Button>
</div>
</div>
{instance.publicAccess && (
<div className="mt-4 flex items-center gap-2 rounded-lg bg-yellow-500/10 px-4 py-3 text-sm text-yellow-500">
<FontAwesomeIcon icon={faExclamationTriangle} />
<p>
This cluster's network configuration is set to public. Configure an IP Allowlist in the cluster's networking
settings to limit network access to the cluster's public endpoint.
</p>
</div>
)}
</div>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="grid grid-cols-2 gap-4">
<Card className="col-span-1 rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-mineshaft-100">Cluster Details</h2>
<div className="space-y-4">
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Status</span>
<Badge variant={instance.status === "RUNNING" ? "success" : "primary"}>
{instance.status}
</Badge>
</div>
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Vault Version</span>
<span className="text-sm font-medium text-mineshaft-100">{instance.version}</span>
</div>
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Version upgrades</span>
<span className="text-sm font-medium text-mineshaft-100">{instance.versionUpgrades}</span>
</div>
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Cluster Size</span>
<span className="text-sm font-medium text-mineshaft-100">{instance.clusterSize}</span>
</div>
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">High Availability</span>
<span className="text-sm font-medium text-mineshaft-100">{instance.highAvailability ? "Yes" : "No"}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-mineshaft-300">Created</span>
<span className="text-sm font-medium text-mineshaft-100">
{new Date(instance.createdAt).toLocaleDateString()}
</span>
</div>
</div>
</div>
</Card>
<div className="col-span-1 space-y-4">
<Card className="rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-mineshaft-100">Quick actions</h2>
<div className="grid grid-cols-2 gap-4">
<Card className="bg-mineshaft-700 rounded-lg">
<div className="p-4">
<h3 className="mb-2 text-sm font-medium text-mineshaft-100">How to access via</h3>
<div className="flex gap-4">
<Button
variant="outline"
colorSchema="secondary"
size="sm"
leftIcon={<FontAwesomeIcon icon={faTerminal} />}
>
Command-line (CLI)
</Button>
<Button
variant="outline"
colorSchema="secondary"
size="sm"
leftIcon={<FontAwesomeIcon icon={faKey} />}
>
API
</Button>
</div>
</div>
</Card>
<Card className="bg-mineshaft-700 rounded-lg">
<div className="p-4">
<h3 className="mb-2 text-sm font-medium text-mineshaft-100">New root token</h3>
<Tooltip content="Generate a root token">
<Button
variant="outline"
colorSchema="secondary"
size="sm"
leftIcon={<FontAwesomeIcon icon={faKey} />}
>
Generate token
</Button>
</Tooltip>
</div>
</Card>
</div>
</div>
</Card>
<Card className="rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-mineshaft-100">Cluster URLs</h2>
<p className="mb-4 text-sm text-mineshaft-300">Copy the address into your CLI or browser to access the cluster.</p>
<div className="space-y-2">
<div className="flex items-center justify-between rounded-lg bg-mineshaft-700 p-3">
<div className="flex items-center gap-2">
<Badge variant="primary">Private</Badge>
<code className="text-sm text-mineshaft-300">https://{instance.subdomain}.infisical.com</code>
</div>
<Button variant="outline" colorSchema="secondary" size="sm">
Copy
</Button>
</div>
<div className="flex items-center justify-between rounded-lg bg-mineshaft-700 p-3">
<div className="flex items-center gap-2">
<Badge variant="primary">Public</Badge>
<code className="text-sm text-mineshaft-300">https://{instance.subdomain}.infisical.com</code>
</div>
<Button variant="outline" colorSchema="secondary" size="sm">
Copy
</Button>
</div>
</div>
</div>
</Card>
<Card className="rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-mineshaft-100">Cluster networking</h2>
<div className="space-y-4">
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Provider/region</span>
<span className="text-sm font-medium text-mineshaft-100">
{instance.provider} ({instance.region})
</span>
</div>
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Cluster accessibility</span>
<Badge variant={instance.publicAccess ? "danger" : "success"}>
{instance.publicAccess ? "Public" : "Private"}
</Badge>
</div>
<div className="flex justify-between">
<span className="text-sm text-mineshaft-300">IP allowlist</span>
<span className="text-sm font-medium text-mineshaft-100">{instance.ipAllowlist}</span>
</div>
</div>
</div>
</Card>
<Card className="rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-mineshaft-100">In case of emergency</h2>
<div className="space-y-4">
<Button
variant="outline"
colorSchema="danger"
className="w-full"
leftIcon={<FontAwesomeIcon icon={faLock} />}
onClick={() => setIsAPILockModalOpen(true)}
>
API Lock
</Button>
<Button
variant="outline"
colorSchema="danger"
className="w-full"
leftIcon={<FontAwesomeIcon icon={faRotateRight} />}
onClick={() => setIsRevokeTokensModalOpen(true)}
>
Revoke all admin tokens
</Button>
</div>
</div>
</Card>
</div>
</div>
</div>
<Modal isOpen={isAPILockModalOpen} onOpenChange={setIsAPILockModalOpen}>
<ModalContent
title="API Lock"
subTitle="Are you sure you want to lock the API? This will prevent all API access until unlocked."
>
<div className="mt-8 flex justify-end space-x-4">
<Button
variant="plain"
colorSchema="secondary"
onClick={() => setIsAPILockModalOpen(false)}
>
Cancel
</Button>
<Button
variant="solid"
colorSchema="danger"
onClick={() => {
// Handle API lock
setIsAPILockModalOpen(false);
}}
>
Lock API
</Button>
</div>
</ModalContent>
</Modal>
<Modal isOpen={isRevokeTokensModalOpen} onOpenChange={setIsRevokeTokensModalOpen}>
<ModalContent
title="Revoke Admin Tokens"
subTitle="Are you sure you want to revoke all admin tokens? This action cannot be undone."
>
<div className="mt-8 flex justify-end space-x-4">
<Button
variant="plain"
colorSchema="secondary"
onClick={() => setIsRevokeTokensModalOpen(false)}
>
Cancel
</Button>
<Button
variant="solid"
colorSchema="danger"
onClick={() => {
// Handle token revocation
setIsRevokeTokensModalOpen(false);
}}
>
Revoke Tokens
</Button>
</div>
</ModalContent>
</Modal>
</div>
);
};

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstanceDetailsPage } from '../DedicatedInstanceDetailsPage';
export const Route = createFileRoute('/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId')({
component: DedicatedInstanceDetailsPage
});

View File

@ -0,0 +1,602 @@
import { useState } from "react";
import {
Button,
Card,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem,
DropdownMenu,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuContent,
Table,
TableContainer,
TBody,
Th,
THead,
Tr,
Td,
IconButton,
EmptyState,
FormHelperText,
Spinner
} from "@app/components/v2";
import {
faPlus,
faSearch,
faServer,
faEllipsisVertical,
faBookOpen,
faArrowUpRightFromSquare,
faArrowDown,
faArrowUp,
faMagnifyingGlass,
faHome,
faSpinner
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Link, useNavigate } from "@tanstack/react-router";
import { useQuery, useMutation, useQueryClient, useQueries } from "@tanstack/react-query";
import { useOrganization } from "@app/context/OrganizationContext";
import { apiRequest } from "@app/config/request";
interface DedicatedInstance {
id: string;
orgId: string;
instanceName: string;
subdomain: string;
status: "RUNNING" | "UPGRADING" | "PROVISIONING" | "FAILED";
stackStatus?: string;
stackStatusReason?: string;
events?: Array<{
timestamp?: Date;
logicalResourceId?: string;
resourceType?: string;
resourceStatus?: string;
resourceStatusReason?: string;
}>;
rdsInstanceType: string;
elasticCacheType: string;
elasticContainerMemory: number;
elasticContainerCpu: number;
region: string;
version: string;
backupRetentionDays: number;
lastBackupTime: string | null;
lastUpgradeTime: string | null;
publiclyAccessible: boolean;
vpcId: string | null;
subnetIds: string[] | null;
tags: Record<string, string> | null;
createdAt: string;
updatedAt: string;
}
const INSTANCE_SIZES = [
{ value: "small", label: "Small (2 vCPU, 8GB RAM)" },
{ value: "medium", label: "Medium (4 vCPU, 16GB RAM)" },
{ value: "large", label: "Large (8 vCPU, 32GB RAM)" }
];
type CloudProvider = "aws" | "gcp" | "azure";
interface CreateInstancePayload {
instanceName: string;
subdomain: string;
region: string;
provider: "aws";
publiclyAccessible: boolean;
clusterSize: "small" | "medium" | "large";
}
const INITIAL_INSTANCE_STATE: CreateInstancePayload = {
instanceName: "",
subdomain: "",
region: "",
provider: "aws",
publiclyAccessible: false,
clusterSize: "small"
};
const PROVIDERS: Array<{
value: CloudProvider;
label: string;
image: string;
description: string;
}> = [
{
value: "aws",
label: "Amazon Web Services",
image: "/images/integrations/Amazon Web Services.png",
description: "Deploy your instance on AWS infrastructure"
},
{
value: "gcp",
label: "Google Cloud Platform",
image: "/images/integrations/Google Cloud Platform.png",
description: "Deploy your instance on Google Cloud infrastructure"
},
{
value: "azure",
label: "Microsoft Azure",
image: "/images/integrations/Microsoft Azure.png",
description: "Deploy your instance on Azure infrastructure"
}
];
const REGIONS = {
aws: [
{ value: "us-east-1", label: "US East (N. Virginia)" },
{ value: "us-west-2", label: "US West (Oregon)" },
{ value: "eu-west-1", label: "EU West (Ireland)" },
{ value: "ap-southeast-1", label: "Asia Pacific (Singapore)" }
],
gcp: [
{ value: "us-east1", label: "US East (South Carolina)" },
{ value: "us-west1", label: "US West (Oregon)" },
{ value: "europe-west1", label: "Europe West (Belgium)" },
{ value: "asia-southeast1", label: "Asia Southeast (Singapore)" }
],
azure: [
{ value: "eastus", label: "East US (Virginia)" },
{ value: "westus", label: "West US (California)" },
{ value: "westeurope", label: "West Europe (Netherlands)" },
{ value: "southeastasia", label: "Southeast Asia (Singapore)" }
]
};
interface RouteParams {
organizationId: string;
}
const dedicatedInstanceKeys = {
getInstances: (organizationId: string) => ["dedicatedInstances", { organizationId }] as const,
getInstance: (organizationId: string, instanceId: string) => ["dedicatedInstance", { organizationId, instanceId }] as const
};
const getDeploymentStage = (events?: DedicatedInstance['events']) => {
if (!events?.length) return 'Deploying';
return 'Deploying';
};
const getDeploymentProgress = (instance: DedicatedInstance) => {
if (instance.status === 'RUNNING') return { stage: 'Complete', progress: 100 };
if (instance.status === 'FAILED') return { stage: 'Failed', progress: 0 };
const events = instance.events || [];
if (events.length === 0) return { stage: 'Deploying', progress: 0 };
// Count completed events vs total events
const completedEvents = events.filter(event =>
event.resourceStatus?.includes('COMPLETE') ||
event.resourceStatus?.includes('CREATE_COMPLETE')
).length;
const progress = Math.round((completedEvents / events.length) * 100);
return { stage: 'Deploying', progress };
};
const fetchInstances = async (organizationId: string) => {
const { data } = await apiRequest.get<{ instances: DedicatedInstance[] }>(
`/api/v1/organizations/${organizationId}/dedicated-instances`
);
return data;
};
const fetchInstanceDetails = async (organizationId: string, instanceId: string) => {
const { data } = await apiRequest.get<DedicatedInstance>(
`/api/v1/organizations/${organizationId}/dedicated-instances/${instanceId}`
);
return data;
};
const createInstance = async (organizationId: string, data: CreateInstancePayload) => {
const { data: response } = await apiRequest.post<DedicatedInstance>(
`/api/v1/organizations/${organizationId}/dedicated-instances`,
data
);
return response;
};
export const DedicatedInstancesPage = () => {
const { currentOrg } = useOrganization();
const navigate = useNavigate();
const organizationId = currentOrg?.id || "";
const queryClient = useQueryClient();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [newInstance, setNewInstance] = useState(INITIAL_INSTANCE_STATE);
type InstancesResponse = { instances: DedicatedInstance[] };
// Fetch all instances
const { data: instancesData, isLoading, error } = useQuery<InstancesResponse>({
queryKey: dedicatedInstanceKeys.getInstances(organizationId),
queryFn: () => fetchInstances(organizationId),
enabled: Boolean(organizationId),
});
// Fetch details for provisioning instances
const provisioningInstances = instancesData?.instances.filter(
instance => instance.status === "PROVISIONING"
) || [];
const instanceDetailsQueries = useQueries({
queries: provisioningInstances.map(instance => ({
queryKey: dedicatedInstanceKeys.getInstance(organizationId, instance.id),
queryFn: () => fetchInstanceDetails(organizationId, instance.id),
refetchInterval: 2000, // Poll every 2 seconds
}))
});
// Merge instance details with the main instances list
const instances = instancesData?.instances.map(instance => {
if (instance.status === "PROVISIONING") {
const detailsQuery = instanceDetailsQueries.find(
q => q.data?.id === instance.id
);
return detailsQuery?.data || instance;
}
return instance;
}) || [];
// Create instance mutation
const createInstanceMutation = useMutation({
mutationFn: (data: CreateInstancePayload) => createInstance(organizationId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: dedicatedInstanceKeys.getInstances(organizationId) });
setIsCreateModalOpen(false);
setNewInstance(INITIAL_INSTANCE_STATE);
}
});
const handleCreateInstance = () => {
createInstanceMutation.mutate(newInstance);
};
const handleModalOpenChange = (open: boolean) => {
setIsCreateModalOpen(open);
if (!open) {
setNewInstance(INITIAL_INSTANCE_STATE);
}
};
// Get available regions based on selected provider
const availableRegions = newInstance.provider ? REGIONS[newInstance.provider] : [];
// Filter instances based on search term
const filteredInstances = instances.filter((instance: DedicatedInstance) =>
instance.instanceName.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="flex h-full flex-col space-y-4 p-4">
<div className="mb-6">
<div className="flex items-center gap-2 text-sm text-mineshaft-300">
<Link to="/" className="flex items-center gap-1 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faHome} />
<span>Home</span>
</Link>
<span>/</span>
<span className="text-mineshaft-200">Dedicated Instances</span>
</div>
<div className="mt-4 flex items-center gap-3">
<h1 className="text-3xl font-semibold text-mineshaft-100">Dedicated Instances</h1>
<a
href="https://infisical.com/docs/dedicated-instances/overview"
target="_blank"
rel="noopener noreferrer"
className="mt-1.5 inline-flex items-center gap-1.5 rounded-md bg-yellow/20 px-2 py-1 text-sm text-yellow opacity-80 hover:opacity-100"
>
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="text-[10px]" />
</a>
</div>
<p className="mt-2 text-base text-mineshaft-300">
Create and manage dedicated Infisical instances across different regions
</p>
</div>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-end mb-4">
<Button
onClick={() => setIsCreateModalOpen(true)}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Create Instance
</Button>
</div>
<div>
<div className="flex gap-2">
<Input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search instances..."
className="flex-1"
/>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-1/4">Name</Th>
<Th className="w-1/4">Type</Th>
<Th className="w-1/4">Region</Th>
<Th className="w-1/4">Status</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading ? (
<tr>
<td colSpan={5}>
<div className="flex items-center justify-center py-8">
<Spinner size="md" className="text-mineshaft-300" />
</div>
</td>
</tr>
) : error ? (
<tr>
<td colSpan={5}>
<EmptyState
title="Error loading instances"
icon={faServer}
>
<p className="text-sm text-bunker-400">
{error instanceof Error ? error.message : "An error occurred while loading instances"}
</p>
</EmptyState>
</td>
</tr>
) : filteredInstances.length === 0 ? (
<tr>
<td colSpan={5}>
<EmptyState
title={searchTerm ? "No Instances match search..." : "No Instances Found"}
icon={faServer}
>
<p className="text-sm text-bunker-400">
You don't have any dedicated instances yet. Create a new one to get started.
</p>
</EmptyState>
</td>
</tr>
) : (
filteredInstances.map((instance: DedicatedInstance) => (
<Tr
key={instance.id}
className={twMerge("group h-12 transition-colors duration-100 hover:bg-mineshaft-700 cursor-pointer")}
onClick={() => {
navigate({
to: "/organization/dedicated-instances/$instanceId",
params: {
instanceId: instance.id
}
});
}}
>
<Td>
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-bunker-700/50">
<FontAwesomeIcon icon={faServer} className="text-bunker-300" />
</div>
<div>
<p className="text-sm font-medium text-bunker-100">{instance.instanceName}</p>
<p className="text-xs text-bunker-300">Created {new Date(instance.createdAt).toLocaleDateString()}</p>
</div>
</div>
</Td>
<Td>
<p className="text-sm font-medium text-bunker-100">
{`${instance.elasticContainerCpu / 1024} vCPU, ${instance.elasticContainerMemory / 1024}GB RAM`}
</p>
<p className="text-xs text-bunker-300">Instance Type</p>
</Td>
<Td>
<p className="text-sm font-medium text-bunker-100">
{REGIONS.aws.find(r => r.value === instance.region)?.label || instance.region}
</p>
<p className="text-xs text-bunker-300">Region</p>
</Td>
<Td>
{instance.status === "PROVISIONING" ? (
<div className="space-y-1">
<div className="flex items-center">
<span className="text-sm font-medium text-yellow-500">
{getDeploymentProgress(instance).stage}
</span>
</div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-mineshaft-600">
<div
className="h-full rounded-full bg-yellow-500/50 transition-all duration-500 [animation:pulse_0.7s_cubic-bezier(0.4,0,0.6,1)_infinite]"
style={{ width: `${getDeploymentProgress(instance).progress}%` }}
/>
</div>
</div>
) : (
<div
className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
instance.status === "RUNNING"
? "bg-emerald-100/10 text-emerald-500"
: instance.status === "FAILED"
? "bg-red-100/10 text-red-500"
: "bg-yellow-100/10 text-yellow-500"
}`}
>
{instance.status.toLowerCase()}
</div>
)}
</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
onClick={(e) => e.stopPropagation()}
>
<FontAwesomeIcon icon={faEllipsisVertical} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(instance.id);
}}>
Copy Instance ID
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
// TODO: Start instance
}}>
Start Instance
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
// TODO: Stop instance
}}>
Stop Instance
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
// TODO: Delete instance
}} className="text-red-500">
Delete Instance
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
))
)}
</TBody>
</Table>
</TableContainer>
</div>
</div>
<Modal isOpen={isCreateModalOpen} onOpenChange={handleModalOpenChange}>
<ModalContent title="Create Dedicated Instance" subTitle="Configure your dedicated Infisical instance">
<div className="mt-6 space-y-6">
<div>
<div className="mb-2 text-sm font-medium text-mineshaft-200">Cloud Provider</div>
<div className="grid grid-cols-3 gap-2">
{PROVIDERS.map(({ value, label, image, description }) => (
<button
key={value}
type="button"
onClick={() => {
if (value === "aws") {
setNewInstance({ ...newInstance, provider: value, region: "" });
}
}}
className={twMerge(
"group relative flex h-24 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600",
newInstance.provider === value && "border-primary/50 bg-primary/10"
)}
>
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-mineshaft-800">
<img
src={image}
alt={`${label} logo`}
className="h-7 w-7 object-contain"
/>
</div>
<div className="mt-2 max-w-xs text-center text-sm font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{label}
</div>
</button>
))}
</div>
<div className="mt-1 text-xs text-mineshaft-400">
{PROVIDERS.find(p => p.value === newInstance.provider)?.description || "Select a cloud provider for your instance"}
</div>
</div>
<FormControl label="Instance Name" helperText="Give your instance a unique name to identify it">
<Input
value={newInstance.instanceName}
onChange={(e) => setNewInstance({ ...newInstance, instanceName: e.target.value })}
placeholder="Enter instance name"
/>
</FormControl>
<FormControl label="Subdomain" helperText="Enter the subdomain for your instance">
<div className="flex items-center">
<Input
value={newInstance.subdomain}
onChange={(e) => setNewInstance({ ...newInstance, subdomain: e.target.value })}
placeholder="your-subdomain"
className="rounded-r-none"
/>
<div className="flex h-10 items-center rounded-r-lg border border-l-0 border-mineshaft-600 bg-mineshaft-800 px-3 text-sm text-mineshaft-300">
.infisical.com
</div>
</div>
</FormControl>
<FormControl label="Region" helperText="Select the geographical location where your instance will be deployed">
<Select
value={newInstance.region}
onValueChange={(value) => setNewInstance({ ...newInstance, region: value })}
placeholder="Select region"
isDisabled={!newInstance.provider}
>
{availableRegions.map(({ value, label }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Cluster Size" helperText="Select the size of your dedicated instance">
<Select
value={newInstance.clusterSize}
onValueChange={(value) => setNewInstance({ ...newInstance, clusterSize: value as "small" | "medium" | "large" })}
placeholder="Select size"
>
<SelectItem value="small">Small (1 vCPU, 2GB RAM)</SelectItem>
<SelectItem value="medium">Medium (2 vCPU, 4GB RAM)</SelectItem>
<SelectItem value="large">Large (4 vCPU, 8GB RAM)</SelectItem>
</Select>
</FormControl>
</div>
<div className="mt-8 flex justify-end space-x-4">
<Button
variant="plain"
colorSchema="secondary"
onClick={() => setIsCreateModalOpen(false)}
size="md"
disabled={createInstanceMutation.status === "pending"}
>
Cancel
</Button>
<Button
variant="solid"
colorSchema="primary"
onClick={handleCreateInstance}
disabled={!newInstance.instanceName || !newInstance.region || !newInstance.provider || !newInstance.subdomain || createInstanceMutation.status === "pending"}
size="md"
leftIcon={createInstanceMutation.status === "pending" ? <Spinner size="xs" className="text-black" /> : undefined}
>
{createInstanceMutation.status === "pending" ? "Creating..." : "Create Instance"}
</Button>
</div>
</ModalContent>
</Modal>
</div>
);
};

View File

@ -0,0 +1,2 @@
export { DedicatedInstancesPage } from './DedicatedInstancesPage';
export { DedicatedInstanceDetailsPage } from './DedicatedInstanceDetailsPage';

View File

@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
import { DedicatedInstancesPage } from './DedicatedInstancesPage'
export const Route = createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/',
)({
component: DedicatedInstancesPage,
})

View File

@ -59,7 +59,9 @@ import { Route as organizationUserDetailsByIDPageRouteImport } from './pages/org
import { Route as organizationKmsOverviewPageRouteImport } from './pages/organization/KmsOverviewPage/route'
import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages/organization/IdentityDetailsByIDPage/route'
import { Route as organizationGroupDetailsByIDPageRouteImport } from './pages/organization/GroupDetailsByIDPage/route'
import { Route as organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteImport } from './pages/organization/DedicatedInstancesPage/DedicatedInstanceDetailsPage/route'
import { Route as organizationCertManagerOverviewPageRouteImport } from './pages/organization/CertManagerOverviewPage/route'
import { Route as organizationDedicatedInstancesPageRouteImport } from './pages/organization/DedicatedInstancesPage/route'
import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route'
import { Route as projectAccessControlPageRouteSshImport } from './pages/project/AccessControlPage/route-ssh'
import { Route as projectAccessControlPageRouteSecretManagerImport } from './pages/project/AccessControlPage/route-secret-manager'
@ -209,6 +211,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId',
)()
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances',
)()
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections',
@ -454,6 +460,16 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRoute =
getParentRoute: () => organizationLayoutRoute,
} as any)
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesImport.update(
{
id: '/dedicated-instances',
path: '/dedicated-instances',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any,
)
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport.update({
id: '/app-connections',
@ -617,6 +633,16 @@ const organizationGroupDetailsByIDPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute =
organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteImport.update(
{
id: '/$instanceId',
path: '/$instanceId',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute,
} as any,
)
const organizationCertManagerOverviewPageRouteRoute =
organizationCertManagerOverviewPageRouteImport.update({
id: '/cert-manager/overview',
@ -625,6 +651,14 @@ const organizationCertManagerOverviewPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const organizationDedicatedInstancesPageRouteRoute =
organizationDedicatedInstancesPageRouteImport.update({
id: '/',
path: '/',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute,
} as any)
const organizationAppConnectionsAppConnectionsPageRouteRoute =
organizationAppConnectionsAppConnectionsPageRouteImport.update({
id: '/',
@ -1886,6 +1920,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances'
path: '/dedicated-instances'
fullPath: '/organization/dedicated-instances'
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': {
id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
path: '/secret-manager/$projectId'
@ -1907,6 +1948,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationAppConnectionsAppConnectionsPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/'
path: '/'
fullPath: '/organization/dedicated-instances/'
preLoaderRoute: typeof organizationDedicatedInstancesPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview'
path: '/cert-manager/overview'
@ -1914,6 +1962,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationCertManagerOverviewPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId'
path: '/$instanceId'
fullPath: '/organization/dedicated-instances/$instanceId'
preLoaderRoute: typeof organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId'
path: '/groups/$groupId'
@ -2900,6 +2955,24 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithCh
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteChildren {
organizationDedicatedInstancesPageRouteRoute: typeof organizationDedicatedInstancesPageRouteRoute
organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute: typeof organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteChildren =
{
organizationDedicatedInstancesPageRouteRoute:
organizationDedicatedInstancesPageRouteRoute,
organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute:
organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute,
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteWithChildren =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute._addFileChildren(
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
organizationAccessManagementPageRouteRoute: typeof organizationAccessManagementPageRouteRoute
organizationAdminPageRouteRoute: typeof organizationAdminPageRouteRoute
@ -2909,6 +2982,7 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute
organizationSettingsPageRouteRoute: typeof organizationSettingsPageRouteRoute
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteWithChildren
organizationCertManagerOverviewPageRouteRoute: typeof organizationCertManagerOverviewPageRouteRoute
organizationGroupDetailsByIDPageRouteRoute: typeof organizationGroupDetailsByIDPageRouteRoute
organizationIdentityDetailsByIDPageRouteRoute: typeof organizationIdentityDetailsByIDPageRouteRoute
@ -2933,6 +3007,8 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
organizationSettingsPageRouteRoute: organizationSettingsPageRouteRoute,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteWithChildren,
organizationCertManagerOverviewPageRouteRoute:
organizationCertManagerOverviewPageRouteRoute,
organizationGroupDetailsByIDPageRouteRoute:
@ -3608,10 +3684,13 @@ export interface FileRoutesByFullPath {
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
'/kms/$projectId': typeof kmsLayoutRouteWithChildren
'/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/organization/dedicated-instances': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteWithChildren
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
'/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/organization/dedicated-instances/': typeof organizationDedicatedInstancesPageRouteRoute
'/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/organization/dedicated-instances/$instanceId': typeof organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
'/organization/kms/overview': typeof organizationKmsOverviewPageRouteRoute
@ -3780,7 +3859,9 @@ export interface FileRoutesByTo {
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
'/organization/app-connections': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/organization/dedicated-instances': typeof organizationDedicatedInstancesPageRouteRoute
'/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/organization/dedicated-instances/$instanceId': typeof organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
'/organization/kms/overview': typeof organizationKmsOverviewPageRouteRoute
@ -3956,10 +4037,13 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSshProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/': typeof organizationDedicatedInstancesPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId': typeof organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/kms/overview': typeof organizationKmsOverviewPageRouteRoute
@ -4136,10 +4220,13 @@ export interface FileRouteTypes {
| '/cert-manager/$projectId'
| '/kms/$projectId'
| '/organization/app-connections'
| '/organization/dedicated-instances'
| '/secret-manager/$projectId'
| '/ssh/$projectId'
| '/organization/app-connections/'
| '/organization/dedicated-instances/'
| '/organization/cert-manager/overview'
| '/organization/dedicated-instances/$instanceId'
| '/organization/groups/$groupId'
| '/organization/identities/$identityId'
| '/organization/kms/overview'
@ -4307,7 +4394,9 @@ export interface FileRouteTypes {
| '/secret-manager/$projectId'
| '/ssh/$projectId'
| '/organization/app-connections'
| '/organization/dedicated-instances'
| '/organization/cert-manager/overview'
| '/organization/dedicated-instances/$instanceId'
| '/organization/groups/$groupId'
| '/organization/identities/$identityId'
| '/organization/kms/overview'
@ -4481,10 +4570,13 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/'
| '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/'
| '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview'
| '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId'
| '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId'
| '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId'
| '/_authenticate/_inject-org-details/_org-layout/organization/kms/overview'
@ -4848,6 +4940,7 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/settings",
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections",
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances",
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview",
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId",
"/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId",
@ -4919,6 +5012,14 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback"
]
},
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization",
"children": [
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/",
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId"
]
},
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout",
@ -4937,10 +5038,18 @@ export const routeTree = rootRoute
"filePath": "organization/AppConnections/AppConnectionsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections"
},
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/": {
"filePath": "organization/DedicatedInstancesPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances"
},
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview": {
"filePath": "organization/CertManagerOverviewPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
},
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId": {
"filePath": "organization/DedicatedInstancesPage/DedicatedInstanceDetailsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances"
},
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId": {
"filePath": "organization/GroupDetailsByIDPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"

View File

@ -16,6 +16,10 @@ const organizationRoutes = route("/organization", [
route("/admin", "organization/AdminPage/route.tsx"),
route("/audit-logs", "organization/AuditLogsPage/route.tsx"),
route("/billing", "organization/BillingPage/route.tsx"),
route("/dedicated-instances", [
index("organization/DedicatedInstancesPage/route.tsx"),
route("/$instanceId", "organization/DedicatedInstancesPage/DedicatedInstanceDetailsPage/route.tsx")
]),
route("/secret-sharing", "organization/SecretSharingPage/route.tsx"),
route("/settings", "organization/SettingsPage/route.tsx"),
route("/secret-scanning", "organization/SecretScanningPage/route.tsx"),

View File

@ -0,0 +1,27 @@
import { Router, Route as RootRoute } from '@tanstack/react-router';
import { DedicatedInstancesPage, DedicatedInstanceDetailsPage } from '@app/pages/organization/DedicatedInstancesPage';
// ... existing routes ...
// Add dedicated instances routes
const dedicatedInstancesRoute = new RootRoute({
path: '/organization/:organizationId/dedicated-instances',
component: DedicatedInstancesPage
});
const dedicatedInstanceDetailsRoute = new RootRoute({
path: '/organization/:organizationId/dedicated-instances/:instanceId',
component: DedicatedInstanceDetailsPage
});
// Add to routeTree
routeTree.addChildren([dedicatedInstancesRoute, dedicatedInstanceDetailsRoute]);
// Create and export router
export const router = new Router({ routeTree });
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstanceDetailsPage } from '@app/pages/organization/DedicatedInstancesPage';
export const Route = createFileRoute('/organization/$organizationId/dedicated-instances/$instanceId')({
component: DedicatedInstanceDetailsPage,
});

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstancesPage } from '@app/pages/organization/DedicatedInstancesPage';
export const Route = createFileRoute('/organization/$organizationId/dedicated-instances')({
component: DedicatedInstancesPage,
});

View File

@ -0,0 +1,12 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstancesPage, DedicatedInstanceDetailsPage } from '@app/pages/organization/DedicatedInstancesPage';
// List route
export const Route = createFileRoute('/organization/$organizationId/dedicated-instances')({
component: DedicatedInstancesPage
});
// Details route
export const InstanceRoute = createFileRoute('/organization/$organizationId/dedicated-instances/$instanceId')({
component: DedicatedInstanceDetailsPage
});