feat: gcp secret sync management

This commit is contained in:
Sheen Capadngan
2025-01-25 03:01:10 +08:00
parent 58f51411c0
commit 753c28a2d3
34 changed files with 467 additions and 22 deletions

View File

@@ -0,0 +1,51 @@
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { useGcpConnectionListProjects } from "@app/hooks/api/appConnections/gcp/queries";
import { TGitHubConnectionEnvironment } from "@app/hooks/api/appConnections/github";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const GcpSyncFields = () => {
const { control, setValue } = useFormContext<TSecretSyncForm & { destination: SecretSync.GCP }>();
const connectionId = useWatch({ name: "connection.id", control });
const { data: projects, isPending } = useGcpConnectionListProjects(connectionId, {
enabled: Boolean(connectionId)
});
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.projectId", "");
}}
/>
<Controller
name="destinationConfig.projectId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Project">
<FilterableSelect
menuPlacement="top"
isLoading={isPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={projects?.find((project) => project.id === value) ?? null}
onChange={(option) =>
onChange((option as SingleValue<TGitHubConnectionEnvironment>)?.id ?? null)
}
options={projects}
placeholder="Select a GCP project..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id.toString()}
/>
</FormControl>
)}
/>
</>
);
};

View File

@@ -4,6 +4,7 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields";
import { GcpSyncFields } from "./GcpSyncFields";
import { GitHubSyncFields } from "./GitHubSyncFields";
export const SecretSyncDestinationFields = () => {
@@ -16,6 +17,8 @@ export const SecretSyncDestinationFields = () => {
return <AwsParameterStoreSyncFields />;
case SecretSync.GitHub:
return <GitHubSyncFields />;
case SecretSync.GCP:
return <GcpSyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}

View File

@@ -0,0 +1,12 @@
import { useFormContext } from "react-hook-form";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const GcpSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.GCP }>();
const projectId = watch("destinationConfig.projectId");
return <SecretSyncLabel label="Project ID">{projectId}</SecretSyncLabel>;
};

View File

@@ -8,6 +8,7 @@ import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/hel
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields";
import { GcpSyncReviewFields } from "./GcpSyncReviewFIelds";
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
export const SecretSyncReviewFields = () => {
@@ -38,6 +39,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.GitHub:
DestinationFieldsComponent = <GitHubSyncReviewFields />;
break;
case SecretSync.GCP:
DestinationFieldsComponent = <GcpSyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}

View File

@@ -0,0 +1,10 @@
import { z } from "zod";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const GcpSyncDestinationSchema = z.object({
destination: z.literal(SecretSync.GCP),
destinationConfig: z.object({
projectId: z.string().min(1, "Project ID required")
})
});

View File

@@ -5,6 +5,7 @@ import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs";
import { slugSchema } from "@app/lib/schemas";
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
const BaseSecretSyncSchema = z.object({
name: slugSchema({ field: "Name" }),
@@ -31,7 +32,8 @@ const BaseSecretSyncSchema = z.object({
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncDestinationSchema,
GitHubSyncDestinationSchema
GitHubSyncDestinationSchema,
GcpSyncDestinationSchema
]);
export const SecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema);

View File

@@ -7,12 +7,14 @@ import {
export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = {
[SecretSync.AWSParameterStore]: { name: "Parameter Store", image: "Amazon Web Services.png" },
[SecretSync.GitHub]: { name: "GitHub", image: "GitHub.png" }
[SecretSync.GitHub]: { name: "GitHub", image: "GitHub.png" },
[SecretSync.GCP]: { name: "GCP Secret Manager", image: "Google Cloud Platform.png" }
};
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.AWSParameterStore]: AppConnection.AWS,
[SecretSync.GitHub]: AppConnection.GitHub
[SecretSync.GitHub]: AppConnection.GitHub,
[SecretSync.GCP]: AppConnection.GCP
};
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<

View File

@@ -0,0 +1,37 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries";
import { TGcpProject } from "./types";
const gcpConnectionKeys = {
all: [...appConnectionKeys.all, "gcp"] as const,
listProjects: (connectionId: string) =>
[...gcpConnectionKeys.all, "projects", connectionId] as const
};
export const useGcpConnectionListProjects = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TGcpProject[],
unknown,
TGcpProject[],
ReturnType<typeof gcpConnectionKeys.listProjects>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: gcpConnectionKeys.listProjects(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TGcpProject[]>(
`/api/v1/app-connections/gcp/${connectionId}/secret-manager-projects`
);
return data;
},
...options
});
};

View File

@@ -0,0 +1,4 @@
export type TGcpProject = {
id: string;
name: string;
};

View File

@@ -1,6 +1,7 @@
export enum SecretSync {
AWSParameterStore = "aws-parameter-store",
GitHub = "github"
GitHub = "github",
GCP = "gcp"
}
export enum SecretSyncStatus {

View File

@@ -0,0 +1,15 @@
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";
export type TGcpSync = TRootSecretSync & {
destination: SecretSync.GCP;
destinationConfig: {
projectId: string;
};
connection: {
app: AppConnection.GCP;
name: string;
id: string;
};
};

View File

@@ -3,13 +3,15 @@ import { TAwsParameterStoreSync } from "@app/hooks/api/secretSyncs/types/aws-par
import { TGitHubSync } from "@app/hooks/api/secretSyncs/types/github-sync";
import { DiscriminativePick } from "@app/types";
import { TGcpSync } from "./gcp-sync";
export type TSecretSyncOption = {
name: string;
destination: SecretSync;
canImportSecrets: boolean;
};
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync;
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };

View File

@@ -0,0 +1,14 @@
import { TGcpSync } from "@app/hooks/api/secretSyncs/types/gcp-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TGcpSync;
};
export const GcpSyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};

View File

@@ -1,6 +1,7 @@
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
import { AwsParameterStoreSyncDestinationCol } from "./AwsParameterStoreSyncDestinationCol";
import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol";
import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
type Props = {
@@ -13,6 +14,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <AwsParameterStoreSyncDestinationCol secretSync={secretSync} />;
case SecretSync.GitHub:
return <GitHubSyncDestinationCol secretSync={secretSync} />;
case SecretSync.GCP:
return <GcpSyncDestinationCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`

View File

@@ -39,6 +39,9 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
throw new Error(`Unhandled GitHub Scope Destination Col Values ${destination}`);
}
break;
case SecretSync.GCP:
primaryText = destinationConfig.projectId;
break;
default:
throw new Error(`Unhandled Destination Col Values ${destination}`);
}

View File

@@ -0,0 +1,14 @@
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TGcpSync } from "@app/hooks/api/secretSyncs/types/gcp-sync";
type Props = {
secretSync: TGcpSync;
};
export const GcpSyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: { projectId }
} = secretSync;
return <SecretSyncLabel label="Project ID">{projectId}</SecretSyncLabel>;
};

View File

@@ -12,6 +12,8 @@ import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
import { AwsParameterStoreSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsParameterStoreSyncDestinationSection";
import { GitHubSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GitHubSyncDestinationSection";
import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection";
type Props = {
secretSync: TSecretSync;
onEditDestination: VoidFunction;
@@ -30,6 +32,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.GitHub:
DestinationComponents = <GitHubSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.GCP:
DestinationComponents = <GcpSyncDestinationSection secretSync={secretSync} />;
break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}