Compare commits
19 Commits
daniel/go-
...
dedicated-
Author | SHA1 | Date | |
---|---|---|---|
65b6f61b53 | |||
19940522aa | |||
28b18c1cb1 | |||
7ae2cc2db8 | |||
4a51b4d619 | |||
478e0c5ff5 | |||
5c08136fca | |||
cb8528adc4 | |||
d7935d30ce | |||
ac3bab3074 | |||
63b8301065 | |||
babe70e00f | |||
f23ea0991c | |||
f8ab2bcdfd | |||
9cdb4dcde9 | |||
69fb87bbfc | |||
b0cd5bd10d | |||
15119ffda9 | |||
4df409e627 |
3026
backend/package-lock.json
generated
@ -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",
|
||||
@ -139,9 +140,9 @@
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@google-cloud/kms": "^4.5.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/auth-app": "^7.1.1",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/auth-app": "^7.1.5",
|
||||
"@octokit/plugin-retry": "^7.1.4",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@octopusdeploy/api-client": "^3.4.1",
|
||||
"@opentelemetry/api": "^1.9.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",
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
@ -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
|
||||
|
5
backend/src/@types/knex.d.ts
vendored
@ -930,5 +930,10 @@ declare module "knex/types/tables" {
|
||||
TKmipClientCertificatesInsert,
|
||||
TKmipClientCertificatesUpdate
|
||||
>;
|
||||
[TableName.DedicatedInstances]: KnexOriginal.CompositeTableType<
|
||||
TDedicatedInstances,
|
||||
TDedicatedInstancesInsert,
|
||||
TDedicatedInstancesUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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'))`);
|
||||
}
|
34
backend/src/db/schemas/dedicated-instances.ts
Normal 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>>;
|
@ -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";
|
||||
|
1
backend/src/ee/migrations/dedicated-instance.ts
Normal file
@ -0,0 +1 @@
|
||||
|
141
backend/src/ee/routes/v1/dedicated-instance-router.ts
Normal 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
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
@ -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);
|
||||
|
@ -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
|
||||
};
|
||||
};
|
@ -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
|
||||
};
|
||||
};
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -1722,6 +1722,18 @@ export const SecretSyncs = {
|
||||
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`
|
||||
};
|
||||
},
|
||||
ADDITIONAL_SYNC_OPTIONS: {
|
||||
AWS_PARAMETER_STORE: {
|
||||
keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.",
|
||||
tags: "Optional resource tags to add to parameters synced by Infisical.",
|
||||
syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as resource tags to parameters synced by Infisical.`
|
||||
},
|
||||
AWS_SECRETS_MANAGER: {
|
||||
keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.",
|
||||
tags: "Optional tags to add to secrets synced by Infisical.",
|
||||
syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as tags to secrets synced by Infisical.`
|
||||
}
|
||||
},
|
||||
DESTINATION_CONFIG: {
|
||||
AWS_PARAMETER_STORE: {
|
||||
region: "The AWS region to sync secrets to.",
|
||||
|
@ -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[] = [];
|
||||
|
@ -1,13 +1,19 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { z } from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateAwsConnectionSchema,
|
||||
SanitizedAwsConnectionSchema,
|
||||
UpdateAwsConnectionSchema
|
||||
} from "@app/services/app-connection/aws";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
||||
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.AWS,
|
||||
server,
|
||||
@ -15,3 +21,42 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
||||
createSchema: CreateAwsConnectionSchema,
|
||||
updateSchema: UpdateAwsConnectionSchema
|
||||
});
|
||||
|
||||
// The below endpoints are not exposed and for Infisical App use
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/kms-keys`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
region: z.nativeEnum(AWSRegion),
|
||||
destination: z.enum([SecretSync.AWSParameterStore, SecretSync.AWSSecretsManager])
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
kmsKeys: z.object({ alias: z.string(), id: z.string() }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const kmsKeys = await server.services.appConnection.aws.listKmsKeys(
|
||||
{
|
||||
connectionId,
|
||||
...req.query
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
return { kmsKeys };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -22,18 +22,19 @@ import {
|
||||
TUpdateAppConnectionDTO,
|
||||
TValidateAppConnectionCredentials
|
||||
} from "@app/services/app-connection/app-connection-types";
|
||||
import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "@app/services/app-connection/databricks";
|
||||
import { databricksConnectionService } from "@app/services/app-connection/databricks/databricks-connection-service";
|
||||
import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github";
|
||||
import { githubConnectionService } from "@app/services/app-connection/github/github-connection-service";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
||||
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
||||
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
||||
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||
import { githubConnectionService } from "./github/github-connection-service";
|
||||
|
||||
export type TAppConnectionServiceFactoryDep = {
|
||||
appConnectionDAL: TAppConnectionDALFactory;
|
||||
@ -369,6 +370,7 @@ export const appConnectionServiceFactory = ({
|
||||
listAvailableAppConnectionsForUser,
|
||||
github: githubConnectionService(connectAppConnectionById),
|
||||
gcp: gcpConnectionService(connectAppConnectionById),
|
||||
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
||||
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
aws: awsConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
TAwsConnection,
|
||||
TAwsConnectionConfig,
|
||||
@ -16,6 +17,7 @@ import {
|
||||
TGitHubConnectionInput,
|
||||
TValidateGitHubConnectionCredentials
|
||||
} from "@app/services/app-connection/github";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import {
|
||||
TAzureAppConfigurationConnection,
|
||||
@ -73,3 +75,9 @@ export type TValidateAppConnectionCredentials =
|
||||
| TValidateAzureKeyVaultConnectionCredentials
|
||||
| TValidateAzureAppConfigurationConnectionCredentials
|
||||
| TValidateDatabricksConnectionCredentials;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
region: AWSRegion;
|
||||
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
|
||||
};
|
||||
|
@ -0,0 +1,88 @@
|
||||
import AWS from "aws-sdk";
|
||||
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TListAwsConnectionKmsKeys } from "@app/services/app-connection/app-connection-types";
|
||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TAwsConnection>;
|
||||
|
||||
const listAwsKmsKeys = async (
|
||||
appConnection: TAwsConnection,
|
||||
{ region, destination }: Pick<TListAwsConnectionKmsKeys, "region" | "destination">
|
||||
) => {
|
||||
const { credentials } = await getAwsConnectionConfig(appConnection, region);
|
||||
|
||||
const awsKms = new AWS.KMS({
|
||||
credentials,
|
||||
region
|
||||
});
|
||||
|
||||
const aliasEntries: AWS.KMS.AliasList = [];
|
||||
let aliasMarker: string | undefined;
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await awsKms.listAliases({ Limit: 100, Marker: aliasMarker }).promise();
|
||||
aliasEntries.push(...(response.Aliases || []));
|
||||
aliasMarker = response.NextMarker;
|
||||
} while (aliasMarker);
|
||||
|
||||
const keyMetadataRecord: Record<string, AWS.KMS.KeyMetadata | undefined> = {};
|
||||
for await (const aliasEntry of aliasEntries) {
|
||||
if (aliasEntry.TargetKeyId) {
|
||||
const keyDescription = await awsKms.describeKey({ KeyId: aliasEntry.TargetKeyId }).promise();
|
||||
|
||||
keyMetadataRecord[aliasEntry.TargetKeyId] = keyDescription.KeyMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
const validAliasEntries = aliasEntries.filter((aliasEntry) => {
|
||||
if (!aliasEntry.TargetKeyId) return false;
|
||||
|
||||
if (destination === SecretSync.AWSParameterStore && aliasEntry.AliasName === "alias/aws/ssm") return true;
|
||||
|
||||
if (destination === SecretSync.AWSSecretsManager && aliasEntry.AliasName === "alias/aws/secretsmanager")
|
||||
return true;
|
||||
|
||||
if (aliasEntry.AliasName?.includes("alias/aws/")) return false;
|
||||
|
||||
const keyMetadata = keyMetadataRecord[aliasEntry.TargetKeyId];
|
||||
|
||||
if (!keyMetadata || keyMetadata.KeyUsage !== "ENCRYPT_DECRYPT" || keyMetadata.KeySpec !== "SYMMETRIC_DEFAULT")
|
||||
return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const kmsKeys = validAliasEntries.map((aliasEntry) => {
|
||||
return {
|
||||
id: aliasEntry.TargetKeyId!,
|
||||
alias: aliasEntry.AliasName!
|
||||
};
|
||||
});
|
||||
|
||||
return kmsKeys;
|
||||
};
|
||||
|
||||
export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listKmsKeys = async (
|
||||
{ connectionId, region, destination }: TListAwsConnectionKmsKeys,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const appConnection = await getAppConnection(AppConnection.AWS, connectionId, actor);
|
||||
|
||||
const kmsKeys = await listAwsKmsKeys(appConnection, { region, destination });
|
||||
|
||||
return kmsKeys;
|
||||
};
|
||||
|
||||
return {
|
||||
listKmsKeys
|
||||
};
|
||||
};
|
@ -2257,7 +2257,9 @@ const syncSecretsFlyio = async ({
|
||||
}
|
||||
`;
|
||||
|
||||
await request.post(
|
||||
type TFlyioErrors = { message: string }[];
|
||||
|
||||
const setSecretsResp = await request.post<{ errors?: TFlyioErrors }>(
|
||||
IntegrationUrls.FLYIO_API_URL,
|
||||
{
|
||||
query: SetSecrets,
|
||||
@ -2279,6 +2281,10 @@ const syncSecretsFlyio = async ({
|
||||
}
|
||||
);
|
||||
|
||||
if (setSecretsResp.data.errors?.length) {
|
||||
throw new Error(JSON.stringify(setSecretsResp.data.errors));
|
||||
}
|
||||
|
||||
// get secrets
|
||||
interface FlyioSecret {
|
||||
name: string;
|
||||
|
@ -34,6 +34,25 @@ export const secretSharingServiceFactory = ({
|
||||
orgDAL,
|
||||
kmsService
|
||||
}: TSecretSharingServiceFactoryDep) => {
|
||||
const $validateSharedSecretExpiry = (expiresAt: string) => {
|
||||
if (new Date(expiresAt) < new Date()) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||
}
|
||||
|
||||
// Limit Expiry Time to 1 month
|
||||
const expiryTime = new Date(expiresAt).getTime();
|
||||
const currentTime = new Date().getTime();
|
||||
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
||||
if (expiryTime - currentTime > thirtyDays) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" });
|
||||
}
|
||||
|
||||
const fiveMins = 5 * 60 * 1000;
|
||||
if (expiryTime - currentTime < fiveMins) {
|
||||
throw new BadRequestError({ message: "Expiration time cannot be less than 5 mins" });
|
||||
}
|
||||
};
|
||||
|
||||
const createSharedSecret = async ({
|
||||
actor,
|
||||
actorId,
|
||||
@ -49,18 +68,7 @@ export const secretSharingServiceFactory = ({
|
||||
}: TCreateSharedSecretDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
|
||||
|
||||
if (new Date(expiresAt) < new Date()) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||
}
|
||||
|
||||
// Limit Expiry Time to 1 month
|
||||
const expiryTime = new Date(expiresAt).getTime();
|
||||
const currentTime = new Date().getTime();
|
||||
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
||||
if (expiryTime - currentTime > thirtyDays) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" });
|
||||
}
|
||||
$validateSharedSecretExpiry(expiresAt);
|
||||
|
||||
if (secretValue.length > 10_000) {
|
||||
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||
@ -100,17 +108,7 @@ export const secretSharingServiceFactory = ({
|
||||
expiresAfterViews,
|
||||
accessType
|
||||
}: TCreatePublicSharedSecretDTO) => {
|
||||
if (new Date(expiresAt) < new Date()) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||
}
|
||||
|
||||
// Limit Expiry Time to 1 month
|
||||
const expiryTime = new Date(expiresAt).getTime();
|
||||
const currentTime = new Date().getTime();
|
||||
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
||||
if (expiryTime - currentTime > thirtyDays) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" });
|
||||
}
|
||||
$validateSharedSecretExpiry(expiresAt);
|
||||
|
||||
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
||||
|
@ -7,6 +7,8 @@ import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
import { TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store-sync-types";
|
||||
|
||||
type TAWSParameterStoreRecord = Record<string, AWS.SSM.Parameter>;
|
||||
type TAWSParameterStoreMetadataRecord = Record<string, AWS.SSM.ParameterMetadata>;
|
||||
type TAWSParameterStoreTagsRecord = Record<string, Record<string, string>>;
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
const BATCH_SIZE = 10;
|
||||
@ -80,6 +82,129 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSPara
|
||||
return awsParameterStoreSecretsRecord;
|
||||
};
|
||||
|
||||
const getParameterMetadataByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSParameterStoreMetadataRecord> => {
|
||||
const awsParameterStoreMetadataRecord: TAWSParameterStoreMetadataRecord = {};
|
||||
let hasNext = true;
|
||||
let nextToken: string | undefined;
|
||||
let attempt = 0;
|
||||
|
||||
while (hasNext) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const parameters = await ssm
|
||||
.describeParameters({
|
||||
MaxResults: 10,
|
||||
NextToken: nextToken,
|
||||
ParameterFilters: [
|
||||
{
|
||||
Key: "Path",
|
||||
Option: "OneLevel",
|
||||
Values: [path]
|
||||
}
|
||||
]
|
||||
})
|
||||
.promise();
|
||||
|
||||
attempt = 0;
|
||||
|
||||
if (parameters.Parameters) {
|
||||
parameters.Parameters.forEach((parameter) => {
|
||||
if (parameter.Name) {
|
||||
// no leading slash if path is '/'
|
||||
const secKey = path.length > 1 ? parameter.Name.substring(path.length) : parameter.Name;
|
||||
awsParameterStoreMetadataRecord[secKey] = parameter;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hasNext = Boolean(parameters.NextToken);
|
||||
nextToken = parameters.NextToken;
|
||||
} catch (e) {
|
||||
if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
attempt += 1;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await sleep();
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return awsParameterStoreMetadataRecord;
|
||||
};
|
||||
|
||||
const getParameterStoreTagsRecord = async (
|
||||
ssm: AWS.SSM,
|
||||
awsParameterStoreSecretsRecord: TAWSParameterStoreRecord,
|
||||
needsTagsPermissions: boolean
|
||||
): Promise<{ shouldManageTags: boolean; awsParameterStoreTagsRecord: TAWSParameterStoreTagsRecord }> => {
|
||||
const awsParameterStoreTagsRecord: TAWSParameterStoreTagsRecord = {};
|
||||
|
||||
for await (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
|
||||
const [key, parameter] = entry;
|
||||
|
||||
if (!parameter.Name) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const tags = await ssm
|
||||
.listTagsForResource({
|
||||
ResourceType: "Parameter",
|
||||
ResourceId: parameter.Name
|
||||
})
|
||||
.promise();
|
||||
|
||||
awsParameterStoreTagsRecord[key] = Object.fromEntries(tags.TagList?.map((tag) => [tag.Key, tag.Value]) ?? []);
|
||||
} catch (e) {
|
||||
// users aren't required to provide tag permissions to use sync so we handle gracefully if unauthorized
|
||||
// and they aren't trying to configure tags
|
||||
if ((e as AWSError).code === "AccessDeniedException") {
|
||||
if (!needsTagsPermissions) {
|
||||
return { shouldManageTags: false, awsParameterStoreTagsRecord: {} };
|
||||
}
|
||||
|
||||
throw new SecretSyncError({
|
||||
message:
|
||||
"IAM role has inadequate permissions to manage resource tags. Ensure the following polices are present: ssm:ListTagsForResource, ssm:AddTagsToResource, and ssm:RemoveTagsFromResource",
|
||||
shouldRetry: false
|
||||
});
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return { shouldManageTags: true, awsParameterStoreTagsRecord };
|
||||
};
|
||||
|
||||
const processParameterTags = ({
|
||||
syncTagsRecord,
|
||||
awsTagsRecord
|
||||
}: {
|
||||
syncTagsRecord: Record<string, string>;
|
||||
awsTagsRecord: Record<string, string>;
|
||||
}) => {
|
||||
const tagsToAdd: AWS.SSM.TagList = [];
|
||||
const tagKeysToRemove: string[] = [];
|
||||
|
||||
for (const syncEntry of Object.entries(syncTagsRecord)) {
|
||||
const [syncKey, syncValue] = syncEntry;
|
||||
|
||||
if (!(syncKey in awsTagsRecord) || syncValue !== awsTagsRecord[syncKey])
|
||||
tagsToAdd.push({ Key: syncKey, Value: syncValue });
|
||||
}
|
||||
|
||||
for (const awsKey of Object.keys(awsTagsRecord)) {
|
||||
if (!(awsKey in syncTagsRecord)) tagKeysToRemove.push(awsKey);
|
||||
}
|
||||
|
||||
return { tagsToAdd, tagKeysToRemove };
|
||||
};
|
||||
|
||||
const putParameter = async (
|
||||
ssm: AWS.SSM,
|
||||
params: AWS.SSM.PutParameterRequest,
|
||||
@ -98,6 +223,42 @@ const putParameter = async (
|
||||
}
|
||||
};
|
||||
|
||||
const addTagsToParameter = async (
|
||||
ssm: AWS.SSM,
|
||||
params: Omit<AWS.SSM.AddTagsToResourceRequest, "ResourceType">,
|
||||
attempt = 0
|
||||
): Promise<AWS.SSM.AddTagsToResourceResult> => {
|
||||
try {
|
||||
return await ssm.addTagsToResource({ ...params, ResourceType: "Parameter" }).promise();
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return addTagsToParameter(ssm, params, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const removeTagsFromParameter = async (
|
||||
ssm: AWS.SSM,
|
||||
params: Omit<AWS.SSM.RemoveTagsFromResourceRequest, "ResourceType">,
|
||||
attempt = 0
|
||||
): Promise<AWS.SSM.RemoveTagsFromResourceResult> => {
|
||||
try {
|
||||
return await ssm.removeTagsFromResource({ ...params, ResourceType: "Parameter" }).promise();
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return removeTagsFromParameter(ssm, params, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteParametersBatch = async (
|
||||
ssm: AWS.SSM,
|
||||
parameters: AWS.SSM.Parameter[],
|
||||
@ -132,35 +293,92 @@ const deleteParametersBatch = async (
|
||||
|
||||
export const AwsParameterStoreSyncFns = {
|
||||
syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
const { destinationConfig, syncOptions } = secretSync;
|
||||
|
||||
const ssm = await getSSM(secretSync);
|
||||
|
||||
// TODO(scott): KMS Key ID, Tags
|
||||
|
||||
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
|
||||
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value }] = entry;
|
||||
const awsParameterStoreMetadataRecord = await getParameterMetadataByPath(ssm, destinationConfig.path);
|
||||
|
||||
// skip empty values (not allowed by AWS) or secrets that haven't changed
|
||||
if (!value || (key in awsParameterStoreSecretsRecord && awsParameterStoreSecretsRecord[key].Value === value)) {
|
||||
const { shouldManageTags, awsParameterStoreTagsRecord } = await getParameterStoreTagsRecord(
|
||||
ssm,
|
||||
awsParameterStoreSecretsRecord,
|
||||
Boolean(syncOptions.tags?.length || syncOptions.syncSecretMetadataAsTags)
|
||||
);
|
||||
const syncTagsRecord = Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []);
|
||||
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value, secretMetadata }] = entry;
|
||||
|
||||
// skip empty values (not allowed by AWS)
|
||||
if (!value) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await putParameter(ssm, {
|
||||
Name: `${destinationConfig.path}${key}`,
|
||||
Type: "SecureString",
|
||||
Value: value,
|
||||
Overwrite: true
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
const keyId = syncOptions.keyId ?? "alias/aws/ssm";
|
||||
|
||||
// create parameter or update if changed
|
||||
if (
|
||||
!(key in awsParameterStoreSecretsRecord) ||
|
||||
value !== awsParameterStoreSecretsRecord[key].Value ||
|
||||
keyId !== awsParameterStoreMetadataRecord[key]?.KeyId
|
||||
) {
|
||||
try {
|
||||
await putParameter(ssm, {
|
||||
Name: `${destinationConfig.path}${key}`,
|
||||
Type: "SecureString",
|
||||
Value: value,
|
||||
Overwrite: true,
|
||||
KeyId: keyId
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldManageTags) {
|
||||
const { tagsToAdd, tagKeysToRemove } = processParameterTags({
|
||||
syncTagsRecord: {
|
||||
// configured sync tags take preference over secret metadata
|
||||
...(syncOptions.syncSecretMetadataAsTags &&
|
||||
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
|
||||
...syncTagsRecord
|
||||
},
|
||||
awsTagsRecord: awsParameterStoreTagsRecord[key] ?? {}
|
||||
});
|
||||
|
||||
if (tagsToAdd.length) {
|
||||
try {
|
||||
await addTagsToParameter(ssm, {
|
||||
ResourceId: `${destinationConfig.path}${key}`,
|
||||
Tags: tagsToAdd
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tagKeysToRemove.length) {
|
||||
try {
|
||||
await removeTagsFromParameter(ssm, {
|
||||
ResourceId: `${destinationConfig.path}${key}`,
|
||||
TagKeys: tagKeysToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
||||
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region),
|
||||
@ -20,19 +21,68 @@ const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path)
|
||||
});
|
||||
|
||||
export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema(SecretSync.AWSParameterStore).extend({
|
||||
const AwsParameterStoreSyncOptionsSchema = z.object({
|
||||
keyId: z
|
||||
.string()
|
||||
.regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID")
|
||||
.min(1, "Invalid KMS Key ID")
|
||||
.max(256, "Invalid KMS Key ID")
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.keyId),
|
||||
tags: z
|
||||
.object({
|
||||
key: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.min(1, "Resource tag key required")
|
||||
.max(128, "Resource tag key cannot exceed 128 characters"),
|
||||
value: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid resource tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.max(256, "Resource tag value cannot exceed 256 characters")
|
||||
})
|
||||
.array()
|
||||
.max(50)
|
||||
.refine((items) => new Set(items.map((item) => item.key)).size === items.length, {
|
||||
message: "Resource tag keys must be unique"
|
||||
})
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.tags),
|
||||
syncSecretMetadataAsTags: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.syncSecretMetadataAsTags)
|
||||
});
|
||||
|
||||
const AwsParameterStoreSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema(
|
||||
SecretSync.AWSParameterStore,
|
||||
AwsParameterStoreSyncOptionsConfig,
|
||||
AwsParameterStoreSyncOptionsSchema
|
||||
).extend({
|
||||
destination: z.literal(SecretSync.AWSParameterStore),
|
||||
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateAwsParameterStoreSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSParameterStore
|
||||
SecretSync.AWSParameterStore,
|
||||
AwsParameterStoreSyncOptionsConfig,
|
||||
AwsParameterStoreSyncOptionsSchema
|
||||
).extend({
|
||||
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateAwsParameterStoreSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSParameterStore
|
||||
SecretSync.AWSParameterStore,
|
||||
AwsParameterStoreSyncOptionsConfig,
|
||||
AwsParameterStoreSyncOptionsSchema
|
||||
).extend({
|
||||
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
@ -1,16 +1,28 @@
|
||||
import { UntagResourceCommandOutput } from "@aws-sdk/client-kms";
|
||||
import {
|
||||
BatchGetSecretValueCommand,
|
||||
CreateSecretCommand,
|
||||
CreateSecretCommandInput,
|
||||
DeleteSecretCommand,
|
||||
DeleteSecretResponse,
|
||||
DescribeSecretCommand,
|
||||
DescribeSecretCommandInput,
|
||||
ListSecretsCommand,
|
||||
SecretsManagerClient,
|
||||
TagResourceCommand,
|
||||
TagResourceCommandOutput,
|
||||
UntagResourceCommand,
|
||||
UpdateSecretCommand,
|
||||
UpdateSecretCommandInput
|
||||
} from "@aws-sdk/client-secrets-manager";
|
||||
import { AWSError } from "aws-sdk";
|
||||
import { CreateSecretResponse, SecretListEntry, SecretValueEntry } from "aws-sdk/clients/secretsmanager";
|
||||
import {
|
||||
CreateSecretResponse,
|
||||
DescribeSecretResponse,
|
||||
SecretListEntry,
|
||||
SecretValueEntry,
|
||||
Tag
|
||||
} from "aws-sdk/clients/secretsmanager";
|
||||
|
||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
|
||||
@ -21,6 +33,7 @@ import { TAwsSecretsManagerSyncWithCredentials } from "./aws-secrets-manager-syn
|
||||
|
||||
type TAwsSecretsRecord = Record<string, SecretListEntry>;
|
||||
type TAwsSecretValuesRecord = Record<string, SecretValueEntry>;
|
||||
type TAwsSecretDescriptionsRecord = Record<string, DescribeSecretResponse>;
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
const BATCH_SIZE = 20;
|
||||
@ -135,6 +148,46 @@ const getSecretValuesRecord = async (
|
||||
return awsSecretValuesRecord;
|
||||
};
|
||||
|
||||
const describeSecret = async (
|
||||
client: SecretsManagerClient,
|
||||
input: DescribeSecretCommandInput,
|
||||
attempt = 0
|
||||
): Promise<DescribeSecretResponse> => {
|
||||
try {
|
||||
return await client.send(new DescribeSecretCommand(input));
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return describeSecret(client, input, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getSecretDescriptionsRecord = async (
|
||||
client: SecretsManagerClient,
|
||||
awsSecretsRecord: TAwsSecretsRecord
|
||||
): Promise<TAwsSecretDescriptionsRecord> => {
|
||||
const awsSecretDescriptionsRecord: TAwsSecretValuesRecord = {};
|
||||
|
||||
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||
try {
|
||||
awsSecretDescriptionsRecord[secretKey] = await describeSecret(client, {
|
||||
SecretId: secretKey
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
secretKey,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return awsSecretDescriptionsRecord;
|
||||
};
|
||||
|
||||
const createSecret = async (
|
||||
client: SecretsManagerClient,
|
||||
input: CreateSecretCommandInput,
|
||||
@ -189,9 +242,71 @@ const deleteSecret = async (
|
||||
}
|
||||
};
|
||||
|
||||
const addTags = async (
|
||||
client: SecretsManagerClient,
|
||||
secretKey: string,
|
||||
tags: Tag[],
|
||||
attempt = 0
|
||||
): Promise<TagResourceCommandOutput> => {
|
||||
try {
|
||||
return await client.send(new TagResourceCommand({ SecretId: secretKey, Tags: tags }));
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return addTags(client, secretKey, tags, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const removeTags = async (
|
||||
client: SecretsManagerClient,
|
||||
secretKey: string,
|
||||
tagKeys: string[],
|
||||
attempt = 0
|
||||
): Promise<UntagResourceCommandOutput> => {
|
||||
try {
|
||||
return await client.send(new UntagResourceCommand({ SecretId: secretKey, TagKeys: tagKeys }));
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return removeTags(client, secretKey, tagKeys, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const processTags = ({
|
||||
syncTagsRecord,
|
||||
awsTagsRecord
|
||||
}: {
|
||||
syncTagsRecord: Record<string, string>;
|
||||
awsTagsRecord: Record<string, string>;
|
||||
}) => {
|
||||
const tagsToAdd: Tag[] = [];
|
||||
const tagKeysToRemove: string[] = [];
|
||||
|
||||
for (const syncEntry of Object.entries(syncTagsRecord)) {
|
||||
const [syncKey, syncValue] = syncEntry;
|
||||
|
||||
if (!(syncKey in awsTagsRecord) || syncValue !== awsTagsRecord[syncKey])
|
||||
tagsToAdd.push({ Key: syncKey, Value: syncValue });
|
||||
}
|
||||
|
||||
for (const awsKey of Object.keys(awsTagsRecord)) {
|
||||
if (!(awsKey in syncTagsRecord)) tagKeysToRemove.push(awsKey);
|
||||
}
|
||||
|
||||
return { tagsToAdd, tagKeysToRemove };
|
||||
};
|
||||
|
||||
export const AwsSecretsManagerSyncFns = {
|
||||
syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
const { destinationConfig, syncOptions } = secretSync;
|
||||
|
||||
const client = await getSecretsManagerClient(secretSync);
|
||||
|
||||
@ -199,9 +314,15 @@ export const AwsSecretsManagerSyncFns = {
|
||||
|
||||
const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord);
|
||||
|
||||
const awsDescriptionsRecord = await getSecretDescriptionsRecord(client, awsSecretsRecord);
|
||||
|
||||
const syncTagsRecord = Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []);
|
||||
|
||||
const keyId = syncOptions.keyId ?? "alias/aws/secretsmanager";
|
||||
|
||||
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value }] = entry;
|
||||
const [key, { value, secretMetadata }] = entry;
|
||||
|
||||
// skip secrets that don't have a value set
|
||||
if (!value) {
|
||||
@ -211,15 +332,26 @@ export const AwsSecretsManagerSyncFns = {
|
||||
|
||||
if (awsSecretsRecord[key]) {
|
||||
// skip secrets that haven't changed
|
||||
if (awsValuesRecord[key]?.SecretString === value) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
if (awsValuesRecord[key]?.SecretString !== value || keyId !== awsDescriptionsRecord[key]?.KmsKeyId) {
|
||||
try {
|
||||
await updateSecret(client, {
|
||||
SecretId: key,
|
||||
SecretString: value,
|
||||
KmsKeyId: keyId
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
try {
|
||||
await updateSecret(client, {
|
||||
SecretId: key,
|
||||
SecretString: value
|
||||
await createSecret(client, {
|
||||
Name: key,
|
||||
SecretString: value,
|
||||
KmsKeyId: keyId
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
@ -227,12 +359,34 @@ export const AwsSecretsManagerSyncFns = {
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
const { tagsToAdd, tagKeysToRemove } = processTags({
|
||||
syncTagsRecord: {
|
||||
// configured sync tags take preference over secret metadata
|
||||
...(syncOptions.syncSecretMetadataAsTags &&
|
||||
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
|
||||
...syncTagsRecord
|
||||
},
|
||||
awsTagsRecord: Object.fromEntries(
|
||||
awsDescriptionsRecord[key]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
|
||||
)
|
||||
});
|
||||
|
||||
if (tagsToAdd.length) {
|
||||
try {
|
||||
await createSecret(client, {
|
||||
Name: key,
|
||||
SecretString: value
|
||||
await addTags(client, key, tagsToAdd);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tagKeysToRemove.length) {
|
||||
try {
|
||||
await removeTags(client, key, tagKeysToRemove);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
@ -261,17 +415,48 @@ export const AwsSecretsManagerSyncFns = {
|
||||
Object.fromEntries(Object.entries(secretMap).map(([key, secretData]) => [key, secretData.value]))
|
||||
);
|
||||
|
||||
if (awsValuesRecord[destinationConfig.secretName]) {
|
||||
if (awsSecretsRecord[destinationConfig.secretName]) {
|
||||
await updateSecret(client, {
|
||||
SecretId: destinationConfig.secretName,
|
||||
SecretString: secretValue
|
||||
SecretString: secretValue,
|
||||
KmsKeyId: keyId
|
||||
});
|
||||
} else {
|
||||
await createSecret(client, {
|
||||
Name: destinationConfig.secretName,
|
||||
SecretString: secretValue
|
||||
SecretString: secretValue,
|
||||
KmsKeyId: keyId
|
||||
});
|
||||
}
|
||||
|
||||
const { tagsToAdd, tagKeysToRemove } = processTags({
|
||||
syncTagsRecord,
|
||||
awsTagsRecord: Object.fromEntries(
|
||||
awsDescriptionsRecord[destinationConfig.secretName]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
|
||||
)
|
||||
});
|
||||
|
||||
if (tagsToAdd.length) {
|
||||
try {
|
||||
await addTags(client, destinationConfig.secretName, tagsToAdd);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: destinationConfig.secretName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tagKeysToRemove.length) {
|
||||
try {
|
||||
await removeTags(client, destinationConfig.secretName, tagKeysToRemove);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: destinationConfig.secretName
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials): Promise<TSecretMap> => {
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const AwsSecretsManagerSyncDestinationConfigSchema = z
|
||||
.discriminatedUnion("mappingBehavior", [
|
||||
@ -38,22 +39,95 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z
|
||||
})
|
||||
);
|
||||
|
||||
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(SecretSync.AWSSecretsManager).extend({
|
||||
const AwsSecretsManagerSyncOptionsSchema = z.object({
|
||||
keyId: z
|
||||
.string()
|
||||
.regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID")
|
||||
.min(1, "Invalid KMS Key ID")
|
||||
.max(256, "Invalid KMS Key ID")
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.keyId),
|
||||
tags: z
|
||||
.object({
|
||||
key: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.min(1, "Tag key required")
|
||||
.max(128, "Tag key cannot exceed 128 characters"),
|
||||
value: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.max(256, "Tag value cannot exceed 256 characters")
|
||||
})
|
||||
.array()
|
||||
.max(50)
|
||||
.refine((items) => new Set(items.map((item) => item.key)).size === items.length, {
|
||||
message: "Tag keys must be unique"
|
||||
})
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.tags),
|
||||
syncSecretMetadataAsTags: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.syncSecretMetadataAsTags)
|
||||
});
|
||||
|
||||
const AwsSecretsManagerSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(
|
||||
SecretSync.AWSSecretsManager,
|
||||
AwsSecretsManagerSyncOptionsConfig,
|
||||
AwsSecretsManagerSyncOptionsSchema
|
||||
).extend({
|
||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSSecretsManager
|
||||
).extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||
});
|
||||
SecretSync.AWSSecretsManager,
|
||||
AwsSecretsManagerSyncOptionsConfig,
|
||||
AwsSecretsManagerSyncOptionsSchema
|
||||
)
|
||||
.extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||
})
|
||||
.superRefine((sync, ctx) => {
|
||||
if (
|
||||
sync.destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne &&
|
||||
sync.syncOptions.syncSecretMetadataAsTags
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Syncing secret metadata is not supported with "Many-to-One" mapping behavior.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSSecretsManager
|
||||
).extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
SecretSync.AWSSecretsManager,
|
||||
AwsSecretsManagerSyncOptionsConfig,
|
||||
AwsSecretsManagerSyncOptionsSchema
|
||||
)
|
||||
.extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
|
||||
})
|
||||
.superRefine((sync, ctx) => {
|
||||
if (
|
||||
sync.destinationConfig?.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne &&
|
||||
sync.syncOptions.syncSecretMetadataAsTags
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Syncing secret metadata is not supported with "Many-to-One" mapping behavior.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const AwsSecretsManagerSyncListItemSchema = z.object({
|
||||
name: z.literal("AWS Secrets Manager"),
|
||||
|
@ -233,6 +233,7 @@ export const secretSyncQueueFactory = ({
|
||||
}
|
||||
|
||||
secretMap[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
||||
secretMap[secretKey].secretMetadata = secret.secretMetadata;
|
||||
})
|
||||
);
|
||||
|
||||
@ -258,7 +259,8 @@ export const secretSyncQueueFactory = ({
|
||||
secretMap[importedSecret.key] = {
|
||||
skipMultilineEncoding: importedSecret.skipMultilineEncoding,
|
||||
comment: importedSecret.secretComment,
|
||||
value: importedSecret.secretValue || ""
|
||||
value: importedSecret.secretValue || "",
|
||||
secretMetadata: importedSecret.secretMetadata
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { AnyZodObject, z } from "zod";
|
||||
|
||||
import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs";
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
@ -8,34 +8,45 @@ import { SecretSync, SecretSyncInitialSyncBehavior } from "@app/services/secret-
|
||||
import { SECRET_SYNC_CONNECTION_MAP } from "@app/services/secret-sync/secret-sync-maps";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const SyncOptionsSchema = (secretSync: SecretSync, options: TSyncOptionsConfig = { canImportSecrets: true }) =>
|
||||
z.object({
|
||||
initialSyncBehavior: (options.canImportSecrets
|
||||
const BaseSyncOptionsSchema = <T extends AnyZodObject | undefined = undefined>({
|
||||
destination,
|
||||
syncOptionsConfig: { canImportSecrets },
|
||||
merge,
|
||||
isUpdateSchema
|
||||
}: {
|
||||
destination: SecretSync;
|
||||
syncOptionsConfig: TSyncOptionsConfig;
|
||||
merge?: T;
|
||||
isUpdateSchema?: boolean;
|
||||
}) => {
|
||||
const baseSchema = z.object({
|
||||
initialSyncBehavior: (canImportSecrets
|
||||
? z.nativeEnum(SecretSyncInitialSyncBehavior)
|
||||
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
|
||||
).describe(SecretSyncs.SYNC_OPTIONS(secretSync).initialSyncBehavior)
|
||||
// prependPrefix: z
|
||||
// .string()
|
||||
// .trim()
|
||||
// .transform((str) => str.toUpperCase())
|
||||
// .optional()
|
||||
// .describe(SecretSyncs.SYNC_OPTIONS(secretSync).PREPEND_PREFIX),
|
||||
// appendSuffix: z
|
||||
// .string()
|
||||
// .trim()
|
||||
// .transform((str) => str.toUpperCase())
|
||||
// .optional()
|
||||
// .describe(SecretSyncs.SYNC_OPTIONS(secretSync).APPEND_SUFFIX)
|
||||
).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior)
|
||||
});
|
||||
|
||||
export const BaseSecretSyncSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) =>
|
||||
const schema = merge ? baseSchema.merge(merge) : baseSchema;
|
||||
|
||||
return (
|
||||
isUpdateSchema
|
||||
? schema.describe(SecretSyncs.UPDATE(destination).syncOptions).optional()
|
||||
: schema.describe(SecretSyncs.CREATE(destination).syncOptions)
|
||||
) as T extends AnyZodObject ? z.ZodObject<z.objectUtil.MergeShapes<typeof schema.shape, T["shape"]>> : typeof schema;
|
||||
};
|
||||
|
||||
export const BaseSecretSyncSchema = <T extends AnyZodObject | undefined = undefined>(
|
||||
destination: SecretSync,
|
||||
syncOptionsConfig: TSyncOptionsConfig,
|
||||
merge?: T
|
||||
) =>
|
||||
SecretSyncsSchema.omit({
|
||||
destination: true,
|
||||
destinationConfig: true,
|
||||
syncOptions: true
|
||||
}).extend({
|
||||
// destination needs to be on the extended object for type differentiation
|
||||
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig),
|
||||
syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge }),
|
||||
// join properties
|
||||
projectId: z.string(),
|
||||
connection: z.object({
|
||||
@ -47,7 +58,11 @@ export const BaseSecretSyncSchema = (destination: SecretSync, syncOptionsConfig?
|
||||
folder: z.object({ id: z.string(), path: z.string() }).nullable()
|
||||
});
|
||||
|
||||
export const GenericCreateSecretSyncFieldsSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) =>
|
||||
export const GenericCreateSecretSyncFieldsSchema = <T extends AnyZodObject | undefined = undefined>(
|
||||
destination: SecretSync,
|
||||
syncOptionsConfig: TSyncOptionsConfig,
|
||||
merge?: T
|
||||
) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(SecretSyncs.CREATE(destination).name),
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.CREATE(destination).projectId),
|
||||
@ -66,10 +81,14 @@ export const GenericCreateSecretSyncFieldsSchema = (destination: SecretSync, syn
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(SecretSyncs.CREATE(destination).secretPath),
|
||||
isAutoSyncEnabled: z.boolean().default(true).describe(SecretSyncs.CREATE(destination).isAutoSyncEnabled),
|
||||
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig).describe(SecretSyncs.CREATE(destination).syncOptions)
|
||||
syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge })
|
||||
});
|
||||
|
||||
export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syncOptionsConfig?: TSyncOptionsConfig) =>
|
||||
export const GenericUpdateSecretSyncFieldsSchema = <T extends AnyZodObject | undefined = undefined>(
|
||||
destination: SecretSync,
|
||||
syncOptionsConfig: TSyncOptionsConfig,
|
||||
merge?: T
|
||||
) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(SecretSyncs.UPDATE(destination).name).optional(),
|
||||
connectionId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).connectionId).optional(),
|
||||
@ -90,7 +109,5 @@ export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syn
|
||||
.optional()
|
||||
.describe(SecretSyncs.UPDATE(destination).secretPath),
|
||||
isAutoSyncEnabled: z.boolean().optional().describe(SecretSyncs.UPDATE(destination).isAutoSyncEnabled),
|
||||
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig)
|
||||
.optional()
|
||||
.describe(SecretSyncs.UPDATE(destination).syncOptions)
|
||||
syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge, isUpdateSchema: true })
|
||||
});
|
||||
|
@ -2,6 +2,7 @@ import { Job } from "bullmq";
|
||||
|
||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { QueueJobs } from "@app/queue";
|
||||
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
import {
|
||||
TAwsSecretsManagerSync,
|
||||
TAwsSecretsManagerSyncInput,
|
||||
@ -197,5 +198,10 @@ export type TSendSecretSyncFailedNotificationsJobDTO = Job<
|
||||
|
||||
export type TSecretMap = Record<
|
||||
string,
|
||||
{ value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined }
|
||||
{
|
||||
value: string;
|
||||
comment?: string;
|
||||
skipMultilineEncoding?: boolean | null | undefined;
|
||||
secretMetadata?: ResourceMetadataDTO;
|
||||
}
|
||||
>;
|
||||
|
BIN
docs/images/app-connections/aws/kms-key-user.png
Normal file
After Width: | Height: | Size: 492 KiB |
Before Width: | Height: | Size: 306 KiB After Width: | Height: | Size: 500 KiB |
Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 523 KiB |
Before Width: | Height: | Size: 659 KiB After Width: | Height: | Size: 885 KiB |
Before Width: | Height: | Size: 832 KiB After Width: | Height: | Size: 878 KiB |
@ -82,22 +82,26 @@ Infisical supports two methods for connecting to AWS.
|
||||
"Sid": "AllowSecretsManagerAccess",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"secretsmanager:GetSecretValue",
|
||||
"secretsmanager:CreateSecret",
|
||||
"secretsmanager:UpdateSecret",
|
||||
"secretsmanager:DescribeSecret",
|
||||
"secretsmanager:TagResource",
|
||||
"secretsmanager:UntagResource",
|
||||
"kms:ListKeys",
|
||||
"kms:ListAliases",
|
||||
"kms:Encrypt",
|
||||
"kms:Decrypt"
|
||||
"secretsmanager:ListSecrets",
|
||||
"secretsmanager:GetSecretValue",
|
||||
"secretsmanager:BatchGetSecretValue",
|
||||
"secretsmanager:CreateSecret",
|
||||
"secretsmanager:UpdateSecret",
|
||||
"secretsmanager:DeleteSecret",
|
||||
"secretsmanager:DescribeSecret",
|
||||
"secretsmanager:TagResource",
|
||||
"secretsmanager:UntagResource",
|
||||
"kms:ListAliases", // if you need to specify the KMS key
|
||||
"kms:Encrypt", // if you need to specify the KMS key
|
||||
"kms:Decrypt", // if you need to specify the KMS key
|
||||
"kms:DescribeKey" // if you need to specify the KMS key
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
<Note>If using a custom KMS key, be sure to add the IAM user or role as a key user. </Note>
|
||||
</Accordion>
|
||||
<Accordion title="AWS Parameter Store">
|
||||
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
|
||||
@ -112,23 +116,25 @@ Infisical supports two methods for connecting to AWS.
|
||||
"Sid": "AllowSSMAccess",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ssm:PutParameter",
|
||||
"ssm:DeleteParameter",
|
||||
"ssm:GetParameters",
|
||||
"ssm:GetParametersByPath",
|
||||
"ssm:DescribeParameters",
|
||||
"ssm:DeleteParameters",
|
||||
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
||||
"kms:ListKeys", // if you need to specify the KMS key
|
||||
"kms:ListAliases", // if you need to specify the KMS key
|
||||
"kms:Encrypt", // if you need to specify the KMS key
|
||||
"kms:Decrypt" // if you need to specify the KMS key
|
||||
"ssm:PutParameter",
|
||||
"ssm:GetParameters",
|
||||
"ssm:GetParametersByPath",
|
||||
"ssm:DescribeParameters",
|
||||
"ssm:DeleteParameters",
|
||||
"ssm:ListTagsForResource", // if you need to add tags to secrets
|
||||
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
||||
"ssm:RemoveTagsFromResource", // if you need to add tags to secrets
|
||||
"kms:ListAliases", // if you need to specify the KMS key
|
||||
"kms:Encrypt", // if you need to specify the KMS key
|
||||
"kms:Decrypt", // if you need to specify the KMS key
|
||||
"kms:DescribeKey" // if you need to specify the KMS key
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
<Note>If using a custom KMS key, be sure to add the IAM user or role as a key user. </Note>
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</Tab>
|
||||
@ -223,22 +229,26 @@ Infisical supports two methods for connecting to AWS.
|
||||
"Sid": "AllowSecretsManagerAccess",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"secretsmanager:GetSecretValue",
|
||||
"secretsmanager:CreateSecret",
|
||||
"secretsmanager:UpdateSecret",
|
||||
"secretsmanager:DescribeSecret",
|
||||
"secretsmanager:TagResource",
|
||||
"secretsmanager:UntagResource",
|
||||
"kms:ListKeys",
|
||||
"kms:ListAliases",
|
||||
"kms:Encrypt",
|
||||
"kms:Decrypt"
|
||||
"secretsmanager:ListSecrets",
|
||||
"secretsmanager:GetSecretValue",
|
||||
"secretsmanager:BatchGetSecretValue",
|
||||
"secretsmanager:CreateSecret",
|
||||
"secretsmanager:UpdateSecret",
|
||||
"secretsmanager:DeleteSecret",
|
||||
"secretsmanager:DescribeSecret",
|
||||
"secretsmanager:TagResource",
|
||||
"secretsmanager:UntagResource",
|
||||
"kms:ListAliases", // if you need to specify the KMS key
|
||||
"kms:Encrypt", // if you need to specify the KMS key
|
||||
"kms:Decrypt", // if you need to specify the KMS key
|
||||
"kms:DescribeKey" // if you need to specify the KMS key
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
<Note>If using a custom KMS key, be sure to add the IAM user or role as a key user. </Note>
|
||||
</Accordion>
|
||||
<Accordion title="AWS Parameter Store">
|
||||
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
|
||||
@ -253,23 +263,25 @@ Infisical supports two methods for connecting to AWS.
|
||||
"Sid": "AllowSSMAccess",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ssm:PutParameter",
|
||||
"ssm:DeleteParameter",
|
||||
"ssm:GetParameters",
|
||||
"ssm:GetParametersByPath",
|
||||
"ssm:DescribeParameters",
|
||||
"ssm:DeleteParameters",
|
||||
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
||||
"kms:ListKeys", // if you need to specify the KMS key
|
||||
"kms:ListAliases", // if you need to specify the KMS key
|
||||
"kms:Encrypt", // if you need to specify the KMS key
|
||||
"kms:Decrypt" // if you need to specify the KMS key
|
||||
"ssm:PutParameter",
|
||||
"ssm:GetParameters",
|
||||
"ssm:GetParametersByPath",
|
||||
"ssm:DescribeParameters",
|
||||
"ssm:DeleteParameters",
|
||||
"ssm:ListTagsForResource", // if you need to add tags to secrets
|
||||
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
||||
"ssm:RemoveTagsFromResource", // if you need to add tags to secrets
|
||||
"kms:ListAliases", // if you need to specify the KMS key
|
||||
"kms:Encrypt", // if you need to specify the KMS key
|
||||
"kms:Decrypt", // if you need to specify the KMS key
|
||||
"kms:DescribeKey" // if you need to specify the KMS key
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
<Note>If using a custom KMS key, be sure to add the IAM user or role as a key user. </Note>
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</Tab>
|
||||
|
@ -40,6 +40,10 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
|
||||
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
||||
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict.
|
||||
- **Import Secrets (Prioritize AWS Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict.
|
||||
- **KMS Key**: The AWS KMS key ID or alias to encrypt parameters with.
|
||||
- **Tags**: Optional resource tags to add to parameters synced by Infisical.
|
||||
- **Sync Secret Metadata as Resource Tags**: If enabled, metadata attached to secrets will be added as resource tags to parameters synced by Infisical.
|
||||
<Note>Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict.</Note>
|
||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||
|
||||
6. Configure the **Details** of your Parameter Store Sync, then click **Next**.
|
||||
|
@ -43,6 +43,9 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical."
|
||||
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
||||
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict.
|
||||
- **Import Secrets (Prioritize AWS Secrets Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
|
||||
- **KMS Key**: The AWS KMS key ID or alias to encrypt secrets with.
|
||||
- **Tags**: Optional tags to add to secrets synced by Infisical.
|
||||
- **Sync Secret Metadata as Tags**: If enabled, metadata attached to secrets will be added as tags to secrets synced by Infisical.
|
||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||
|
||||
6. Configure the **Details** of your Secrets Manager Sync, then click **Next**.
|
||||
|
@ -77,6 +77,13 @@ via the UI or API for the third-party service you intend to sync secrets to.
|
||||
- <strong>Destination:</strong> The App Connection to utilize and the destination endpoint to deploy secrets to. These can vary between services.
|
||||
- <strong>Options:</strong> Customize how secrets should be synced. Examples include adding a suffix or prefix to your secrets, or importing secrets from the destination on the initial sync.
|
||||
|
||||
<Note>
|
||||
Secret Syncs are the source of truth for connected third-party services. Any secret,
|
||||
including associated data, not present or imported in Infisical before syncing will be
|
||||
overwritten, and changes made directly in the connected service outside of infisical may also
|
||||
be overwritten by future syncs.
|
||||
</Note>
|
||||
|
||||
<Info>
|
||||
Some third-party services do not support importing secrets.
|
||||
</Info>
|
||||
|
113
frontend/package-lock.json
generated
@ -23,7 +23,7 @@
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@lottiefiles/dotlottie-react": "^0.12.0",
|
||||
"@octokit/rest": "^21.0.2",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"@peculiar/x509": "^1.12.3",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.3",
|
||||
@ -1605,16 +1605,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/core": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz",
|
||||
"integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==",
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.4.tgz",
|
||||
"integrity": "sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^5.0.0",
|
||||
"@octokit/graphql": "^8.0.0",
|
||||
"@octokit/request": "^9.0.0",
|
||||
"@octokit/request-error": "^6.0.1",
|
||||
"@octokit/types": "^13.0.0",
|
||||
"@octokit/graphql": "^8.1.2",
|
||||
"@octokit/request": "^9.2.1",
|
||||
"@octokit/request-error": "^6.1.7",
|
||||
"@octokit/types": "^13.6.2",
|
||||
"before-after-hook": "^3.0.2",
|
||||
"universal-user-agent": "^7.0.0"
|
||||
},
|
||||
@ -1623,12 +1623,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/endpoint": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
|
||||
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
|
||||
"version": "10.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.3.tgz",
|
||||
"integrity": "sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.0.0",
|
||||
"@octokit/types": "^13.6.2",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@ -1636,13 +1636,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz",
|
||||
"integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==",
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.1.tgz",
|
||||
"integrity": "sha512-n57hXtOoHrhwTWdvhVkdJHdhTv0JstjDbDRhJfwIRNfFqmSo1DaK/mD2syoNUoLCyqSjBpGAKOG0BuwF392slw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/request": "^9.0.0",
|
||||
"@octokit/types": "^13.0.0",
|
||||
"@octokit/request": "^9.2.2",
|
||||
"@octokit/types": "^13.8.0",
|
||||
"universal-user-agent": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@ -1650,18 +1650,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==",
|
||||
"version": "23.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz",
|
||||
"integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/plugin-paginate-rest": {
|
||||
"version": "11.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.6.tgz",
|
||||
"integrity": "sha512-zcvqqf/+TicbTCa/Z+3w4eBJcAxCFymtc0UAIsR3dEVoNilWld4oXdscQ3laXamTszUZdusw97K8+DrbFiOwjw==",
|
||||
"version": "11.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.2.tgz",
|
||||
"integrity": "sha512-BXJ7XPCTDXFF+wxcg/zscfgw2O/iDPtNSkwwR1W1W5c4Mb3zav/M2XvxQ23nVmKj7jpweB4g8viMeCQdm7LMVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.6.2"
|
||||
"@octokit/types": "^13.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
@ -1683,12 +1683,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-rest-endpoint-methods": {
|
||||
"version": "13.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.6.tgz",
|
||||
"integrity": "sha512-wMsdyHMjSfKjGINkdGKki06VEkgdEldIGstIEyGX0wbYHGByOwN/KiM+hAAlUwAtPkP3gvXtVQA9L3ITdV2tVw==",
|
||||
"version": "13.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.1.tgz",
|
||||
"integrity": "sha512-o8uOBdsyR+WR8MK9Cco8dCgvG13H1RlM1nWnK/W7TEACQBFux/vPREgKucxUfuDQ5yi1T3hGf4C5ZmZXAERgwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.6.1"
|
||||
"@octokit/types": "^13.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
@ -1698,14 +1698,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request": {
|
||||
"version": "9.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz",
|
||||
"integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==",
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.2.tgz",
|
||||
"integrity": "sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/endpoint": "^10.0.0",
|
||||
"@octokit/request-error": "^6.0.1",
|
||||
"@octokit/types": "^13.1.0",
|
||||
"@octokit/endpoint": "^10.1.3",
|
||||
"@octokit/request-error": "^6.1.7",
|
||||
"@octokit/types": "^13.6.2",
|
||||
"fast-content-type-parse": "^2.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@ -1713,39 +1714,39 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request-error": {
|
||||
"version": "6.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz",
|
||||
"integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==",
|
||||
"version": "6.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.7.tgz",
|
||||
"integrity": "sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.0.0"
|
||||
"@octokit/types": "^13.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest": {
|
||||
"version": "21.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.0.2.tgz",
|
||||
"integrity": "sha512-+CiLisCoyWmYicH25y1cDfCrv41kRSvTq6pPWtRroRJzhsCZWZyCqGyI8foJT5LmScADSwRAnr/xo+eewL04wQ==",
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.1.tgz",
|
||||
"integrity": "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/core": "^6.1.2",
|
||||
"@octokit/plugin-paginate-rest": "^11.0.0",
|
||||
"@octokit/core": "^6.1.4",
|
||||
"@octokit/plugin-paginate-rest": "^11.4.2",
|
||||
"@octokit/plugin-request-log": "^5.3.1",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^13.0.0"
|
||||
"@octokit/plugin-rest-endpoint-methods": "^13.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/types": {
|
||||
"version": "13.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.2.tgz",
|
||||
"integrity": "sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA==",
|
||||
"version": "13.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz",
|
||||
"integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^22.2.0"
|
||||
"@octokit/openapi-types": "^23.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-cms": {
|
||||
@ -6905,6 +6906,22 @@
|
||||
"safe-buffer": "^5.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-content-type-parse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz",
|
||||
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
|
@ -27,7 +27,7 @@
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@lottiefiles/dotlottie-react": "^0.12.0",
|
||||
"@octokit/rest": "^21.0.2",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"@peculiar/x509": "^1.12.3",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.3",
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, Checkbox, FormControl, Switch } from "@app/components/v2";
|
||||
import { Button, FormControl, Switch } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import {
|
||||
@ -16,10 +18,10 @@ import {
|
||||
useSecretSyncOption
|
||||
} from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields/SecretSyncOptionsFields";
|
||||
import { SecretSyncFormSchema, TSecretSyncForm } from "./schemas";
|
||||
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
|
||||
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
|
||||
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields";
|
||||
import { SecretSyncReviewFields } from "./SecretSyncReviewFields";
|
||||
import { SecretSyncSourceFields } from "./SecretSyncSourceFields";
|
||||
|
||||
@ -32,7 +34,7 @@ type Props = {
|
||||
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[] }[] = [
|
||||
{ name: "Source", key: "source", fields: ["secretPath", "environment"] },
|
||||
{ name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] },
|
||||
{ name: "Options", key: "options", fields: ["syncOptions"] },
|
||||
{ name: "Sync Options", key: "options", fields: ["syncOptions"] },
|
||||
{ name: "Details", key: "details", fields: ["name", "description"] },
|
||||
{ name: "Review", key: "review", fields: [] }
|
||||
];
|
||||
@ -42,8 +44,9 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { name: destinationName } = SECRET_SYNC_MAP[destination];
|
||||
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
const [confirmOverwrite, setConfirmOverwrite] = useState(false);
|
||||
|
||||
const { syncOption } = useSecretSyncOption(destination);
|
||||
|
||||
@ -77,6 +80,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
onComplete(secretSync);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setShowConfirmation(false);
|
||||
createNotification({
|
||||
title: `Failed to add ${destinationName} Sync`,
|
||||
text: err.message,
|
||||
@ -94,7 +98,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
setSelectedTabIndex((prev) => prev - 1);
|
||||
};
|
||||
|
||||
const { handleSubmit, trigger, watch, control } = formMethods;
|
||||
const { handleSubmit, trigger, control } = formMethods;
|
||||
|
||||
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
|
||||
|
||||
@ -102,7 +106,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
|
||||
const handleNext = async () => {
|
||||
if (isFinalStep) {
|
||||
handleSubmit(onSubmit)();
|
||||
setShowConfirmation(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -123,7 +127,42 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
return isEnabled;
|
||||
};
|
||||
|
||||
const initialSyncBehavior = watch("syncOptions.initialSyncBehavior");
|
||||
if (showConfirmation)
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col rounded-sm border border-l-[2px] border-mineshaft-600 border-l-primary bg-mineshaft-700/80 px-4 py-3">
|
||||
<div className="mb-1 flex items-center text-sm">
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
|
||||
Secret Sync Behavior
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-bunker-200">
|
||||
Secret Syncs are the source of truth for connected third-party services. Any secret,
|
||||
including associated data, not present or imported in Infisical before syncing will be
|
||||
overwritten, and changes made directly in the connected service outside of infisical may
|
||||
also be overwritten by future syncs.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-4">
|
||||
<Button
|
||||
isDisabled={createSecretSync.isPending}
|
||||
isLoading={createSecretSync.isPending}
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
colorSchema="secondary"
|
||||
>
|
||||
I Understand
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
isDisabled={createSecretSync.isPending}
|
||||
variant="plain"
|
||||
onClick={() => setShowConfirmation(false)}
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
|
||||
@ -174,7 +213,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/50"
|
||||
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="auto-sync-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
@ -196,32 +235,9 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</FormProvider>
|
||||
{isFinalStep &&
|
||||
initialSyncBehavior === SecretSyncInitialSyncBehavior.OverwriteDestination && (
|
||||
<Checkbox
|
||||
id="confirm-overwrite"
|
||||
isChecked={confirmOverwrite}
|
||||
containerClassName="-mt-5"
|
||||
onCheckedChange={(isChecked) => setConfirmOverwrite(Boolean(isChecked))}
|
||||
>
|
||||
<p
|
||||
className={`mt-5 text-wrap text-xs ${confirmOverwrite ? "text-mineshaft-200" : "text-red"}`}
|
||||
>
|
||||
I understand all secrets present in the configured {destinationName} destination will
|
||||
be removed if they are not present within Infisical.
|
||||
</p>
|
||||
</Checkbox>
|
||||
)}
|
||||
|
||||
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
||||
<Button
|
||||
isDisabled={
|
||||
isFinalStep &&
|
||||
initialSyncBehavior === SecretSyncInitialSyncBehavior.OverwriteDestination &&
|
||||
!confirmOverwrite
|
||||
}
|
||||
onClick={handleNext}
|
||||
colorSchema="secondary"
|
||||
>
|
||||
<Button onClick={handleNext} colorSchema="secondary">
|
||||
{isFinalStep ? "Create Sync" : "Next"}
|
||||
</Button>
|
||||
{selectedTabIndex > 0 && (
|
||||
|
@ -8,10 +8,10 @@ import { Button, ModalClose } from "@app/components/v2";
|
||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { TSecretSync, useUpdateSecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields/SecretSyncOptionsFields";
|
||||
import { TSecretSyncForm, UpdateSecretSyncFormSchema } from "./schemas";
|
||||
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
|
||||
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
|
||||
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields";
|
||||
import { SecretSyncSourceFields } from "./SecretSyncSourceFields";
|
||||
|
||||
type Props = {
|
||||
|
@ -8,13 +8,17 @@ import { TSecretSyncForm } from "../schemas";
|
||||
import { AwsRegionSelect } from "./shared";
|
||||
|
||||
export const AwsParameterStoreSyncFields = () => {
|
||||
const { control } = useFormContext<
|
||||
const { control, setValue } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
||||
>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncConnectionField />
|
||||
<SecretSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("syncOptions.keyId", undefined);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
|
||||
|
@ -9,7 +9,7 @@ import { TSecretSyncForm } from "../schemas";
|
||||
import { AwsRegionSelect } from "./shared";
|
||||
|
||||
export const AwsSecretsManagerSyncFields = () => {
|
||||
const { control, watch } = useFormContext<
|
||||
const { control, watch, setValue } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
|
||||
>();
|
||||
|
||||
@ -59,7 +59,10 @@ export const AwsSecretsManagerSyncFields = () => {
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
onValueChange={(val) => {
|
||||
onChange(val);
|
||||
setValue("syncOptions.syncSecretMetadataAsTags", false);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
placeholder="Select an option..."
|
||||
|
@ -0,0 +1,209 @@
|
||||
import { Fragment } from "react";
|
||||
import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form";
|
||||
import { SingleValue } from "react-select";
|
||||
import { faPlus, faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
Switch,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
TAwsConnectionKmsKey,
|
||||
useListAwsConnectionKmsKeys
|
||||
} from "@app/hooks/api/appConnections/aws";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
export const AwsParameterStoreSyncOptionsFields = () => {
|
||||
const { control, watch } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
||||
>();
|
||||
|
||||
const region = watch("destinationConfig.region");
|
||||
const connectionId = useWatch({ name: "connection.id", control });
|
||||
|
||||
const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys(
|
||||
{
|
||||
connectionId,
|
||||
region,
|
||||
destination: SecretSync.AWSParameterStore
|
||||
},
|
||||
{ enabled: Boolean(connectionId && region) }
|
||||
);
|
||||
|
||||
const tagFields = useFieldArray({
|
||||
control,
|
||||
name: "syncOptions.tags"
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
name="syncOptions.keyId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="The AWS KMS key to encrypt parameters with"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="KMS Key"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isKmsKeysPending && Boolean(connectionId && region)}
|
||||
isDisabled={!connectionId}
|
||||
value={kmsKeys.find((org) => org.alias === value) ?? null}
|
||||
onChange={(option) =>
|
||||
onChange((option as SingleValue<TAwsConnectionKmsKey>)?.alias ?? null)
|
||||
}
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
noOptionsMessage={({ inputValue }) =>
|
||||
inputValue ? undefined : (
|
||||
<p>
|
||||
To configure a KMS key, ensure the following permissions are present on the
|
||||
selected IAM role:{" "}
|
||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||
"kms:ListAliases"
|
||||
</span>
|
||||
,{" "}
|
||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||
"kms:DescribeKey"
|
||||
</span>
|
||||
,{" "}
|
||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||
"kms:Encrypt"
|
||||
</span>
|
||||
,{" "}
|
||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||
"kms:Decrypt"
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
options={kmsKeys}
|
||||
placeholder="Leave blank to use default KMS key"
|
||||
getOptionLabel={(option) =>
|
||||
option.alias === "alias/aws/ssm" ? `${option.alias} (Default)` : option.alias
|
||||
}
|
||||
getOptionValue={(option) => option.alias}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormLabel
|
||||
label="Resource Tags"
|
||||
tooltipText="Add resource tags to parameters synced by Infisical"
|
||||
/>
|
||||
<div className="mb-3 grid max-h-[20vh] grid-cols-12 flex-col items-end gap-2 overflow-y-auto">
|
||||
{tagFields.fields.map(({ id: tagFieldId }, i) => (
|
||||
<Fragment key={tagFieldId}>
|
||||
<div className="col-span-5">
|
||||
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`syncOptions.tags.${i}.key`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input className="text-xs" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-6">
|
||||
{i === 0 && (
|
||||
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional />
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`syncOptions.tags.${i}.value`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input className="text-xs" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Tooltip content="Remove tag" position="right">
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="Remove tag"
|
||||
className="col-span-1 mb-1.5"
|
||||
colorSchema="danger"
|
||||
size="xs"
|
||||
onClick={() => tagFields.remove(i)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 flex">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() => tagFields.append({ key: "", value: "" })}
|
||||
>
|
||||
Add Tag
|
||||
</Button>
|
||||
</div>
|
||||
<Controller
|
||||
name="syncOptions.syncSecretMetadataAsTags"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mt-6"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="overwrite-existing-secrets"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
>
|
||||
<p className="w-[18rem]">
|
||||
Sync Secret Metadata as Resource Tags{" "}
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
If enabled, metadata attached to secrets will be added as resource tags to
|
||||
parameters synced by Infisical.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
Manually configured tags from the field above will take precedence over
|
||||
secret metadata when tag keys conflict.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,208 @@
|
||||
import { Fragment } from "react";
|
||||
import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form";
|
||||
import { SingleValue } from "react-select";
|
||||
import { faPlus, faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
Switch,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
TAwsConnectionKmsKey,
|
||||
useListAwsConnectionKmsKeys
|
||||
} from "@app/hooks/api/appConnections/aws";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
export const AwsSecretsManagerSyncOptionsFields = () => {
|
||||
const { control, watch } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
|
||||
>();
|
||||
|
||||
const region = watch("destinationConfig.region");
|
||||
const connectionId = useWatch({ name: "connection.id", control });
|
||||
const mappingBehavior = watch("destinationConfig.mappingBehavior");
|
||||
|
||||
const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys(
|
||||
{
|
||||
connectionId,
|
||||
region,
|
||||
destination: SecretSync.AWSSecretsManager
|
||||
},
|
||||
{ enabled: Boolean(connectionId && region) }
|
||||
);
|
||||
|
||||
const tagFields = useFieldArray({
|
||||
control,
|
||||
name: "syncOptions.tags"
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
name="syncOptions.keyId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="The AWS KMS key to encrypt secrets with"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="KMS Key"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isKmsKeysPending && Boolean(connectionId && region)}
|
||||
isDisabled={!connectionId}
|
||||
value={kmsKeys.find((org) => org.alias === value) ?? null}
|
||||
onChange={(option) =>
|
||||
onChange((option as SingleValue<TAwsConnectionKmsKey>)?.alias ?? null)
|
||||
}
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
noOptionsMessage={({ inputValue }) =>
|
||||
inputValue ? undefined : (
|
||||
<p>
|
||||
To configure a KMS key, ensure the following permissions are present on the
|
||||
selected IAM role:{" "}
|
||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||
"kms:ListAliases"
|
||||
</span>
|
||||
,{" "}
|
||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||
"kms:DescribeKey"
|
||||
</span>
|
||||
,{" "}
|
||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||
"kms:Encrypt"
|
||||
</span>
|
||||
,{" "}
|
||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||
"kms:Decrypt"
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
options={kmsKeys}
|
||||
placeholder="Leave blank to use default KMS key"
|
||||
getOptionLabel={(option) =>
|
||||
option.alias === "alias/aws/secretsmanager"
|
||||
? `${option.alias} (Default)`
|
||||
: option.alias
|
||||
}
|
||||
getOptionValue={(option) => option.alias}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormLabel label="Tags" tooltipText="Add tags to secrets synced by Infisical" />
|
||||
<div className="mb-3 grid max-h-[20vh] grid-cols-12 flex-col items-end gap-2 overflow-y-auto">
|
||||
{tagFields.fields.map(({ id: tagFieldId }, i) => (
|
||||
<Fragment key={tagFieldId}>
|
||||
<div className="col-span-5">
|
||||
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`syncOptions.tags.${i}.key`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input className="text-xs" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-6">
|
||||
{i === 0 && (
|
||||
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional />
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`syncOptions.tags.${i}.value`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input className="text-xs" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Tooltip content="Remove tag" position="right">
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="Remove tag"
|
||||
className="col-span-1 mb-1.5"
|
||||
colorSchema="danger"
|
||||
size="xs"
|
||||
onClick={() => tagFields.remove(i)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className="mb-6 mt-2 flex">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() => tagFields.append({ key: "", value: "" })}
|
||||
>
|
||||
Add Tag
|
||||
</Button>
|
||||
</div>
|
||||
{mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne && (
|
||||
<Controller
|
||||
name="syncOptions.syncSecretMetadataAsTags"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="overwrite-existing-secrets"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
>
|
||||
<p className="w-[14rem]">
|
||||
Sync Secret Metadata as Tags{" "}
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
If enabled, metadata attached to secrets will be added as tags to secrets
|
||||
synced by Infisical.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
Manually configured tags from the field above will take precedence over
|
||||
secret metadata when tag keys conflict.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,12 +1,15 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { useSecretSyncOption } from "@app/hooks/api/secretSyncs";
|
||||
import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "./schemas";
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields";
|
||||
import { AwsSecretsManagerSyncOptionsFields } from "./AwsSecretsManagerSyncOptionsFields";
|
||||
|
||||
type Props = {
|
||||
hideInitialSync?: boolean;
|
||||
@ -21,6 +24,26 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
|
||||
const { syncOption } = useSecretSyncOption(destination);
|
||||
|
||||
let AdditionalSyncOptionsFieldsComponent: ReactNode;
|
||||
|
||||
switch (destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
AdditionalSyncOptionsFieldsComponent = <AwsParameterStoreSyncOptionsFields />;
|
||||
break;
|
||||
case SecretSync.AWSSecretsManager:
|
||||
AdditionalSyncOptionsFieldsComponent = <AwsSecretsManagerSyncOptionsFields />;
|
||||
break;
|
||||
case SecretSync.GitHub:
|
||||
case SecretSync.GCPSecretManager:
|
||||
case SecretSync.AzureKeyVault:
|
||||
case SecretSync.AzureAppConfiguration:
|
||||
case SecretSync.Databricks:
|
||||
AdditionalSyncOptionsFieldsComponent = null;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Additional Sync Options Fields: ${destination}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">Configure how secrets should be synced.</p>
|
||||
@ -91,6 +114,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{AdditionalSyncOptionsFieldsComponent}
|
||||
{/* <Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
@ -0,0 +1 @@
|
||||
export * from "./SecretSyncOptionsFields";
|
@ -1,17 +1,71 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
|
||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const AwsParameterStoreSyncReviewFields = () => {
|
||||
export const AwsParameterStoreSyncOptionsReviewFields = () => {
|
||||
const { watch } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
||||
>();
|
||||
|
||||
const [region, path] = watch(["destinationConfig.region", "destinationConfig.path"]);
|
||||
const [{ keyId, tags, syncSecretMetadataAsTags }] = watch(["syncOptions"]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
|
||||
{tags && tags.length > 0 && (
|
||||
<SecretSyncLabel label="Resource Tags">
|
||||
<Tooltip
|
||||
side="right"
|
||||
className="max-w-xl p-1"
|
||||
content={
|
||||
<Table>
|
||||
<THead>
|
||||
<Th className="whitespace-nowrap p-2">Key</Th>
|
||||
<Th className="p-2">Value</Th>
|
||||
</THead>
|
||||
<TBody>
|
||||
{tags.map((tag) => (
|
||||
<Tr key={tag.key}>
|
||||
<Td className="p-2">{tag.key}</Td>
|
||||
<Td className="p-2">{tag.value}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>
|
||||
{tags.length} Tag{tags.length > 1 ? "s" : ""}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</SecretSyncLabel>
|
||||
)}
|
||||
{syncSecretMetadataAsTags && (
|
||||
<SecretSyncLabel label="Sync Secret Metadata as Resource Tags">
|
||||
<Badge variant="success">Enabled</Badge>
|
||||
</SecretSyncLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AwsParameterStoreDestinationReviewFields = () => {
|
||||
const { watch } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
|
||||
>();
|
||||
|
||||
const [{ region, path }] = watch(["destinationConfig"]);
|
||||
|
||||
const awsRegion = AWS_REGIONS.find((r) => r.slug === region);
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
|
||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
||||
@ -37,3 +39,55 @@ export const AwsSecretsManagerSyncReviewFields = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AwsSecretsManagerSyncOptionsReviewFields = () => {
|
||||
const { watch } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
|
||||
>();
|
||||
|
||||
const [{ keyId, tags, syncSecretMetadataAsTags }] = watch(["syncOptions"]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
|
||||
{tags && tags.length > 0 && (
|
||||
<SecretSyncLabel label="Tags">
|
||||
<Tooltip
|
||||
side="right"
|
||||
className="max-w-xl p-1"
|
||||
content={
|
||||
<Table>
|
||||
<THead>
|
||||
<Th className="whitespace-nowrap p-2">Key</Th>
|
||||
<Th className="p-2">Value</Th>
|
||||
</THead>
|
||||
<TBody>
|
||||
{tags.map((tag) => (
|
||||
<Tr key={tag.key}>
|
||||
<Td className="p-2">{tag.key}</Td>
|
||||
<Td className="p-2">{tag.value}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>
|
||||
{tags.length} Tag{tags.length > 1 ? "s" : ""}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</SecretSyncLabel>
|
||||
)}
|
||||
{syncSecretMetadataAsTags && (
|
||||
<SecretSyncLabel label="Sync Secret Metadata as Resource Tags">
|
||||
<Badge variant="success">Enabled</Badge>
|
||||
</SecretSyncLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -3,15 +3,21 @@ import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { AwsSecretsManagerSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/AwsSecretsManagerSyncReviewFields";
|
||||
import { DatabricksSyncReviewFields } from "@app/components/secret-syncs/forms/SecretSyncReviewFields/DatabricksSyncReviewFields";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields";
|
||||
import {
|
||||
AwsParameterStoreDestinationReviewFields,
|
||||
AwsParameterStoreSyncOptionsReviewFields
|
||||
} from "./AwsParameterStoreSyncReviewFields";
|
||||
import {
|
||||
AwsSecretsManagerSyncOptionsReviewFields,
|
||||
AwsSecretsManagerSyncReviewFields
|
||||
} from "./AwsSecretsManagerSyncReviewFields";
|
||||
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
|
||||
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
|
||||
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
|
||||
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
|
||||
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
|
||||
|
||||
@ -19,6 +25,7 @@ export const SecretSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm>();
|
||||
|
||||
let DestinationFieldsComponent: ReactNode;
|
||||
let AdditionalSyncOptionsFieldsComponent: ReactNode;
|
||||
|
||||
const {
|
||||
name,
|
||||
@ -38,10 +45,12 @@ export const SecretSyncReviewFields = () => {
|
||||
|
||||
switch (destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
DestinationFieldsComponent = <AwsParameterStoreSyncReviewFields />;
|
||||
DestinationFieldsComponent = <AwsParameterStoreDestinationReviewFields />;
|
||||
AdditionalSyncOptionsFieldsComponent = <AwsParameterStoreSyncOptionsReviewFields />;
|
||||
break;
|
||||
case SecretSync.AWSSecretsManager:
|
||||
DestinationFieldsComponent = <AwsSecretsManagerSyncReviewFields />;
|
||||
AdditionalSyncOptionsFieldsComponent = <AwsSecretsManagerSyncOptionsReviewFields />;
|
||||
break;
|
||||
case SecretSync.GitHub:
|
||||
DestinationFieldsComponent = <GitHubSyncReviewFields />;
|
||||
@ -84,7 +93,7 @@ export const SecretSyncReviewFields = () => {
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Options</span>
|
||||
<span className="text-sm text-mineshaft-300">Sync Options</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<SecretSyncLabel label="Auto-Sync">
|
||||
@ -97,6 +106,7 @@ export const SecretSyncReviewFields = () => {
|
||||
</SecretSyncLabel>
|
||||
{/* <SecretSyncLabel label="Prepend Prefix">{prependPrefix}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Append Suffix">{appendSuffix}</SecretSyncLabel> */}
|
||||
{AdditionalSyncOptionsFieldsComponent}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
|
@ -1,16 +1,45 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const AwsParameterStoreSyncDestinationSchema = z.object({
|
||||
destination: z.literal(SecretSync.AWSParameterStore),
|
||||
destinationConfig: z.object({
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Parameter Store Path required")
|
||||
.max(2048, "Cannot exceed 2048 characters")
|
||||
.regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format'),
|
||||
region: z.string().min(1, "Region required")
|
||||
export const AwsParameterStoreSyncDestinationSchema = BaseSecretSyncSchema(
|
||||
z.object({
|
||||
keyId: z.string().optional(),
|
||||
tags: z
|
||||
.object({
|
||||
key: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.min(1, "Key required")
|
||||
.max(128, "Tag key cannot exceed 128 characters"),
|
||||
value: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.max(256, "Tag value cannot exceed 256 characters")
|
||||
})
|
||||
.array()
|
||||
.max(50)
|
||||
.optional(),
|
||||
syncSecretMetadataAsTags: z.boolean().optional()
|
||||
})
|
||||
});
|
||||
).merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.AWSParameterStore),
|
||||
destinationConfig: z.object({
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Parameter Store Path required")
|
||||
.max(2048, "Cannot exceed 2048 characters")
|
||||
.regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format'),
|
||||
region: z.string().min(1, "Region required")
|
||||
})
|
||||
})
|
||||
);
|
||||
|
@ -1,30 +1,59 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
||||
|
||||
export const AwsSecretsManagerSyncDestinationSchema = z.object({
|
||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||
destinationConfig: z
|
||||
.discriminatedUnion("mappingBehavior", [
|
||||
z.object({
|
||||
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne)
|
||||
}),
|
||||
z.object({
|
||||
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne),
|
||||
secretName: z
|
||||
export const AwsSecretsManagerSyncDestinationSchema = BaseSecretSyncSchema(
|
||||
z.object({
|
||||
keyId: z.string().optional(),
|
||||
tags: z
|
||||
.object({
|
||||
key: z
|
||||
.string()
|
||||
.regex(
|
||||
/^[a-zA-Z0-9/_+=.@-]+$/,
|
||||
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.min(1, "Secret name is required")
|
||||
.max(256, "Secret name cannot exceed 256 characters")
|
||||
.min(1, "Key required")
|
||||
.max(128, "Tag key cannot exceed 128 characters"),
|
||||
value: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.max(256, "Tag value cannot exceed 256 characters")
|
||||
})
|
||||
])
|
||||
.and(
|
||||
z.object({
|
||||
region: z.string().min(1, "Region required")
|
||||
})
|
||||
)
|
||||
});
|
||||
.array()
|
||||
.max(50)
|
||||
.optional(),
|
||||
syncSecretMetadataAsTags: z.boolean().optional()
|
||||
})
|
||||
).merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||
destinationConfig: z
|
||||
.discriminatedUnion("mappingBehavior", [
|
||||
z.object({
|
||||
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne)
|
||||
}),
|
||||
z.object({
|
||||
mappingBehavior: z.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne),
|
||||
secretName: z
|
||||
.string()
|
||||
.regex(
|
||||
/^[a-zA-Z0-9/_+=.@-]+$/,
|
||||
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
|
||||
)
|
||||
.min(1, "Secret name is required")
|
||||
.max(256, "Secret name cannot exceed 256 characters")
|
||||
})
|
||||
])
|
||||
.and(
|
||||
z.object({
|
||||
region: z.string().min(1, "Region required")
|
||||
})
|
||||
)
|
||||
})
|
||||
);
|
||||
|
@ -1,19 +1,22 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const AzureAppConfigurationSyncDestinationSchema = z.object({
|
||||
destination: z.literal(SecretSync.AzureAppConfiguration),
|
||||
destinationConfig: z.object({
|
||||
configurationUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Azure App Configuration URL is required" })
|
||||
.url()
|
||||
.refine(
|
||||
(val) => val.endsWith(".azconfig.io"),
|
||||
"URL should have the following format: https://resource-name-here.azconfig.io"
|
||||
),
|
||||
label: z.string().optional()
|
||||
export const AzureAppConfigurationSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.AzureAppConfiguration),
|
||||
destinationConfig: z.object({
|
||||
configurationUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Azure App Configuration URL is required" })
|
||||
.url()
|
||||
.refine(
|
||||
(val) => val.endsWith(".azconfig.io"),
|
||||
"URL should have the following format: https://resource-name-here.azconfig.io"
|
||||
),
|
||||
label: z.string().optional()
|
||||
})
|
||||
})
|
||||
});
|
||||
);
|
||||
|
@ -1,10 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const AzureKeyVaultSyncDestinationSchema = z.object({
|
||||
destination: z.literal(SecretSync.AzureKeyVault),
|
||||
destinationConfig: z.object({
|
||||
vaultBaseUrl: z.string().url("Invalid vault base URL format").min(1, "Vault base URL required")
|
||||
export const AzureKeyVaultSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.AzureKeyVault),
|
||||
destinationConfig: z.object({
|
||||
vaultBaseUrl: z
|
||||
.string()
|
||||
.url("Invalid vault base URL format")
|
||||
.min(1, "Vault base URL required")
|
||||
})
|
||||
})
|
||||
});
|
||||
);
|
||||
|
@ -0,0 +1,39 @@
|
||||
import { AnyZodObject, z } from "zod";
|
||||
|
||||
import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
export const BaseSecretSyncSchema = <T extends AnyZodObject | undefined = undefined>(
|
||||
additionalSyncOptions?: T
|
||||
) => {
|
||||
const baseSyncOptionsSchema = z.object({
|
||||
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior)
|
||||
// scott: removed temporarily for evaluation of template formatting
|
||||
// prependPrefix: z
|
||||
// .string()
|
||||
// .trim()
|
||||
// .transform((str) => str.toUpperCase())
|
||||
// .optional(),
|
||||
// appendSuffix: z
|
||||
// .string()
|
||||
// .trim()
|
||||
// .transform((str) => str.toUpperCase())
|
||||
// .optional()
|
||||
});
|
||||
|
||||
const syncOptionsSchema = additionalSyncOptions
|
||||
? baseSyncOptionsSchema.merge(additionalSyncOptions)
|
||||
: (baseSyncOptionsSchema as T extends AnyZodObject
|
||||
? z.ZodObject<z.objectUtil.MergeShapes<typeof baseSyncOptionsSchema.shape, T["shape"]>>
|
||||
: typeof baseSyncOptionsSchema);
|
||||
|
||||
return z.object({
|
||||
name: slugSchema({ field: "Name" }),
|
||||
description: z.string().trim().max(256, "Cannot exceed 256 characters").optional(),
|
||||
connection: z.object({ name: z.string(), id: z.string().uuid() }),
|
||||
environment: z.object({ slug: z.string(), id: z.string(), name: z.string() }),
|
||||
secretPath: z.string().min(1, "Secret path required"),
|
||||
syncOptions: syncOptionsSchema,
|
||||
isAutoSyncEnabled: z.boolean()
|
||||
});
|
||||
};
|
@ -1,10 +1,13 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const DatabricksSyncDestinationSchema = z.object({
|
||||
destination: z.literal(SecretSync.Databricks),
|
||||
destinationConfig: z.object({
|
||||
scope: z.string().trim().min(1, "Databricks scope required")
|
||||
export const DatabricksSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.Databricks),
|
||||
destinationConfig: z.object({
|
||||
scope: z.string().trim().min(1, "Databricks scope required")
|
||||
})
|
||||
})
|
||||
});
|
||||
);
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||
|
||||
export const GcpSyncDestinationSchema = z.object({
|
||||
destination: z.literal(SecretSync.GCPSecretManager),
|
||||
destinationConfig: z.object({
|
||||
scope: z.literal(GcpSyncScope.Global),
|
||||
projectId: z.string().min(1, "Project ID required")
|
||||
export const GcpSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.GCPSecretManager),
|
||||
destinationConfig: z.object({
|
||||
scope: z.literal(GcpSyncScope.Global),
|
||||
projectId: z.string().min(1, "Project ID required")
|
||||
})
|
||||
})
|
||||
});
|
||||
);
|
||||
|
@ -1,45 +1,48 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import {
|
||||
GitHubSyncScope,
|
||||
GitHubSyncVisibility
|
||||
} from "@app/hooks/api/secretSyncs/types/github-sync";
|
||||
|
||||
export const GitHubSyncDestinationSchema = z.object({
|
||||
destination: z.literal(SecretSync.GitHub),
|
||||
destinationConfig: z
|
||||
.discriminatedUnion("scope", [
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.Organization),
|
||||
org: z.string().min(1, "Organization name required"),
|
||||
visibility: z.nativeEnum(GitHubSyncVisibility),
|
||||
selectedRepositoryIds: z.number().array().optional()
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.Repository),
|
||||
owner: z.string().min(1, "Repository owner name required"),
|
||||
repo: z.string().min(1, "Repository name required")
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.RepositoryEnvironment),
|
||||
owner: z.string().min(1, "Repository owner name required"),
|
||||
repo: z.string().min(1, "Repository name required"),
|
||||
env: z.string().min(1, "Environment name required")
|
||||
})
|
||||
])
|
||||
.superRefine((options, ctx) => {
|
||||
if (options.scope === GitHubSyncScope.Organization) {
|
||||
if (
|
||||
options.visibility === GitHubSyncVisibility.Selected &&
|
||||
!options.selectedRepositoryIds?.length
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Select at least 1 repository",
|
||||
path: ["selectedRepositoryIds"]
|
||||
});
|
||||
export const GitHubSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.GitHub),
|
||||
destinationConfig: z
|
||||
.discriminatedUnion("scope", [
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.Organization),
|
||||
org: z.string().min(1, "Organization name required"),
|
||||
visibility: z.nativeEnum(GitHubSyncVisibility),
|
||||
selectedRepositoryIds: z.number().array().optional()
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.Repository),
|
||||
owner: z.string().min(1, "Repository owner name required"),
|
||||
repo: z.string().min(1, "Repository name required")
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.RepositoryEnvironment),
|
||||
owner: z.string().min(1, "Repository owner name required"),
|
||||
repo: z.string().min(1, "Repository name required"),
|
||||
env: z.string().min(1, "Environment name required")
|
||||
})
|
||||
])
|
||||
.superRefine((options, ctx) => {
|
||||
if (options.scope === GitHubSyncScope.Organization) {
|
||||
if (
|
||||
options.visibility === GitHubSyncVisibility.Selected &&
|
||||
!options.selectedRepositoryIds?.length
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Select at least 1 repository",
|
||||
path: ["selectedRepositoryIds"]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
})
|
||||
);
|
||||
|
@ -3,37 +3,12 @@ import { z } from "zod";
|
||||
import { AwsSecretsManagerSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/aws-secrets-manager-sync-destination-schema";
|
||||
import { DatabricksSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/databricks-sync-destination-schema";
|
||||
import { GitHubSyncDestinationSchema } from "@app/components/secret-syncs/forms/schemas/github-sync-destination-schema";
|
||||
import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
|
||||
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
|
||||
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
|
||||
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
|
||||
|
||||
const BaseSecretSyncSchema = z.object({
|
||||
name: slugSchema({ field: "Name" }),
|
||||
description: z.string().trim().max(256, "Cannot exceed 256 characters").optional(),
|
||||
connection: z.object({ name: z.string(), id: z.string().uuid() }),
|
||||
environment: z.object({ slug: z.string(), id: z.string(), name: z.string() }),
|
||||
secretPath: z.string().min(1, "Secret path required"),
|
||||
syncOptions: z.object({
|
||||
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior)
|
||||
// scott: removed temporarily for evaluation of template formatting
|
||||
// prependPrefix: z
|
||||
// .string()
|
||||
// .trim()
|
||||
// .transform((str) => str.toUpperCase())
|
||||
// .optional(),
|
||||
// appendSuffix: z
|
||||
// .string()
|
||||
// .trim()
|
||||
// .transform((str) => str.toUpperCase())
|
||||
// .optional()
|
||||
}),
|
||||
isAutoSyncEnabled: z.boolean()
|
||||
});
|
||||
|
||||
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
AwsParameterStoreSyncDestinationSchema,
|
||||
AwsSecretsManagerSyncDestinationSchema,
|
||||
@ -44,8 +19,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
DatabricksSyncDestinationSchema
|
||||
]);
|
||||
|
||||
export const SecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema);
|
||||
export const SecretSyncFormSchema = SecretSyncUnionSchema;
|
||||
|
||||
export const UpdateSecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema.partial());
|
||||
export const UpdateSecretSyncFormSchema = SecretSyncUnionSchema;
|
||||
|
||||
export type TSecretSyncForm = z.infer<typeof SecretSyncFormSchema>;
|
||||
|
2
frontend/src/hooks/api/appConnections/aws/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
42
frontend/src/hooks/api/appConnections/aws/queries.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { appConnectionKeys } from "@app/hooks/api/appConnections";
|
||||
|
||||
import {
|
||||
TAwsConnectionKmsKey,
|
||||
TAwsConnectionListKmsKeysResponse,
|
||||
TListAwsConnectionKmsKeys
|
||||
} from "./types";
|
||||
|
||||
const awsConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "aws"] as const,
|
||||
listKmsKeys: (params: TListAwsConnectionKmsKeys) =>
|
||||
[...awsConnectionKeys.all, "kms-keys", params] as const
|
||||
};
|
||||
|
||||
export const useListAwsConnectionKmsKeys = (
|
||||
{ connectionId, ...params }: TListAwsConnectionKmsKeys,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TAwsConnectionKmsKey[],
|
||||
unknown,
|
||||
TAwsConnectionKmsKey[],
|
||||
ReturnType<typeof awsConnectionKeys.listKmsKeys>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: awsConnectionKeys.listKmsKeys({ connectionId, ...params }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TAwsConnectionListKmsKeysResponse>(
|
||||
`/api/v1/app-connections/aws/${connectionId}/kms-keys`,
|
||||
{ params }
|
||||
);
|
||||
|
||||
return data.kmsKeys;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
16
frontend/src/hooks/api/appConnections/aws/types.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
region: string;
|
||||
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
|
||||
};
|
||||
|
||||
export type TAwsConnectionKmsKey = {
|
||||
alias: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TAwsConnectionListKmsKeysResponse = {
|
||||
kmsKeys: TAwsConnectionKmsKey[];
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||
import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||
|
||||
export type TAwsParameterStoreSync = TRootSecretSync & {
|
||||
destination: SecretSync.AWSParameterStore;
|
||||
@ -13,4 +13,9 @@ export type TAwsParameterStoreSync = TRootSecretSync & {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
syncOptions: RootSyncOptions & {
|
||||
keyId?: string;
|
||||
tags?: { key: string; value?: string }[];
|
||||
syncSecretMetadataAsTags?: boolean;
|
||||
};
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||
import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||
|
||||
export type TAwsSecretsManagerSync = TRootSecretSync & {
|
||||
destination: SecretSync.AWSSecretsManager;
|
||||
@ -19,6 +19,11 @@ export type TAwsSecretsManagerSync = TRootSecretSync & {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
syncOptions: RootSyncOptions & {
|
||||
keyId?: string;
|
||||
tags?: { key: string; value?: string }[];
|
||||
syncSecretMetadataAsTags?: boolean;
|
||||
};
|
||||
};
|
||||
export enum AwsSecretsManagerSyncMappingBehavior {
|
||||
OneToOne = "one-to-one",
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretSyncInitialSyncBehavior, SecretSyncStatus } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export type RootSyncOptions = {
|
||||
initialSyncBehavior: SecretSyncInitialSyncBehavior;
|
||||
// prependPrefix?: string;
|
||||
// appendSuffix?: string;
|
||||
};
|
||||
|
||||
export type TRootSecretSync = {
|
||||
id: string;
|
||||
name: string;
|
||||
@ -24,11 +30,7 @@ export type TRootSecretSync = {
|
||||
lastRemoveJobId: string | null;
|
||||
lastRemovedAt: Date | null;
|
||||
lastRemoveMessage: string | null;
|
||||
syncOptions: {
|
||||
initialSyncBehavior: SecretSyncInitialSyncBehavior;
|
||||
// prependPrefix?: string;
|
||||
// appendSuffix?: string;
|
||||
};
|
||||
syncOptions: RootSyncOptions;
|
||||
connection: {
|
||||
app: AppConnection;
|
||||
id: string;
|
||||
|
@ -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")) && (
|
||||
|
@ -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")) && (
|
||||
|
@ -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,
|
||||
});
|
@ -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,
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export { DedicatedInstancesPage } from './DedicatedInstancesPage';
|
||||
export { DedicatedInstanceDetailsPage } from './DedicatedInstanceDetailsPage';
|
@ -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,
|
||||
})
|
@ -19,7 +19,7 @@ export const SecretSyncsTab = () => {
|
||||
const { data: secretSyncs = [], isPending: isSecretSyncsPending } = useListSecretSyncs(
|
||||
currentWorkspace.id,
|
||||
{
|
||||
refetchInterval: 4000
|
||||
refetchInterval: 30000
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -37,7 +37,7 @@ const PageContent = () => {
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["editSync"] as const);
|
||||
|
||||
const { data: secretSync, isPending } = useGetSecretSync(destination, syncId, {
|
||||
refetchInterval: 4000
|
||||
refetchInterval: 30000
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
@ -66,7 +66,7 @@ const PageContent = () => {
|
||||
|
||||
const handleEditSource = () => handlePopUpOpen("editSync", SecretSyncEditFields.Source);
|
||||
|
||||
// const handleEditOptions = () => handlePopUpOpen("editSync", SecretSyncEditFields.Options);
|
||||
const handleEditOptions = () => handlePopUpOpen("editSync", SecretSyncEditFields.Options);
|
||||
|
||||
const handleEditDestination = () => handlePopUpOpen("editSync", SecretSyncEditFields.Destination);
|
||||
|
||||
@ -108,10 +108,7 @@ const PageContent = () => {
|
||||
<div className="mr-4 flex w-72 flex-col gap-4">
|
||||
<SecretSyncDetailsSection secretSync={secretSync} onEditDetails={handleEditDetails} />
|
||||
<SecretSyncSourceSection secretSync={secretSync} onEditSource={handleEditSource} />
|
||||
<SecretSyncOptionsSection
|
||||
secretSync={secretSync}
|
||||
// onEditOptions={handleEditOptions}
|
||||
/>
|
||||
<SecretSyncOptionsSection secretSync={secretSync} onEditOptions={handleEditOptions} />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-4">
|
||||
<SecretSyncDestinationSection
|
||||
|
@ -1,57 +0,0 @@
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP } from "@app/helpers/secretSyncs";
|
||||
import { TSecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
type Props = {
|
||||
secretSync: TSecretSync;
|
||||
// onEditOptions: VoidFunction;
|
||||
};
|
||||
|
||||
export const SecretSyncOptionsSection = ({
|
||||
secretSync
|
||||
// onEditOptions
|
||||
}: Props) => {
|
||||
const {
|
||||
destination,
|
||||
syncOptions: {
|
||||
// appendSuffix,
|
||||
// prependPrefix,
|
||||
initialSyncBehavior
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-semibold text-mineshaft-100">Sync Options</h3>
|
||||
{/* <ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.Edit}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="Edit sync options"
|
||||
onClick={onEditOptions}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan> */}
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-3">
|
||||
<SecretSyncLabel label="Initial Sync Behavior">
|
||||
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name}
|
||||
</SecretSyncLabel>
|
||||
{/* <SecretSyncLabel label="Prefix">{prependPrefix}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Suffix">{appendSuffix}</SecretSyncLabel> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,60 @@
|
||||
import { faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
|
||||
import { TAwsParameterStoreSync } from "@app/hooks/api/secretSyncs/types/aws-parameter-store-sync";
|
||||
|
||||
type Props = {
|
||||
secretSync: TAwsParameterStoreSync;
|
||||
};
|
||||
|
||||
export const AwsParameterStoreSyncOptionsSection = ({ secretSync }: Props) => {
|
||||
const {
|
||||
syncOptions: { keyId, tags, syncSecretMetadataAsTags }
|
||||
} = secretSync;
|
||||
|
||||
return (
|
||||
<>
|
||||
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
|
||||
{tags && tags.length > 0 && (
|
||||
<SecretSyncLabel label="Resource Tags">
|
||||
<Tooltip
|
||||
side="right"
|
||||
className="max-w-xl p-1"
|
||||
content={
|
||||
<Table>
|
||||
<THead>
|
||||
<Th className="whitespace-nowrap p-2">Key</Th>
|
||||
<Th className="p-2">Value</Th>
|
||||
</THead>
|
||||
<TBody>
|
||||
{tags.map((tag) => (
|
||||
<Tr key={tag.key}>
|
||||
<Td className="p-2">{tag.key}</Td>
|
||||
<Td className="p-2">{tag.value}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>
|
||||
{tags.length} Tag{tags.length > 1 ? "s" : ""}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</SecretSyncLabel>
|
||||
)}
|
||||
{syncSecretMetadataAsTags && (
|
||||
<SecretSyncLabel label="Sync Secret Metadata as Resource Tags">
|
||||
<Badge variant="success">Enabled</Badge>
|
||||
</SecretSyncLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,60 @@
|
||||
import { faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
|
||||
import { TAwsSecretsManagerSync } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
|
||||
|
||||
type Props = {
|
||||
secretSync: TAwsSecretsManagerSync;
|
||||
};
|
||||
|
||||
export const AwsSecretsManagerSyncOptionsSection = ({ secretSync }: Props) => {
|
||||
const {
|
||||
syncOptions: { keyId, tags, syncSecretMetadataAsTags }
|
||||
} = secretSync;
|
||||
|
||||
return (
|
||||
<>
|
||||
{keyId && <SecretSyncLabel label="KMS Key">{keyId}</SecretSyncLabel>}
|
||||
{tags && tags.length > 0 && (
|
||||
<SecretSyncLabel label="Tags">
|
||||
<Tooltip
|
||||
side="right"
|
||||
className="max-w-xl p-1"
|
||||
content={
|
||||
<Table>
|
||||
<THead>
|
||||
<Th className="whitespace-nowrap p-2">Key</Th>
|
||||
<Th className="p-2">Value</Th>
|
||||
</THead>
|
||||
<TBody>
|
||||
{tags.map((tag) => (
|
||||
<Tr key={tag.key}>
|
||||
<Td className="p-2">{tag.key}</Td>
|
||||
<Td className="p-2">{tag.value}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>
|
||||
{tags.length} Tag{tags.length > 1 ? "s" : ""}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</SecretSyncLabel>
|
||||
)}
|
||||
{syncSecretMetadataAsTags && (
|
||||
<SecretSyncLabel label="Sync Secret Metadata as Tags">
|
||||
<Badge variant="success">Enabled</Badge>
|
||||
</SecretSyncLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,92 @@
|
||||
import { ReactNode } from "react";
|
||||
import { faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { IconButton } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP } from "@app/helpers/secretSyncs";
|
||||
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { AwsParameterStoreSyncOptionsSection } from "./AwsParameterStoreSyncOptionsSection";
|
||||
import { AwsSecretsManagerSyncOptionsSection } from "./AwsSecretsManagerSyncOptionsSection";
|
||||
|
||||
type Props = {
|
||||
secretSync: TSecretSync;
|
||||
onEditOptions: VoidFunction;
|
||||
};
|
||||
|
||||
export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) => {
|
||||
const {
|
||||
destination,
|
||||
syncOptions: {
|
||||
// appendSuffix,
|
||||
// prependPrefix,
|
||||
initialSyncBehavior
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
let AdditionalSyncOptionsComponent: ReactNode;
|
||||
|
||||
switch (destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
AdditionalSyncOptionsComponent = (
|
||||
<AwsParameterStoreSyncOptionsSection secretSync={secretSync} />
|
||||
);
|
||||
break;
|
||||
case SecretSync.AWSSecretsManager:
|
||||
AdditionalSyncOptionsComponent = (
|
||||
<AwsSecretsManagerSyncOptionsSection secretSync={secretSync} />
|
||||
);
|
||||
break;
|
||||
case SecretSync.GitHub:
|
||||
case SecretSync.GCPSecretManager:
|
||||
case SecretSync.AzureKeyVault:
|
||||
case SecretSync.AzureAppConfiguration:
|
||||
case SecretSync.Databricks:
|
||||
AdditionalSyncOptionsComponent = null;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-semibold text-mineshaft-100">Sync Options</h3>
|
||||
{AdditionalSyncOptionsComponent && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.Edit}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="Edit sync options"
|
||||
onClick={onEditOptions}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-3">
|
||||
<SecretSyncLabel label="Initial Sync Behavior">
|
||||
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name}
|
||||
</SecretSyncLabel>
|
||||
{/* <SecretSyncLabel label="Prefix">{prependPrefix}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Suffix">{appendSuffix}</SecretSyncLabel> */}
|
||||
{AdditionalSyncOptionsComponent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from "./SecretSyncOptionsSection";
|
@ -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"
|
||||
|
@ -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"),
|
||||
|
27
frontend/src/routes/index.tsx
Normal 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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
});
|
@ -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,
|
||||
});
|
12
frontend/src/routes/organization/dedicated-instances.tsx
Normal 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
|
||||
});
|