Merge pull request #2858 from Infisical/daniel/azure-label

feat(azure-app-integration): label & reference support
This commit is contained in:
Daniel Hougaard
2024-12-17 20:56:53 +01:00
committed by GitHub
8 changed files with 150 additions and 38 deletions
+1
View File
@@ -1126,6 +1126,7 @@ export const INTEGRATION = {
shouldAutoRedeploy: "Used by Render to trigger auto deploy.", shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
secretGCPLabel: "The label for GCP secrets.", secretGCPLabel: "The label for GCP secrets.",
secretAWSTag: "The tags for AWS secrets.", secretAWSTag: "The tags for AWS secrets.",
azureLabel: "Define which label to assign to secrets created in Azure App Configuration.",
githubVisibility: githubVisibility:
"Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.", "Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.",
githubVisibilityRepoIds: githubVisibilityRepoIds:
@@ -0,0 +1,35 @@
export const isAzureKeyVaultReference = (uri: string) => {
const tryJsonDecode = () => {
try {
return (JSON.parse(uri) as { uri: string }).uri || uri;
} catch {
return uri;
}
};
const cleanUri = tryJsonDecode();
if (!cleanUri.startsWith("https://")) {
return false;
}
if (!cleanUri.includes(".vault.azure.net/secrets/")) {
return false;
}
// 3. Check for non-empty string between https:// and .vault.azure.net/secrets/
const parts = cleanUri.split(".vault.azure.net/secrets/");
const vaultName = parts[0].replace("https://", "");
if (!vaultName) {
return false;
}
// 4. Check for non-empty secret name
const secretParts = parts[1].split("/");
const secretName = secretParts[0];
if (!secretName) {
return false;
}
return true;
};
@@ -51,6 +51,7 @@ import {
Integrations, Integrations,
IntegrationUrls IntegrationUrls
} from "./integration-list"; } from "./integration-list";
import { isAzureKeyVaultReference } from "./integration-sync-secret-fns";
const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) => const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) =>
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => { Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
@@ -325,11 +326,12 @@ const syncSecretsAzureAppConfig = async ({
}; };
const metadata = IntegrationMetadataSchema.parse(integration.metadata); const metadata = IntegrationMetadataSchema.parse(integration.metadata);
const azureAppConfigSecrets = (
await getCompleteAzureAppConfigValues( const azureAppConfigValuesUrl = `${integration.app}/kv?api-version=2023-11-01&key=${metadata.secretPrefix}*${
`${integration.app}/kv?api-version=2023-11-01&key=${metadata.secretPrefix || ""}*` metadata.azureLabel ? `&label=${metadata.azureLabel}` : ""
) }`;
).reduce(
const azureAppConfigSecrets = (await getCompleteAzureAppConfigValues(azureAppConfigValuesUrl)).reduce(
(accum, entry) => { (accum, entry) => {
accum[entry.key] = entry.value; accum[entry.key] = entry.value;
@@ -410,14 +412,24 @@ const syncSecretsAzureAppConfig = async ({
} }
// create or update secrets on Azure App Config // create or update secrets on Azure App Config
for await (const key of Object.keys(secrets)) { for await (const key of Object.keys(secrets)) {
if (!(key in azureAppConfigSecrets) || secrets[key]?.value !== azureAppConfigSecrets[key]) { if (!(key in azureAppConfigSecrets) || secrets[key]?.value !== azureAppConfigSecrets[key]) {
await request.put( await request.put(
`${integration.app}/kv/${key}?api-version=2023-11-01`, `${integration.app}/kv/${key}?api-version=2023-11-01`,
{ {
value: secrets[key]?.value value: secrets[key]?.value,
...(isAzureKeyVaultReference(secrets[key]?.value || "") && {
content_type: "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"
})
}, },
{ {
...(metadata.azureLabel && {
params: {
label: metadata.azureLabel
}
}),
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`
}, },
@@ -437,6 +449,11 @@ const syncSecretsAzureAppConfig = async ({
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`
}, },
...(metadata.azureLabel && {
params: {
label: metadata.azureLabel
}
}),
// we force IPV4 because docker setup fails with ipv6 // we force IPV4 because docker setup fails with ipv6
httpsAgent: new https.Agent({ httpsAgent: new https.Agent({
family: 4 family: 4
@@ -35,6 +35,8 @@ export const IntegrationMetadataSchema = z.object({
.optional() .optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag), .describe(INTEGRATION.CREATE.metadata.secretAWSTag),
azureLabel: z.string().optional().describe(INTEGRATION.CREATE.metadata.azureLabel),
githubVisibility: z githubVisibility: z
.union([z.literal("selected"), z.literal("private"), z.literal("all")]) .union([z.literal("selected"), z.literal("private"), z.literal("all")])
.optional() .optional()
@@ -80,6 +80,7 @@ export const useCreateIntegration = () => {
key: string; key: string;
value: string; value: string;
}[]; }[];
azureLabel?: string;
githubVisibility?: string; githubVisibility?: string;
githubVisibilityRepoIds?: string[]; githubVisibilityRepoIds?: string[];
kmsKeyId?: string; kmsKeyId?: string;
@@ -41,6 +41,7 @@ export type TIntegration = {
key: string; key: string;
value: string; value: string;
}[]; }[];
azureLabel?: string;
kmsKeyId?: string; kmsKeyId?: string;
secretSuffix?: string; secretSuffix?: string;
@@ -10,6 +10,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import queryString from "query-string"; import queryString from "query-string";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useCreateIntegration } from "@app/hooks/api"; import { useCreateIntegration } from "@app/hooks/api";
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types"; import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
@@ -19,9 +20,11 @@ import {
Card, Card,
CardTitle, CardTitle,
FormControl, FormControl,
FormLabel,
Input, Input,
Select, Select,
SelectItem SelectItem,
Switch
} from "../../../components/v2"; } from "../../../components/v2";
import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace"; import { useGetWorkspaceById } from "../../../hooks/api/workspace";
@@ -39,7 +42,9 @@ const schema = z.object({
secretPath: z.string().trim().min(1, { message: "Secret path is required" }), secretPath: z.string().trim().min(1, { message: "Secret path is required" }),
sourceEnvironment: z.string().trim().min(1, { message: "Source environment is required" }), sourceEnvironment: z.string().trim().min(1, { message: "Source environment is required" }),
initialSyncBehavior: z.nativeEnum(IntegrationSyncBehavior), initialSyncBehavior: z.nativeEnum(IntegrationSyncBehavior),
secretPrefix: z.string().default("") secretPrefix: z.string().default(""),
useLabels: z.boolean().default(false),
azureLabel: z.string().min(1).optional()
}); });
type TFormSchema = z.infer<typeof schema>; type TFormSchema = z.infer<typeof schema>;
@@ -60,6 +65,7 @@ export default function AzureAppConfigurationCreateIntegration() {
const router = useRouter(); const router = useRouter();
const { const {
control, control,
watch,
setValue, setValue,
handleSubmit, handleSubmit,
formState: { isSubmitting } formState: { isSubmitting }
@@ -85,16 +91,28 @@ export default function AzureAppConfigurationCreateIntegration() {
} }
}, [workspace]); }, [workspace]);
const shouldUseLabels = watch("useLabels");
const handleIntegrationSubmit = async ({ const handleIntegrationSubmit = async ({
secretPath, secretPath,
useLabels,
sourceEnvironment, sourceEnvironment,
baseUrl, baseUrl,
initialSyncBehavior, initialSyncBehavior,
secretPrefix secretPrefix,
azureLabel
}: TFormSchema) => { }: TFormSchema) => {
try { try {
if (!integrationAuth?.id) return; if (!integrationAuth?.id) return;
if (useLabels && !azureLabel) {
createNotification({
type: "error",
text: "Label must be provided when 'Use Labels' is enabled"
});
return;
}
await mutateAsync({ await mutateAsync({
integrationAuthId: integrationAuth?.id, integrationAuthId: integrationAuth?.id,
isActive: true, isActive: true,
@@ -103,7 +121,8 @@ export default function AzureAppConfigurationCreateIntegration() {
secretPath, secretPath,
metadata: { metadata: {
initialSyncBehavior, initialSyncBehavior,
secretPrefix secretPrefix,
...(useLabels && { azureLabel })
} }
}); });
@@ -155,35 +174,70 @@ export default function AzureAppConfigurationCreateIntegration() {
</div> </div>
</CardTitle> </CardTitle>
<div className="px-6"> <div className="px-6">
<Controller <div className="">
control={control} <Controller
name="sourceEnvironment" control={control}
render={({ field, fieldState: { error } }) => ( name="sourceEnvironment"
<FormControl render={({ field, fieldState: { error } }) => (
label="Project Environment" <FormControl
errorText={error?.message} label="Project Environment"
isError={Boolean(error)} errorText={error?.message}
> isError={Boolean(error)}
<Select
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-full"
value={field.value}
onValueChange={(val) => {
field.onChange(val);
}}
> >
{workspace?.environments.map((sourceEnvironment) => ( <Select
<SelectItem className="w-full border border-mineshaft-500"
value={sourceEnvironment.slug} dropdownContainerClassName="max-w-full"
key={`source-environment-${sourceEnvironment.slug}`} value={field.value}
onValueChange={(val) => {
field.onChange(val);
}}
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="mb-2 flex w-full flex-col gap-1">
<Controller
control={control}
name="useLabels"
render={({ field: { onChange, value } }) => (
<Switch
id="use-environment-labels"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
>
<FormLabel label="Use Labels" />
</Switch>
)}
/>
{shouldUseLabels && (
<Controller
control={control}
name="azureLabel"
render={({ field, fieldState: { error } }) => (
<FormControl
className=""
// label="Label"
errorText={error?.message}
isError={Boolean(error)}
> >
{sourceEnvironment.name} <Input {...field} placeholder="pre-prod" />
</SelectItem> </FormControl>
))} )}
</Select> />
</FormControl> )}
)} </div>
/> </div>
<Controller <Controller
control={control} control={control}
name="secretPath" name="secretPath"
@@ -14,6 +14,7 @@ const metadataMappings: Record<keyof NonNullable<TIntegrationWithEnv["metadata"]
githubVisibilityRepoIds: "Github Visibility Repo Ids", githubVisibilityRepoIds: "Github Visibility Repo Ids",
shouldAutoRedeploy: "Auto Redeploy Target Application When Secrets Change", shouldAutoRedeploy: "Auto Redeploy Target Application When Secrets Change",
secretAWSTag: "Tags For Secrets Stored In AWS", secretAWSTag: "Tags For Secrets Stored In AWS",
azureLabel: "Azure Label",
kmsKeyId: "AWS KMS Key ID", kmsKeyId: "AWS KMS Key ID",
secretSuffix: "Secret Suffix", secretSuffix: "Secret Suffix",
secretPrefix: "Secret Prefix", secretPrefix: "Secret Prefix",
@@ -86,7 +87,7 @@ export const IntegrationSettingsSection = ({ integration }: Props) => {
Object.entries(integration.metadata).map(([key, value]) => ( Object.entries(integration.metadata).map(([key, value]) => (
<div key={key} className="flex flex-col"> <div key={key} className="flex flex-col">
<p className="text-sm text-gray-400"> <p className="text-sm text-gray-400">
{metadataMappings[key as keyof typeof metadataMappings]} {!!value && metadataMappings[key as keyof typeof metadataMappings]}
</p> </p>
<p className="text-sm text-gray-200">{renderValue(key as MetadataKey, value)}</p> <p className="text-sm text-gray-200">{renderValue(key as MetadataKey, value)}</p>
</div> </div>