Compare commits

...

2 Commits

Author SHA1 Message Date
Scott Wilson
d57f76d230 improvements: address feedback 2025-06-06 13:22:45 -07:00
Scott Wilson
f8939835e1 feature(gcp-sync): add support for syncing to locations 2025-06-05 13:02:05 -07:00
19 changed files with 448 additions and 70 deletions

View File

@@ -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."

View File

@@ -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;
}
});
};

View File

@@ -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

View File

@@ -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
};
};

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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"
}
);
});
}
}
}

View File

@@ -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

View File

@@ -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**.
![Configure Options](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-options.png)

View File

@@ -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>
)}
/>
)}
</>
);
};

View File

@@ -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>
)}
</>
);
};

View File

@@ -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")
})
])
})
);

View File

@@ -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."
}
};

View File

@@ -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
});
};

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
)}
</>
);
};