mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-28 02:53:22 +00:00
Compare commits
2 Commits
daniel/gat
...
gcp-sync-l
Author | SHA1 | Date | |
---|---|---|---|
|
d57f76d230 | ||
|
f8939835e1 |
@@ -2272,7 +2272,8 @@ export const SecretSyncs = {
|
||||
},
|
||||
GCP: {
|
||||
scope: "The Google project scope that secrets should be synced to.",
|
||||
projectId: "The ID of the Google project secrets should be synced to."
|
||||
projectId: "The ID of the Google project secrets should be synced to.",
|
||||
locationId: 'The ID of the Google project location secrets should be synced to (ie "us-west4").'
|
||||
},
|
||||
DATABRICKS: {
|
||||
scope: "The Databricks secret scope that secrets should be synced to."
|
||||
|
@@ -45,4 +45,37 @@ export const registerGcpConnectionRouter = async (server: FastifyZodProvider) =>
|
||||
return projects;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/secret-manager-project-locations`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ displayName: z.string(), locationId: z.string() }).array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
params: { connectionId },
|
||||
query: { projectId }
|
||||
} = req;
|
||||
|
||||
const locations = await server.services.appConnection.gcp.listSecretManagerProjectLocations(
|
||||
{ connectionId, projectId },
|
||||
req.permission
|
||||
);
|
||||
|
||||
return locations;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -11,8 +11,10 @@ import { AppConnection } from "../app-connection-enums";
|
||||
import { GcpConnectionMethod } from "./gcp-connection-enums";
|
||||
import {
|
||||
GCPApp,
|
||||
GCPGetProjectLocationsRes,
|
||||
GCPGetProjectsRes,
|
||||
GCPGetServiceRes,
|
||||
GCPLocation,
|
||||
TGcpConnection,
|
||||
TGcpConnectionConfig
|
||||
} from "./gcp-connection-types";
|
||||
@@ -145,6 +147,45 @@ export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection)
|
||||
return projects;
|
||||
};
|
||||
|
||||
export const getGcpSecretManagerProjectLocations = async (projectId: string, appConnection: TGcpConnection) => {
|
||||
const accessToken = await getGcpConnectionAuthToken(appConnection);
|
||||
|
||||
let gcpLocations: GCPLocation[] = [];
|
||||
|
||||
const pageSize = 100;
|
||||
let pageToken: string | undefined;
|
||||
let hasMorePages = true;
|
||||
|
||||
while (hasMorePages) {
|
||||
const params = new URLSearchParams({
|
||||
pageSize: String(pageSize),
|
||||
...(pageToken ? { pageToken } : {})
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { data } = await request.get<GCPGetProjectLocationsRes>(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${projectId}/locations`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
gcpLocations = gcpLocations.concat(data.locations);
|
||||
|
||||
if (!data.nextPageToken) {
|
||||
hasMorePages = false;
|
||||
}
|
||||
|
||||
pageToken = data.nextPageToken;
|
||||
}
|
||||
|
||||
return gcpLocations.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||
};
|
||||
|
||||
export const validateGcpConnectionCredentials = async (appConnection: TGcpConnectionConfig) => {
|
||||
// Check if provided service account email suffix matches organization ID.
|
||||
// We do this to mitigate confused deputy attacks in multi-tenant instances
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { getGcpSecretManagerProjects } from "./gcp-connection-fns";
|
||||
import { TGcpConnection } from "./gcp-connection-types";
|
||||
import { getGcpSecretManagerProjectLocations, getGcpSecretManagerProjects } from "./gcp-connection-fns";
|
||||
import { TGcpConnection, TGetGCPProjectLocationsDTO } from "./gcp-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
@@ -23,7 +23,23 @@ export const gcpConnectionService = (getAppConnection: TGetAppConnectionFunc) =>
|
||||
}
|
||||
};
|
||||
|
||||
const listSecretManagerProjectLocations = async (
|
||||
{ connectionId, projectId }: TGetGCPProjectLocationsDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const appConnection = await getAppConnection(AppConnection.GCP, connectionId, actor);
|
||||
|
||||
try {
|
||||
const locations = await getGcpSecretManagerProjectLocations(projectId, appConnection);
|
||||
|
||||
return locations;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listSecretManagerProjects
|
||||
listSecretManagerProjects,
|
||||
listSecretManagerProjectLocations
|
||||
};
|
||||
};
|
||||
|
@@ -38,6 +38,22 @@ export type GCPGetProjectsRes = {
|
||||
nextPageToken?: string;
|
||||
};
|
||||
|
||||
export type GCPLocation = {
|
||||
name: string;
|
||||
locationId: string;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
export type GCPGetProjectLocationsRes = {
|
||||
locations: GCPLocation[];
|
||||
nextPageToken?: string;
|
||||
};
|
||||
|
||||
export type TGetGCPProjectLocationsDTO = {
|
||||
projectId: string;
|
||||
connectionId: string;
|
||||
};
|
||||
|
||||
export type GCPGetServiceRes = {
|
||||
name: string;
|
||||
parent: string;
|
||||
|
@@ -1,3 +1,63 @@
|
||||
export enum GcpSyncScope {
|
||||
Global = "global"
|
||||
Global = "global",
|
||||
Region = "region"
|
||||
}
|
||||
|
||||
export enum GCPSecretManagerLocation {
|
||||
// Asia Pacific
|
||||
ASIA_SOUTHEAST3 = "asia-southeast3", // Bangkok
|
||||
ASIA_SOUTH2 = "asia-south2", // Delhi
|
||||
ASIA_EAST2 = "asia-east2", // Hong Kong
|
||||
ASIA_SOUTHEAST2 = "asia-southeast2", // Jakarta
|
||||
AUSTRALIA_SOUTHEAST2 = "australia-southeast2", // Melbourne
|
||||
ASIA_SOUTH1 = "asia-south1", // Mumbai
|
||||
ASIA_NORTHEAST2 = "asia-northeast2", // Osaka
|
||||
ASIA_NORTHEAST3 = "asia-northeast3", // Seoul
|
||||
ASIA_SOUTHEAST1 = "asia-southeast1", // Singapore
|
||||
AUSTRALIA_SOUTHEAST1 = "australia-southeast1", // Sydney
|
||||
ASIA_EAST1 = "asia-east1", // Taiwan
|
||||
ASIA_NORTHEAST1 = "asia-northeast1", // Tokyo
|
||||
|
||||
// Europe
|
||||
EUROPE_WEST1 = "europe-west1", // Belgium
|
||||
EUROPE_WEST10 = "europe-west10", // Berlin
|
||||
EUROPE_NORTH1 = "europe-north1", // Finland
|
||||
EUROPE_NORTH2 = "europe-north2", // Stockholm
|
||||
EUROPE_WEST3 = "europe-west3", // Frankfurt
|
||||
EUROPE_WEST2 = "europe-west2", // London
|
||||
EUROPE_SOUTHWEST1 = "europe-southwest1", // Madrid
|
||||
EUROPE_WEST8 = "europe-west8", // Milan
|
||||
EUROPE_WEST4 = "europe-west4", // Netherlands
|
||||
EUROPE_WEST12 = "europe-west12", // Turin
|
||||
EUROPE_WEST9 = "europe-west9", // Paris
|
||||
EUROPE_CENTRAL2 = "europe-central2", // Warsaw
|
||||
EUROPE_WEST6 = "europe-west6", // Zurich
|
||||
|
||||
// North America
|
||||
US_CENTRAL1 = "us-central1", // Iowa
|
||||
US_WEST4 = "us-west4", // Las Vegas
|
||||
US_WEST2 = "us-west2", // Los Angeles
|
||||
NORTHAMERICA_SOUTH1 = "northamerica-south1", // Mexico
|
||||
NORTHAMERICA_NORTHEAST1 = "northamerica-northeast1", // Montréal
|
||||
US_EAST4 = "us-east4", // Northern Virginia
|
||||
US_CENTRAL2 = "us-central2", // Oklahoma
|
||||
US_WEST1 = "us-west1", // Oregon
|
||||
US_WEST3 = "us-west3", // Salt Lake City
|
||||
US_EAST1 = "us-east1", // South Carolina
|
||||
NORTHAMERICA_NORTHEAST2 = "northamerica-northeast2", // Toronto
|
||||
US_EAST5 = "us-east5", // Columbus
|
||||
US_SOUTH1 = "us-south1", // Dallas
|
||||
US_WEST8 = "us-west8", // Phoenix
|
||||
|
||||
// South America
|
||||
SOUTHAMERICA_EAST1 = "southamerica-east1", // São Paulo
|
||||
SOUTHAMERICA_WEST1 = "southamerica-west1", // Santiago
|
||||
|
||||
// Middle East
|
||||
ME_CENTRAL2 = "me-central2", // Dammam
|
||||
ME_CENTRAL1 = "me-central1", // Doha
|
||||
ME_WEST1 = "me-west1", // Tel Aviv
|
||||
|
||||
// Africa
|
||||
AFRICA_SOUTH1 = "africa-south1" // Johannesburg
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import { request } from "@app/lib/config/request";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { GcpSyncScope } from "@app/services/secret-sync/gcp/gcp-sync-enums";
|
||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||
|
||||
import { SecretSyncError } from "../secret-sync-errors";
|
||||
@@ -15,9 +16,17 @@ import {
|
||||
TGcpSyncWithCredentials
|
||||
} from "./gcp-sync-types";
|
||||
|
||||
const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => {
|
||||
const getProjectUrl = (secretSync: TGcpSyncWithCredentials) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
|
||||
if (destinationConfig.scope === GcpSyncScope.Global) {
|
||||
return `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}`;
|
||||
}
|
||||
|
||||
return `https://secretmanager.${destinationConfig.locationId}.rep.googleapis.com/v1/projects/${destinationConfig.projectId}/locations/${destinationConfig.locationId}`;
|
||||
};
|
||||
|
||||
const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => {
|
||||
let gcpSecrets: GCPSecret[] = [];
|
||||
|
||||
const pageSize = 100;
|
||||
@@ -31,16 +40,13 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { data: secretsRes } = await request.get<GCPSMListSecretsRes>(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${secretSync.destinationConfig.projectId}/secrets`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
const { data: secretsRes } = await request.get<GCPSMListSecretsRes>(`${getProjectUrl(secretSync)}/secrets`, {
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (secretsRes.secrets) {
|
||||
gcpSecrets = gcpSecrets.concat(secretsRes.secrets);
|
||||
@@ -61,7 +67,7 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden
|
||||
|
||||
try {
|
||||
const { data: secretLatest } = await request.get<GCPLatestSecretVersionAccess>(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}/versions/latest:access`,
|
||||
`${getProjectUrl(secretSync)}/secrets/${key}/versions/latest:access`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
@@ -113,11 +119,14 @@ export const GcpSyncFns = {
|
||||
if (!(key in gcpSecrets)) {
|
||||
// case: create secret
|
||||
await request.post(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets`,
|
||||
`${getProjectUrl(secretSync)}/secrets`,
|
||||
{
|
||||
replication: {
|
||||
automatic: {}
|
||||
}
|
||||
replication:
|
||||
destinationConfig.scope === GcpSyncScope.Global
|
||||
? {
|
||||
automatic: {}
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
{
|
||||
params: {
|
||||
@@ -131,7 +140,7 @@ export const GcpSyncFns = {
|
||||
);
|
||||
|
||||
await request.post(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
|
||||
`${getProjectUrl(secretSync)}/secrets/${key}:addVersion`,
|
||||
{
|
||||
payload: {
|
||||
data: Buffer.from(secretMap[key].value).toString("base64")
|
||||
@@ -163,15 +172,12 @@ export const GcpSyncFns = {
|
||||
if (secretSync.syncOptions.disableSecretDeletion) continue;
|
||||
|
||||
// case: delete secret
|
||||
await request.delete(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
await request.delete(`${getProjectUrl(secretSync)}/secrets/${key}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
);
|
||||
});
|
||||
} else if (secretMap[key].value !== gcpSecrets[key]) {
|
||||
if (!secretMap[key].value) {
|
||||
logger.warn(
|
||||
@@ -180,7 +186,7 @@ export const GcpSyncFns = {
|
||||
}
|
||||
|
||||
await request.post(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
|
||||
`${getProjectUrl(secretSync)}/secrets/${key}:addVersion`,
|
||||
{
|
||||
payload: {
|
||||
data: Buffer.from(secretMap[key].value).toString("base64")
|
||||
@@ -212,21 +218,18 @@ export const GcpSyncFns = {
|
||||
},
|
||||
|
||||
removeSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig, connection } = secretSync;
|
||||
const { connection } = secretSync;
|
||||
const accessToken = await getGcpConnectionAuthToken(connection);
|
||||
|
||||
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
|
||||
for await (const [key] of Object.entries(gcpSecrets)) {
|
||||
if (key in secretMap) {
|
||||
await request.delete(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
await request.delete(`${getProjectUrl(secretSync)}/secrets/${key}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,14 +10,33 @@ import {
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { SecretSync } from "../secret-sync-enums";
|
||||
import { GcpSyncScope } from "./gcp-sync-enums";
|
||||
import { GCPSecretManagerLocation, GcpSyncScope } from "./gcp-sync-enums";
|
||||
|
||||
const GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
const GcpSyncDestinationConfigSchema = z.object({
|
||||
scope: z.literal(GcpSyncScope.Global).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope),
|
||||
projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId)
|
||||
});
|
||||
const GcpSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
|
||||
z
|
||||
.object({
|
||||
scope: z.literal(GcpSyncScope.Global).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope),
|
||||
projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId)
|
||||
})
|
||||
.describe(
|
||||
JSON.stringify({
|
||||
title: "Global"
|
||||
})
|
||||
),
|
||||
z
|
||||
.object({
|
||||
scope: z.literal(GcpSyncScope.Region).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope),
|
||||
projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId),
|
||||
locationId: z.nativeEnum(GCPSecretManagerLocation).describe(SecretSyncs.DESTINATION_CONFIG.GCP.locationId)
|
||||
})
|
||||
.describe(
|
||||
JSON.stringify({
|
||||
title: "Region"
|
||||
})
|
||||
)
|
||||
]);
|
||||
|
||||
export const GcpSyncSchema = BaseSecretSyncSchema(SecretSync.GCPSecretManager, GcpSyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.GCPSecretManager),
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 624 KiB After Width: | Height: | Size: 793 KiB |
@@ -34,6 +34,9 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
|
||||
|
||||
- **GCP Connection**: The GCP Connection to authenticate with.
|
||||
- **Project**: The GCP project to sync with.
|
||||
- **Scope**: The GCP project scope that secrets should be synced to:
|
||||
- **Global**: Secrets will be synced globally; available to all project regions.
|
||||
- **Region**: Secrets will be synced to the specified region.
|
||||
|
||||
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
|
||||

|
||||
|
@@ -5,27 +5,56 @@ import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
|
||||
import { useGcpConnectionListProjects } from "@app/hooks/api/appConnections/gcp/queries";
|
||||
import { TGitHubConnectionEnvironment } from "@app/hooks/api/appConnections/github";
|
||||
import {
|
||||
Badge,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { GCP_SYNC_SCOPES } from "@app/helpers/secretSyncs";
|
||||
import {
|
||||
useGcpConnectionListProjectLocations,
|
||||
useGcpConnectionListProjects
|
||||
} from "@app/hooks/api/appConnections/gcp/queries";
|
||||
import { TGcpLocation, TGcpProject } from "@app/hooks/api/appConnections/gcp/types";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
const formatOptionLabel = ({ displayName, locationId }: TGcpLocation) => (
|
||||
<div className="flex w-full flex-row items-center gap-1">
|
||||
<span>{displayName}</span>{" "}
|
||||
<Badge className="h-5 leading-5" variant="success">
|
||||
{locationId}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const GcpSyncFields = () => {
|
||||
const { control, setValue } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.GCPSecretManager }
|
||||
>();
|
||||
|
||||
const connectionId = useWatch({ name: "connection.id", control });
|
||||
const projectId = useWatch({ name: "destinationConfig.projectId", control });
|
||||
const selectedScope = useWatch({ name: "destinationConfig.scope", control });
|
||||
|
||||
const { data: projects, isPending } = useGcpConnectionListProjects(connectionId, {
|
||||
enabled: Boolean(connectionId)
|
||||
});
|
||||
|
||||
const { data: locations, isPending: areLocationsPending } = useGcpConnectionListProjectLocations(
|
||||
{ connectionId, projectId },
|
||||
{
|
||||
enabled: Boolean(connectionId) && Boolean(projectId)
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setValue("destinationConfig.scope", GcpSyncScope.Global);
|
||||
if (!selectedScope) setValue("destinationConfig.scope", GcpSyncScope.Global);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -33,6 +62,7 @@ export const GcpSyncFields = () => {
|
||||
<SecretSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.projectId", "");
|
||||
setValue("destinationConfig.locationId", "");
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
@@ -60,9 +90,10 @@ export const GcpSyncFields = () => {
|
||||
isLoading={isPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={projects?.find((project) => project.id === value) ?? null}
|
||||
onChange={(option) =>
|
||||
onChange((option as SingleValue<TGitHubConnectionEnvironment>)?.id ?? null)
|
||||
}
|
||||
onChange={(option) => {
|
||||
setValue("destinationConfig.locationId", "");
|
||||
onChange((option as SingleValue<TGcpProject>)?.id ?? null);
|
||||
}}
|
||||
options={projects}
|
||||
placeholder="Select a GCP project..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
@@ -71,6 +102,76 @@ export const GcpSyncFields = () => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.scope"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
tooltipText={
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
Specify how Infisical should sync secrets to GCP. The following options are
|
||||
available:
|
||||
</p>
|
||||
<ul className="flex list-disc flex-col gap-3 pl-4">
|
||||
{Object.values(GCP_SYNC_SCOPES).map(({ name, description }) => {
|
||||
return (
|
||||
<li key={name}>
|
||||
<p className="text-mineshaft-300">
|
||||
<span className="font-medium text-bunker-200">{name}</span>: {description}
|
||||
</p>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
tooltipClassName="max-w-lg"
|
||||
label="Scope"
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
isDisabled={!projectId}
|
||||
>
|
||||
{Object.values(GcpSyncScope).map((scope) => {
|
||||
return (
|
||||
<SelectItem className="capitalize" value={scope} key={scope}>
|
||||
{scope}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{selectedScope === GcpSyncScope.Region && (
|
||||
<Controller
|
||||
name="destinationConfig.locationId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isLoading={areLocationsPending && Boolean(projectId)}
|
||||
isDisabled={!projectId}
|
||||
value={locations?.find((option) => option.locationId === value) ?? null}
|
||||
onChange={(option) =>
|
||||
onChange((option as SingleValue<TGcpLocation>)?.locationId ?? null)
|
||||
}
|
||||
options={locations}
|
||||
placeholder="Select a region..."
|
||||
getOptionValue={(option) => option.locationId}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -3,12 +3,23 @@ import { useFormContext } from "react-hook-form";
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||
|
||||
export const GcpSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.GCPSecretManager }
|
||||
>();
|
||||
const projectId = watch("destinationConfig.projectId");
|
||||
const destinationConfig = watch("destinationConfig");
|
||||
|
||||
return <GenericFieldLabel label="Project ID">{projectId}</GenericFieldLabel>;
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Project ID">{destinationConfig.projectId}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Scope" className="capitalize">
|
||||
{destinationConfig.scope}
|
||||
</GenericFieldLabel>
|
||||
{destinationConfig.scope === GcpSyncScope.Region && (
|
||||
<GenericFieldLabel label="Region">{destinationConfig.locationId}</GenericFieldLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -7,9 +7,16 @@ import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||
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")
|
||||
})
|
||||
destinationConfig: z.discriminatedUnion("scope", [
|
||||
z.object({
|
||||
scope: z.literal(GcpSyncScope.Global),
|
||||
projectId: z.string().min(1, "Project ID required")
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(GcpSyncScope.Region),
|
||||
projectId: z.string().min(1, "Project ID required"),
|
||||
locationId: z.string().min(1, "Region required")
|
||||
})
|
||||
])
|
||||
})
|
||||
);
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
SecretSyncImportBehavior,
|
||||
SecretSyncInitialSyncBehavior
|
||||
} from "@app/hooks/api/secretSyncs";
|
||||
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||
import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
|
||||
|
||||
export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = {
|
||||
@@ -124,3 +125,14 @@ export const HUMANITEC_SYNC_SCOPES: Record<
|
||||
"Infisical will sync secrets as environment level shared values to the specified Humanitec application environment."
|
||||
}
|
||||
};
|
||||
|
||||
export const GCP_SYNC_SCOPES: Record<GcpSyncScope, { name: string; description: string }> = {
|
||||
[GcpSyncScope.Global]: {
|
||||
name: "Global",
|
||||
description: "Secrets will be synced globally; being available in all project regions."
|
||||
},
|
||||
[GcpSyncScope.Region]: {
|
||||
name: "Region",
|
||||
description: "Secrets will be synced to the specified region."
|
||||
}
|
||||
};
|
||||
|
@@ -3,12 +3,14 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { appConnectionKeys } from "../queries";
|
||||
import { TGcpProject } from "./types";
|
||||
import { TGcpLocation, TGcpProject, TListProjectLocations } from "./types";
|
||||
|
||||
const gcpConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "gcp"] as const,
|
||||
listProjects: (connectionId: string) =>
|
||||
[...gcpConnectionKeys.all, "projects", connectionId] as const
|
||||
[...gcpConnectionKeys.all, "projects", connectionId] as const,
|
||||
listProjectLocations: ({ projectId, connectionId }: TListProjectLocations) =>
|
||||
[...gcpConnectionKeys.all, "project-locations", connectionId, projectId] as const
|
||||
};
|
||||
|
||||
export const useGcpConnectionListProjects = (
|
||||
@@ -35,3 +37,29 @@ export const useGcpConnectionListProjects = (
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useGcpConnectionListProjectLocations = (
|
||||
{ connectionId, projectId }: TListProjectLocations,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TGcpLocation[],
|
||||
unknown,
|
||||
TGcpLocation[],
|
||||
ReturnType<typeof gcpConnectionKeys.listProjectLocations>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: gcpConnectionKeys.listProjectLocations({ connectionId, projectId }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TGcpLocation[]>(
|
||||
`/api/v1/app-connections/gcp/${connectionId}/secret-manager-project-locations`,
|
||||
{ params: { projectId } }
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
@@ -2,3 +2,13 @@ export type TGcpProject = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TListProjectLocations = {
|
||||
connectionId: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TGcpLocation = {
|
||||
displayName: string;
|
||||
locationId: string;
|
||||
};
|
||||
|
@@ -3,15 +3,22 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||
|
||||
export enum GcpSyncScope {
|
||||
Global = "global"
|
||||
Global = "global",
|
||||
Region = "region"
|
||||
}
|
||||
|
||||
export type TGcpSync = TRootSecretSync & {
|
||||
destination: SecretSync.GCPSecretManager;
|
||||
destinationConfig: {
|
||||
scope: GcpSyncScope.Global;
|
||||
projectId: string;
|
||||
};
|
||||
destinationConfig:
|
||||
| {
|
||||
scope: GcpSyncScope.Global;
|
||||
projectId: string;
|
||||
}
|
||||
| {
|
||||
scope: GcpSyncScope.Region;
|
||||
projectId: string;
|
||||
locationId: string;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection.GCP;
|
||||
name: string;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { TerraformCloudSyncScope } from "@app/hooks/api/appConnections/terraform-cloud";
|
||||
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||
import {
|
||||
GitHubSyncScope,
|
||||
GitHubSyncVisibility
|
||||
@@ -47,7 +48,8 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
|
||||
break;
|
||||
case SecretSync.GCPSecretManager:
|
||||
primaryText = destinationConfig.projectId;
|
||||
secondaryText = "Global";
|
||||
secondaryText =
|
||||
destinationConfig.scope === GcpSyncScope.Global ? "Global" : destinationConfig.locationId;
|
||||
break;
|
||||
case SecretSync.AzureKeyVault:
|
||||
primaryText = destinationConfig.vaultBaseUrl;
|
||||
|
@@ -1,14 +1,22 @@
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TGcpSync } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||
import { GcpSyncScope, TGcpSync } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||
|
||||
type Props = {
|
||||
secretSync: TGcpSync;
|
||||
};
|
||||
|
||||
export const GcpSyncDestinationSection = ({ secretSync }: Props) => {
|
||||
const {
|
||||
destinationConfig: { projectId }
|
||||
} = secretSync;
|
||||
const { destinationConfig } = secretSync;
|
||||
|
||||
return <GenericFieldLabel label="Project ID">{projectId}</GenericFieldLabel>;
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Project ID">{destinationConfig.projectId}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Scope" className="capitalize">
|
||||
{destinationConfig.scope}
|
||||
</GenericFieldLabel>
|
||||
{destinationConfig.scope === GcpSyncScope.Region && (
|
||||
<GenericFieldLabel label="Region">{destinationConfig.locationId}</GenericFieldLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
Reference in New Issue
Block a user