Finish integration options/react form refactor for GitLab and GCP SM integrations, add docs for it

This commit is contained in:
Tuan Dang
2023-09-15 20:53:31 +01:00
parent 954f15e4df
commit 3ab5db9b2a
15 changed files with 682 additions and 371 deletions

View File

@@ -328,15 +328,19 @@ const syncSecretsGCPSecretManager = async ({
const pageSize = 100;
let pageToken: string | undefined;
let hasMorePages = true;
const filterParam = integration.metadata.secretGCPLabel
? `?filter=labels.${integration.metadata.secretGCPLabel.labelName}=${integration.metadata.secretGCPLabel.labelValue}`
: "";
while (hasMorePages) {
const params = new URLSearchParams({
pageSize: String(pageSize),
...(pageToken ? { pageToken } : {})
});
const res: GCPSMListSecretsRes = (await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets?filter=labels.managed-by=infisical`,
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`,
{
params,
headers: {
@@ -347,7 +351,24 @@ const syncSecretsGCPSecretManager = async ({
)).data;
if (res.secrets) {
gcpSecrets = gcpSecrets.concat(res.secrets);
const filteredSecrets = res.secrets?.filter((gcpSecret) => {
const arr = gcpSecret.name.split("/");
const key = arr[arr.length - 1];
let isValid = true;
if (integration.metadata.secretPrefix && !key.startsWith(integration.metadata.secretPrefix)) {
isValid = false;
}
if (integration.metadata.secretSuffix && !key.endsWith(integration.metadata.secretSuffix)) {
isValid = false;
}
return isValid;
});
gcpSecrets = gcpSecrets.concat(filteredSecrets);
}
if (!res.nextPageToken) {
@@ -371,7 +392,7 @@ const syncSecretsGCPSecretManager = async ({
const key = arr[arr.length - 1];
const secretLatest: GCPLatestSecretVersionAccess = (await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}/versions/latest:access`,
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
@@ -379,6 +400,7 @@ const syncSecretsGCPSecretManager = async ({
}
}
)).data;
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
}
@@ -387,14 +409,16 @@ const syncSecretsGCPSecretManager = async ({
if (!(key in res)) {
// case: create secret
await standardRequest.post(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets`,
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets`,
{
replication: {
automatic: {}
},
labels: {
"managed-by": "infisical"
}
...(integration.metadata.secretGCPLabel ? {
labels: {
[integration.metadata.secretGCPLabel.labelName]: integration.metadata.secretGCPLabel.labelValue
}
} : {})
},
{
params: {
@@ -408,7 +432,7 @@ const syncSecretsGCPSecretManager = async ({
);
await standardRequest.post(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}:addVersion`,
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
{
payload: {
data: Buffer.from(secrets[key].value).toString("base64")
@@ -428,7 +452,7 @@ const syncSecretsGCPSecretManager = async ({
if (!(key in secrets)) {
// case: delete secret
await standardRequest.delete(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}`,
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
@@ -440,7 +464,7 @@ const syncSecretsGCPSecretManager = async ({
// case: update secret
if (secrets[key].value !== res[key]) {
await standardRequest.post(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}:addVersion`,
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
{
payload: {
data: Buffer.from(secrets[key].value).toString("base64")
@@ -1863,10 +1887,24 @@ const syncSecretsGitLab = async ({
};
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
const getSecretsRes: GitLabSecret[] = allEnvVariables.filter(
(secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment
);
const getSecretsRes: GitLabSecret[] = allEnvVariables
.filter(
(secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment
)
.filter((gitLabSecret) => {
let isValid = true;
if (integration.metadata.secretPrefix && !gitLabSecret.key.startsWith(integration.metadata.secretPrefix)) {
isValid = false;
}
if (integration.metadata.secretSuffix && !gitLabSecret.key.endsWith(integration.metadata.secretSuffix)) {
isValid = false;
}
return isValid;
});
for await (const key of Object.keys(secrets)) {
const existingSecret = getSecretsRes.find((s: any) => s.key == key);
if (!existingSecret) {

View File

@@ -1,3 +1,11 @@
// TODO: in the future separate metadata
// into distinct types by integration
export type Metadata = {
secretPrefix?: string;
secretSuffix?: string;
secretGCPLabel?: {
labelName: string;
labelValue: string;
}
}

View File

@@ -35,9 +35,12 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => {
});
const suffixedSecrets: any = {};
if (integration.metadata?.secretSuffix) {
if (integration.metadata) {
for (const key in secrets) {
const newKey = key + integration.metadata?.secretSuffix;
const prefix = (integration.metadata?.secretPrefix || "");
const suffix = (integration.metadata?.secretSuffix || "");
const newKey = prefix + key + suffix;
suffixedSecrets[newKey] = secrets[key];
}
}

View File

@@ -77,7 +77,12 @@ export const CreateIntegrationV1 = z.object({
path: z.string().trim().optional(),
region: z.string().trim().optional(),
metadata: z.object({
secretSuffix: z.string().optional()
secretPrefix: z.string().optional(),
secretSuffix: z.string().optional(),
secretGCPLabel: z.object({
labelName: z.string(),
labelValue: z.string()
}).optional()
}).optional()
})
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -32,6 +32,16 @@ Press on the GitLab tile and grant Infisical access to your GitLab account.
Select which Infisical environment secrets you want to sync to which GitLab repository and press create integration to start syncing secrets to GitLab.
![integrations gitlab](../../images/integrations/gitlab/integrations-gitlab-create.png)
Note that the GitLab integration supports a few options in the **Options** tab:
- Secret Prefix: If inputted, the prefix is appended to the front of every secret name prior to being synced.
- Secret Suffix: If inputted, the suffix to appended to the back of every name of every secret prior to being synced.
Setting a secret prefix or suffix ensures that existing secrets in GCP Secret Manager are not overwritten during the sync. As part of this process, Infisical abstains from mutating any secrets in GitLab without the specified prefix or suffix.
![integrations gitlab options](../../images/integrations/gitlab/integrations-gitlab-create-options.png)
![integrations gitlab](../../images/integrations/gitlab/integrations-gitlab.png)
</Accordion>
<Accordion title="Pipeline">

View File

@@ -35,14 +35,21 @@ Grant Infisical access to GCP.
## Start integration
Select which Infisical environment secrets you want to sync to which GCP secret manager project. Lastly, press create integration to start syncing secrets to GCP secret manager.
In the **Connection** tab, select which Infisical environment secrets you want to sync to which GCP secret manager project. Lastly, press create integration to start syncing secrets to GCP secret manager.
![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create.png)
![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager.png)
<Note>
Secrets synced from Infisical to GCP Secret Manager are automatically labeled `managed-by:infisical` to avoid overwriting existing values in GCP Secret Manager.
</Note>
Note that the GCP Secret Manager integration supports a few options in the **Options** tab:
- Secret Prefix: If inputted, the prefix is appended to the front of every secret name prior to being synced.
- Secret Suffix: If inputted, the suffix to appended to the back of every name of every secret prior to being synced.
- Label in GCP Secret Manager: If selected, every secret will be labeled in GCP Secret Manager (e.g. as `managed-by:infisical`); labels can be customized.
Setting a secret prefix, suffix, or enabling the labeling option ensures that existing secrets in GCP Secret Manager are not overwritten during the sync. As part of this process, Infisical abstains from mutating any secrets in GCP Secret Manager without the specified prefix, suffix, or attached label.
![integrations GCP secret manager options](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create-options.png)
![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager.png)
<Warning>
Using Infisical to sync secrets to GCP Secret Manager requires that you enable
@@ -89,14 +96,21 @@ service account in IAM & Admin > Service Accounts > Service Account > Keys).
## Start integration
Select which Infisical environment secrets you want to sync to the GCP secret manager project. Lastly, press create integration to start syncing secrets to GCP secret manager.
In the **Connection** tab, select which Infisical environment secrets you want to sync to the GCP secret manager project. Lastly, press create integration to start syncing secrets to GCP secret manager.
![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create.png)
![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager.png)
<Note>
Secrets synced from Infisical to GCP Secret Manager are automatically labeled `managed-by:infisical` to avoid overwriting existing values in GCP Secret Manager.
</Note>
Note that the GCP Secret Manager integration supports a few options in the **Options** tab:
- Secret Prefix: If inputted, the prefix is appended to the front of every secret name prior to being synced.
- Secret Suffix: If inputted, the suffix to appended to the back of every name of every secret prior to being synced.
- Label in GCP Secret Manager: If selected, every secret will be labeled in GCP Secret Manager (e.g. as `managed-by:infisical`); labels can be customized.
Setting a secret prefix, suffix, or enabling the labeling option ensures that existing secrets in GCP Secret Manager are not overwritten during the sync. As part of this process, Infisical abstains from mutating any secrets in GCP Secret Manager without the specified prefix, suffix, or attached label.
![integrations GCP secret manager options](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create-options.png)
![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager.png)
<Warning>
Using Infisical to sync secrets to GCP Secret Manager requires that you enable

View File

@@ -57,6 +57,7 @@ export const useCreateIntegration = () => {
path?: string;
region?: string;
metadata?: {
secretPrefix?: string;
secretSuffix?: string;
}
}) => {

View File

@@ -39,7 +39,7 @@ export default function GCPSecretManagerAuthorizeIntegrationPage() {
setIsLoading(false);
router.push(`/integrations/gcp-secret-manager/pat/create?integrationAuthId=${integrationAuth._id}`);
router.push(`/integrations/gcp-secret-manager/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}

View File

@@ -1,6 +1,10 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useRouter } from "next/router";
import { yupResolver } from "@hookform/resolvers/yup";
import { motion } from "framer-motion";
import queryString from "query-string";
import * as yup from "yup";
import {
useCreateIntegration
@@ -13,7 +17,12 @@ import {
FormControl,
Input,
Select,
SelectItem
SelectItem,
Switch,
Tab,
TabList,
TabPanel,
Tabs
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
@@ -21,8 +30,44 @@ import {
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
enum TabSections {
Connection = "connection",
Options = "options"
}
const schema = yup.object({
selectedSourceEnvironment: yup.string().required("Source environment is required"),
secretPath: yup.string().required("Secret path is required"),
targetAppId: yup.string().required("GCP project is required"),
secretPrefix: yup.string(),
secretSuffix: yup.string(),
shouldLabel: yup.boolean(),
labelName: yup.string(),
labelValue: yup.string()
});
type FormData = yup.InferType<typeof schema>;
export default function GCPSecretManagerCreateIntegrationPage() {
const router = useRouter();
const {
control,
handleSubmit,
setValue,
watch
} = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: {
secretPath: "/",
shouldLabel: false,
labelName: "managed-by",
labelValue: "infisical"
}
});
const shouldLabel = watch("shouldLabel");
const selectedSourceEnvironment = watch("selectedSourceEnvironment");
const { mutateAsync } = useCreateIntegration();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
@@ -33,29 +78,45 @@ export default function GCPSecretManagerCreateIntegrationPage() {
integrationAuthId: (integrationAuthId as string) ?? ""
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [targetAppId, setTargetAppId] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (shouldLabel) {
setValue("labelName", "managed-by");
setValue("labelValue", "infisical");
return;
}
setValue("labelName", undefined);
setValue("labelValue", undefined);
}, [shouldLabel]);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
setValue("selectedSourceEnvironment", workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetAppId(integrationAuthApps[0].appId as string);
setValue("targetAppId", integrationAuthApps[0].appId as string);
} else {
setTargetAppId("none");
setValue("targetAppId", "none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
const onFormSubmit = async ({
selectedSourceEnvironment: sce,
secretPath,
targetAppId,
secretPrefix,
secretSuffix,
shouldLabel: sl,
labelName,
labelValue
}: FormData) => {
try {
setIsLoading(true);
@@ -66,82 +127,232 @@ export default function GCPSecretManagerCreateIntegrationPage() {
isActive: true,
app: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name,
appId: targetAppId,
sourceEnvironment: selectedSourceEnvironment,
secretPath
sourceEnvironment: sce,
secretPath,
metadata: {
secretPrefix,
secretSuffix,
...(sl ? {
secretGCPLabel: {
labelName,
labelValue
}
} : {})
}
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
setIsLoading(false);
}
};
}
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetAppId ? (
<div className="flex h-full w-full items-center justify-center">
integrationAuthApps ? (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="flex h-full w-full items-center justify-center"
>
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">GCP Secret Manager Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="GCP Project">
<Select
value={targetAppId}
onValueChange={(val) => setTargetAppId(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.appId as string}
key={`target-app-${integrationAuthApp.appId}`}
<Tabs defaultValue={TabSections.Connection}>
<TabList>
<Tab value={TabSections.Connection}>Connection</Tab>
<Tab value={TabSections.Options}>Options</Tab>
</TabList>
<TabPanel value={TabSections.Connection}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div>
<Controller
control={control}
name="selectedSourceEnvironment"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Project Environment"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secrets Path"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="/"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="targetAppId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="GCP Project"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
{...field}
onValueChange={(e) => {
if (e === "") return;
onChange(e)
}}
className="w-full"
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={String(integrationAuthApp.appId as string)}
key={`target-app-${String(integrationAuthApp.appId)}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
)}
</Select>
</FormControl>
)
}}
/>
<Button
className="mt-4"
size="sm"
type="submit"
isLoading={isLoading}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
Create Integration
</Button>
</div>
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Options}>
<Controller
control={control}
name="secretPrefix"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Prefix"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="INFISICAL_"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="secretSuffix"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Suffix"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="_INFISICAL"
/>
</FormControl>
)}
/>
<div className="mt-8">
<Controller
control={control}
name="shouldLabel"
render={({ field: { onChange, value } }) => (
<Switch
id="label-gcp"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
>
Label in GCP Secret Manager
</Switch>
)}
/>
</div>
{shouldLabel && (
<div className="mt-8">
<Controller
control={control}
name="labelName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Label Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="managed-by"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="labelValue"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Label Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="infisical"
/>
</FormControl>
)}
/>
</div>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</TabPanel>
</Tabs>
</Card>
</div>
</form>
) : (
<div />
);

View File

@@ -1,146 +0,0 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "@app/components/v2";
import {
useCreateIntegration
} from "@app/hooks/api";
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth";
import { useGetWorkspaceById } from "@app/hooks/api/workspace";
export default function GCPSecretManagerCreateIntegrationPage() {
const router = useRouter();
const { mutateAsync } = useCreateIntegration();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? ""
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [targetAppId, setTargetAppId] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetAppId(integrationAuthApps[0].appId as string);
} else {
setTargetAppId("none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
await mutateAsync({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name,
appId: targetAppId,
sourceEnvironment: selectedSourceEnvironment,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps
? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">GCP Secret Manager Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="GCP Project">
<Select
value={targetAppId}
onValueChange={(val) => setTargetAppId(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.appId as string}
key={`target-app-${integrationAuthApp.appId}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
// isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
GCPSecretManagerCreateIntegrationPage.requireAuth = true;

View File

@@ -1,6 +1,10 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useRouter } from "next/router";
import { yupResolver } from "@hookform/resolvers/yup";
import { motion } from "framer-motion";
import queryString from "query-string";
import * as yup from "yup";
import {
useCreateIntegration
@@ -13,7 +17,11 @@ import {
FormControl,
Input,
Select,
SelectItem
SelectItem,
Tab,
TabList,
TabPanel,
Tabs
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
@@ -27,8 +35,43 @@ const gitLabEntities = [
{ name: "Group", value: "group" }
];
enum TabSections {
Connection = "connection",
Options = "options"
}
const schema = yup.object({
targetEntity: yup.string().oneOf(gitLabEntities.map(entity => entity.value), "Invalid entity type"),
targetTeamId: yup.string(),
selectedSourceEnvironment: yup.string().required("Source environment is required"),
secretPath: yup.string().required("Secret path is required"),
targetAppId: yup.string().required("GitLab project is required"),
targetEnvironment: yup.string(),
secretPrefix: yup.string(),
secretSuffix: yup.string()
});
type FormData = yup.InferType<typeof schema>;
export default function GitLabCreateIntegrationPage() {
const router = useRouter();
const {
control,
handleSubmit,
setValue,
watch
} = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: {
targetEntity: "individual",
secretPath: "/"
}
});
const selectedSourceEnvironment = watch("selectedSourceEnvironment");
const targetEntity = watch("targetEntity");
const targetTeamId = watch("targetTeamId");
const { mutateAsync } = useCreateIntegration();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
@@ -36,8 +79,6 @@ export default function GitLabCreateIntegrationPage() {
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const [targetTeamId, setTargetTeamId] = useState<string | null>(null);
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? "",
...(targetTeamId ? { teamId: targetTeamId } : {})
@@ -46,26 +87,20 @@ export default function GitLabCreateIntegrationPage() {
(integrationAuthId as string) ?? ""
);
const [targetEntity, setTargetEntity] = useState(gitLabEntities[0].value);
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [targetAppId, setTargetAppId] = useState("");
const [targetEnvironment, setTargetEnvironment] = useState("");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
setValue("selectedSourceEnvironment", workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetAppId(integrationAuthApps[0].appId as string);
setValue("targetAppId", String(integrationAuthApps[0].appId as string));
} else {
setTargetAppId("none");
setValue("targetAppId", "none");
}
}
}, [integrationAuthApps]);
@@ -75,151 +110,283 @@ export default function GitLabCreateIntegrationPage() {
if (integrationAuthTeams) {
if (integrationAuthTeams.length > 0) {
// case: user is part of at least 1 group in GitLab
setTargetTeamId(integrationAuthTeams[0].teamId);
setValue("targetTeamId", String(integrationAuthTeams[0].teamId));
} else {
// case: user is not part of any groups in GitLab
setTargetTeamId("none");
setValue("targetTeamId", "none");
}
}
} else if (targetEntity === "individual") {
setTargetTeamId(null);
setValue("targetTeamId", undefined);
}
}, [targetEntity, integrationAuthTeams]);
const handleButtonClick = async () => {
const onFormSubmit = async ({
selectedSourceEnvironment: sse,
secretPath,
targetAppId,
targetEnvironment,
secretPrefix,
secretSuffix
}: FormData) => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
await mutateAsync({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name,
app: integrationAuthApps?.find((integrationAuthApp) => String(integrationAuthApp.appId) === targetAppId)?.name,
appId: String(targetAppId),
sourceEnvironment: selectedSourceEnvironment,
sourceEnvironment: sse,
targetEnvironment: targetEnvironment === "" ? "*" : targetEnvironment,
secretPath
secretPath,
metadata: {
secretPrefix,
secretSuffix
}
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
setIsLoading(false);
}
};
}
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
integrationAuthTeams &&
targetAppId ? (
<div className="flex h-full w-full items-center justify-center">
integrationAuthTeams ? (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="flex h-full w-full items-center justify-center"
>
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">GitLab Integration</CardTitle>
<FormControl label="Project Environment">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="GitLab Integration Type">
<Select
value={targetEntity}
onValueChange={(val) => setTargetEntity(val)}
className="w-full border border-mineshaft-500"
>
{gitLabEntities.map((entity) => {
return (
<SelectItem value={entity.value} key={`target-entity-${entity.value}`}>
{entity.name}
</SelectItem>
);
})}
</Select>
</FormControl>
{targetEntity === "group" && targetTeamId && (
<FormControl label="GitLab Group">
<Select
value={targetTeamId}
onValueChange={(val) => setTargetTeamId(val)}
className="w-full border border-mineshaft-500"
<Tabs defaultValue={TabSections.Connection}>
<TabList>
<Tab value={TabSections.Connection}>Connection</Tab>
<Tab value={TabSections.Options}>Options</Tab>
</TabList>
<TabPanel value={TabSections.Connection}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
{integrationAuthTeams.length > 0 ? (
integrationAuthTeams.map((integrationAuthTeam) => (
<SelectItem
value={integrationAuthTeam.teamId}
key={`target-team-${integrationAuthTeam.teamId}`}
>
{integrationAuthTeam.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-team-none">
No groups found
</SelectItem>
)}
</Select>
</FormControl>
)}
<FormControl label="GitLab Project">
<Select
value={targetAppId}
onValueChange={(val) => setTargetAppId(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.appId as string}
key={`target-app-${integrationAuthApp.appId}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
)}
</Select>
</FormControl>
<FormControl label="GitLab Environment Scope (Optional)">
<Input
placeholder="*"
value={targetEnvironment}
onChange={(e) => setTargetEnvironment(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
<div>
<Controller
control={control}
name="selectedSourceEnvironment"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Project Environment"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secrets Path"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="/"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="targetEntity"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="GitLab Integration Type"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{gitLabEntities.map((entity) => {
return (
<SelectItem value={entity.value} key={`target-entity-${entity.value}`}>
{entity.name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
{targetEntity === "group" && targetTeamId && (
<Controller
control={control}
name="targetTeamId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="GitLab Group"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{integrationAuthTeams.length > 0 ? (
integrationAuthTeams.map((integrationAuthTeam) =>
(
<SelectItem
value={String(integrationAuthTeam.teamId as string)}
key={`target-team-${String(integrationAuthTeam.teamId)}`}
>
{integrationAuthTeam.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-team-none">
No groups found
</SelectItem>
)}
</Select>
</FormControl>
)}
/>
)}
<Controller
control={control}
name="targetAppId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="GitLab Project"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
{...field}
onValueChange={(e) => {
if (e === "") return;
onChange(e)
}}
className="w-full"
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={String(integrationAuthApp.appId as string)}
key={`target-app-${String(integrationAuthApp.appId)}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
)}
</Select>
</FormControl>
)}}
/>
<Controller
control={control}
defaultValue=""
name="targetEnvironment"
render={({ field, fieldState: { error } }) => (
<FormControl
label="GitLab Environment Scope (Optional)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="*"
/>
</FormControl>
)}
/>
</div>
<Button
className="mt-4"
size="sm"
type="submit"
isLoading={isLoading}
>
Create Integration
</Button>
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Options}>
<div>
<Controller
control={control}
name="secretPrefix"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Prefix"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="INFISICAL_"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="secretSuffix"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Suffix"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="_INFISICAL"
/>
</FormControl>
)}
/>
</div>
</TabPanel>
</Tabs>
</Card>
</div>
</form>
) : (
<div />
);