mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
add initial sync options to terraform cloud integration
This commit is contained in:
@@ -2157,16 +2157,29 @@ const syncSecretsQovery = async ({
|
||||
* @param {String} obj.accessToken - access token for Terraform Cloud API
|
||||
*/
|
||||
const syncSecretsTerraformCloud = async ({
|
||||
createManySecretsRawFn,
|
||||
updateManySecretsRawFn,
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
accessToken,
|
||||
integrationDAL
|
||||
}: {
|
||||
integration: TIntegrations;
|
||||
secrets: Record<string, { value: string; comment?: string }>;
|
||||
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
|
||||
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
|
||||
integration: TIntegrations & {
|
||||
projectId: string;
|
||||
environment: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
};
|
||||
secrets: Record<string, { value: string; comment?: string } | null>;
|
||||
accessToken: string;
|
||||
integrationDAL: Pick<TIntegrationDALFactory, "updateById">;
|
||||
}) => {
|
||||
// get secrets from Terraform Cloud
|
||||
const getSecretsRes = (
|
||||
const terraformSecrets = (
|
||||
await request.get<{ data: { attributes: { key: string; value: string }; id: string }[] }>(
|
||||
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars`,
|
||||
{
|
||||
@@ -2184,9 +2197,74 @@ const syncSecretsTerraformCloud = async ({
|
||||
{} as Record<string, { attributes: { key: string; value: string }; id: string }>
|
||||
);
|
||||
|
||||
const secretsToAdd: { [key: string]: string } = {};
|
||||
const secretsToUpdate: { [key: string]: string } = {};
|
||||
|
||||
const metadata = z.record(z.any()).parse(integration.metadata);
|
||||
|
||||
Object.keys(terraformSecrets).forEach((key) => {
|
||||
if (!integration.lastUsed) {
|
||||
// first time using integration
|
||||
// -> apply initial sync behavior
|
||||
switch (metadata.initialSyncBehavior) {
|
||||
case IntegrationInitialSyncBehavior.PREFER_TARGET: {
|
||||
if (!(key in secrets)) {
|
||||
secretsToAdd[key] = terraformSecrets[key].attributes.value;
|
||||
} else if (secrets[key]?.value !== terraformSecrets[key].attributes.value) {
|
||||
secretsToUpdate[key] = terraformSecrets[key].attributes.value;
|
||||
}
|
||||
secrets[key] = {
|
||||
value: terraformSecrets[key].attributes.value
|
||||
};
|
||||
break;
|
||||
}
|
||||
case IntegrationInitialSyncBehavior.PREFER_SOURCE: {
|
||||
if (!(key in secrets)) {
|
||||
secrets[key] = {
|
||||
value: terraformSecrets[key].attributes.value
|
||||
};
|
||||
secretsToAdd[key] = terraformSecrets[key].attributes.value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (!(key in secrets)) secrets[key] = null;
|
||||
});
|
||||
|
||||
if (Object.keys(secretsToAdd).length) {
|
||||
await createManySecretsRawFn({
|
||||
projectId: integration.projectId,
|
||||
environment: integration.environment.slug,
|
||||
path: integration.secretPath,
|
||||
secrets: Object.keys(secretsToAdd).map((key) => ({
|
||||
secretName: key,
|
||||
secretValue: secretsToAdd[key],
|
||||
type: SecretType.Shared,
|
||||
secretComment: ""
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.keys(secretsToUpdate).length) {
|
||||
await updateManySecretsRawFn({
|
||||
projectId: integration.projectId,
|
||||
environment: integration.environment.slug,
|
||||
path: integration.secretPath,
|
||||
secrets: Object.keys(secretsToUpdate).map((key) => ({
|
||||
secretName: key,
|
||||
secretValue: secretsToUpdate[key],
|
||||
type: SecretType.Shared,
|
||||
secretComment: ""
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
// create or update secrets on Terraform Cloud
|
||||
for await (const key of Object.keys(secrets)) {
|
||||
if (!(key in getSecretsRes)) {
|
||||
if (!(key in terraformSecrets)) {
|
||||
// case: secret does not exist in Terraform Cloud
|
||||
// -> add secret
|
||||
await request.post(
|
||||
@@ -2196,7 +2274,7 @@ const syncSecretsTerraformCloud = async ({
|
||||
type: "vars",
|
||||
attributes: {
|
||||
key,
|
||||
value: secrets[key].value,
|
||||
value: secrets[key]?.value,
|
||||
category: integration.targetService
|
||||
}
|
||||
}
|
||||
@@ -2210,17 +2288,17 @@ const syncSecretsTerraformCloud = async ({
|
||||
}
|
||||
);
|
||||
// case: secret exists in Terraform Cloud
|
||||
} else if (secrets[key].value !== getSecretsRes[key].attributes.value) {
|
||||
} else if (secrets[key]?.value !== terraformSecrets[key].attributes.value) {
|
||||
// -> update secret
|
||||
await request.patch(
|
||||
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${getSecretsRes[key].id}`,
|
||||
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${terraformSecrets[key].id}`,
|
||||
{
|
||||
data: {
|
||||
type: "vars",
|
||||
id: getSecretsRes[key].id,
|
||||
id: terraformSecrets[key].id,
|
||||
attributes: {
|
||||
...getSecretsRes[key],
|
||||
value: secrets[key].value
|
||||
...terraformSecrets[key],
|
||||
value: secrets[key]?.value
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2235,11 +2313,11 @@ const syncSecretsTerraformCloud = async ({
|
||||
}
|
||||
}
|
||||
|
||||
for await (const key of Object.keys(getSecretsRes)) {
|
||||
for await (const key of Object.keys(terraformSecrets)) {
|
||||
if (!(key in secrets)) {
|
||||
// case: delete secret
|
||||
await request.delete(
|
||||
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${getSecretsRes[key].id}`,
|
||||
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${terraformSecrets[key].id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
@@ -2250,6 +2328,10 @@ const syncSecretsTerraformCloud = async ({
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await integrationDAL.updateById(integration.id, {
|
||||
lastUsed: new Date()
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -3291,9 +3373,12 @@ export const syncIntegrationSecrets = async ({
|
||||
break;
|
||||
case Integrations.TERRAFORM_CLOUD:
|
||||
await syncSecretsTerraformCloud({
|
||||
createManySecretsRawFn,
|
||||
updateManySecretsRawFn,
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
accessToken,
|
||||
integrationDAL
|
||||
});
|
||||
break;
|
||||
case Integrations.HASHICORP_VAULT:
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||
|
||||
@@ -49,12 +54,56 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="max-w-md rounded-md p-8">
|
||||
<CardTitle className="text-center">Terraform Cloud Integration</CardTitle>
|
||||
<Head>
|
||||
<title>Authorize Terraform Cloud Integration</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="After adding the details below, you will be prompted to set up an integration for a particular Infisical project and environment."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="inline flex items-center">
|
||||
<Image
|
||||
src="/images/integrations/Terraform.png"
|
||||
height={35}
|
||||
width={35}
|
||||
alt="Terraform logo"
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-1.5">Terraform Cloud Integration </span>
|
||||
<Link href="https://infisical.com/docs/integrations/cloud/terraform-cloud" passHref>
|
||||
<a target="_blank" rel="noopener noreferrer">
|
||||
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<FormControl
|
||||
label="Terraform Cloud Workspace ID"
|
||||
errorText={workspacesIdErrorText}
|
||||
isError={workspacesIdErrorText !== "" ?? false}
|
||||
className="px-6"
|
||||
>
|
||||
<Input
|
||||
placeholder="Workspace Id"
|
||||
value={workspacesId}
|
||||
onChange={(e) => setWorkSpacesId(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Terraform Cloud API Token"
|
||||
errorText={apiKeyErrorText}
|
||||
isError={apiKeyErrorText !== "" ?? false}
|
||||
className="px-6"
|
||||
>
|
||||
<Input
|
||||
placeholder="API Token"
|
||||
@@ -64,21 +113,11 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Terraform Cloud Workspace ID"
|
||||
errorText={workspacesIdErrorText}
|
||||
isError={workspacesIdErrorText !== "" ?? false}
|
||||
>
|
||||
<Input
|
||||
placeholder="Workspace Id"
|
||||
value={workspacesId}
|
||||
onChange={(e) => setWorkSpacesId(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mb-6 mt-2 ml-auto mr-6 w-min"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Connect to Terraform Cloud
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -19,6 +25,15 @@ import {
|
||||
} from "../../../hooks/api/integrationAuth";
|
||||
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
|
||||
|
||||
const initialSyncBehaviors = [
|
||||
{
|
||||
label: "No Import - Overwrite all values in Terraform Cloud",
|
||||
value: IntegrationSyncBehavior.OVERWRITE_TARGET
|
||||
},
|
||||
{ label: "Import non-sensitive - Prefer values from Terraform Cloud", value: IntegrationSyncBehavior.PREFER_TARGET },
|
||||
{ label: "Import non-sensitive - Prefer values from Infisical", value: IntegrationSyncBehavior.PREFER_SOURCE }
|
||||
];
|
||||
|
||||
const variableTypes = [{ name: "env" }, { name: "terraform" }];
|
||||
|
||||
export default function TerraformCloudCreateIntegrationPage() {
|
||||
@@ -39,6 +54,7 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
const [variableType, setVariableType] = useState("");
|
||||
const [variableTypeErrorText, setVariableTypeErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [initialSyncBehavior, setInitialSyncBehavior] = useState("prefer-source")
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
@@ -78,7 +94,10 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
)?.appId,
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
targetService: variableType,
|
||||
secretPath
|
||||
secretPath,
|
||||
metadata: {
|
||||
initialSyncBehavior
|
||||
}
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -95,9 +114,40 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
integrationAuthApps &&
|
||||
targetApp ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="max-w-md rounded-md p-8">
|
||||
<CardTitle className="text-center">Terraform Cloud Integration</CardTitle>
|
||||
<FormControl label="Project Environment" className="mt-4">
|
||||
<Head>
|
||||
<title>Create Terraform Cloud Integration</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="Specify the encironment and path within Infisical that you want to push to which project in Terraform."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="inline flex items-center">
|
||||
<Image
|
||||
src="/images/integrations/Terraform.png"
|
||||
height={35}
|
||||
width={35}
|
||||
alt="Terraform logo"
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-1.5">Terraform Cloud Integration </span>
|
||||
<Link href="https://infisical.com/docs/integrations/cloud/terraform-cloud" passHref>
|
||||
<a target="_blank" rel="noopener noreferrer">
|
||||
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<FormControl label="Project Environment" className="px-6">
|
||||
<Select
|
||||
value={selectedSourceEnvironment}
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
@@ -113,7 +163,7 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<FormControl label="Secrets Path" className="px-6">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
@@ -122,7 +172,7 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Category"
|
||||
className="mt-4"
|
||||
className="px-6"
|
||||
errorText={variableTypeErrorText}
|
||||
isError={variableTypeErrorText !== "" ?? false}
|
||||
>
|
||||
@@ -138,7 +188,7 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Terraform Cloud Project" className="mt-4">
|
||||
<FormControl label="Terraform Cloud Project" className="px-6">
|
||||
<Select
|
||||
value={targetApp}
|
||||
onValueChange={(val) => setTargetApp(val)}
|
||||
@@ -161,10 +211,22 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Initial Sync Behavior" className="px-6">
|
||||
<Select value={initialSyncBehavior} onValueChange={(e) => setInitialSyncBehavior(e)} className="w-full border border-mineshaft-600">
|
||||
{initialSyncBehaviors.map((b) => {
|
||||
return (
|
||||
<SelectItem value={b.value} key={`sync-behavior-${b.value}`}>
|
||||
{b.label}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mb-6 mt-2 ml-auto mr-6 w-min"
|
||||
isLoading={isLoading}
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
>
|
||||
|
||||
@@ -130,6 +130,7 @@ export const IntegrationsSection = ({
|
||||
(integration.integration === "qovery" && integration?.scope) ||
|
||||
(integration.integration === "aws-secret-manager" && "Secret") ||
|
||||
(integration.integration === "aws-parameter-store" && "Path") ||
|
||||
(integration?.integration === "terraform-cloud" && "Project") ||
|
||||
(integration?.scope === "github-org" && "Organization") ||
|
||||
(["github-repo", "github-env"].includes(integration?.scope as string) &&
|
||||
"Repository") ||
|
||||
@@ -168,6 +169,14 @@ export const IntegrationsSection = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "terraform-cloud" && integration.targetService && (
|
||||
<div className="ml-2">
|
||||
<FormLabel label="Category" />
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration.targetService}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(integration.integration === "checkly" ||
|
||||
integration.integration === "github") && (
|
||||
<div className="ml-2">
|
||||
|
||||
Reference in New Issue
Block a user