Merge pull request #4784 from Infisical/feat/chef-data-bag-app-connection-secret-sync
[ENG-4034] feat: Adds chef data bag app connection and secret sync
@@ -2379,6 +2379,12 @@ export const AppConnections = {
|
||||
},
|
||||
LARAVEL_FORGE: {
|
||||
apiToken: "The API token used to authenticate with Laravel Forge."
|
||||
},
|
||||
CHEF: {
|
||||
serverUrl: "The URL of the Chef server to connect to.",
|
||||
orgName: "The short name of the Chef organization to connect to.",
|
||||
userName: "The username used to access Chef.",
|
||||
privateKey: "The private key used to access Chef."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2624,6 +2630,10 @@ export const SecretSyncs = {
|
||||
siteId: "The ID of the Netlify site to sync secrets to.",
|
||||
context: "The Netlify context to sync secrets to."
|
||||
},
|
||||
CHEF: {
|
||||
dataBagName: "The name of the Chef data bag to sync secrets to.",
|
||||
dataBagItemName: "The name of the Chef data bag item to sync secrets to."
|
||||
},
|
||||
NORTHFLANK: {
|
||||
projectId: "The ID of the Northflank project to sync secrets to.",
|
||||
projectName: "The name of the Northflank project to sync secrets to.",
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
ChecklyConnectionListItemSchema,
|
||||
SanitizedChecklyConnectionSchema
|
||||
} from "@app/services/app-connection/checkly";
|
||||
import { ChefConnectionListItemSchema, SanitizedChefConnectionSchema } from "@app/services/app-connection/chef";
|
||||
import {
|
||||
CloudflareConnectionListItemSchema,
|
||||
SanitizedCloudflareConnectionSchema
|
||||
@@ -168,7 +169,8 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedOktaConnectionSchema.options,
|
||||
...SanitizedAzureADCSConnectionSchema.options,
|
||||
...SanitizedRedisConnectionSchema.options,
|
||||
...SanitizedLaravelForgeConnectionSchema.options
|
||||
...SanitizedLaravelForgeConnectionSchema.options,
|
||||
...SanitizedChefConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@@ -212,7 +214,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
OktaConnectionListItemSchema,
|
||||
AzureADCSConnectionListItemSchema,
|
||||
RedisConnectionListItemSchema,
|
||||
LaravelForgeConnectionListItemSchema
|
||||
LaravelForgeConnectionListItemSchema,
|
||||
ChefConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import z from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateChefConnectionSchema,
|
||||
SanitizedChefConnectionSchema,
|
||||
UpdateChefConnectionSchema
|
||||
} from "@app/services/app-connection/chef";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerChefConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.Chef,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedChefConnectionSchema,
|
||||
createSchema: CreateChefConnectionSchema,
|
||||
updateSchema: UpdateChefConnectionSchema
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/data-bags`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const dataBags = await server.services.appConnection.chef.listDataBags(connectionId, req.permission);
|
||||
|
||||
return dataBags;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/data-bag-items`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
dataBagName: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const { dataBagName } = req.query;
|
||||
const dataBagItems = await server.services.appConnection.chef.listDataBagItems(
|
||||
connectionId,
|
||||
dataBagName,
|
||||
req.permission
|
||||
);
|
||||
|
||||
return dataBagItems;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connect
|
||||
import { registerBitbucketConnectionRouter } from "./bitbucket-connection-router";
|
||||
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
||||
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
|
||||
import { registerChefConnectionRouter } from "./chef-connection-router";
|
||||
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router";
|
||||
@@ -86,5 +87,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
|
||||
[AppConnection.Northflank]: registerNorthflankConnectionRouter,
|
||||
[AppConnection.Okta]: registerOktaConnectionRouter,
|
||||
[AppConnection.Redis]: registerRedisConnectionRouter
|
||||
[AppConnection.Redis]: registerRedisConnectionRouter,
|
||||
[AppConnection.Chef]: registerChefConnectionRouter
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ChefSyncSchema, CreateChefSyncSchema, UpdateChefSyncSchema } from "@app/services/secret-sync/chef";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerChefSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.Chef,
|
||||
server,
|
||||
responseSchema: ChefSyncSchema,
|
||||
createSchema: CreateChefSyncSchema,
|
||||
updateSchema: UpdateChefSyncSchema
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router";
|
||||
import { registerBitbucketSyncRouter } from "./bitbucket-sync-router";
|
||||
import { registerCamundaSyncRouter } from "./camunda-sync-router";
|
||||
import { registerChecklySyncRouter } from "./checkly-sync-router";
|
||||
import { registerChefSyncRouter } from "./chef-sync-router";
|
||||
import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router";
|
||||
import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router";
|
||||
import { registerDatabricksSyncRouter } from "./databricks-sync-router";
|
||||
@@ -67,5 +68,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
||||
[SecretSync.Netlify]: registerNetlifySyncRouter,
|
||||
[SecretSync.Northflank]: registerNorthflankSyncRouter,
|
||||
[SecretSync.Bitbucket]: registerBitbucketSyncRouter,
|
||||
[SecretSync.LaravelForge]: registerLaravelForgeSyncRouter
|
||||
[SecretSync.LaravelForge]: registerLaravelForgeSyncRouter,
|
||||
[SecretSync.Chef]: registerChefSyncRouter
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/s
|
||||
import { BitbucketSyncListItemSchema, BitbucketSyncSchema } from "@app/services/secret-sync/bitbucket";
|
||||
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
|
||||
import { ChecklySyncListItemSchema, ChecklySyncSchema } from "@app/services/secret-sync/checkly/checkly-sync-schemas";
|
||||
import { ChefSyncListItemSchema, ChefSyncSchema } from "@app/services/secret-sync/chef";
|
||||
import {
|
||||
CloudflarePagesSyncListItemSchema,
|
||||
CloudflarePagesSyncSchema
|
||||
@@ -88,7 +89,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
NetlifySyncSchema,
|
||||
NorthflankSyncSchema,
|
||||
BitbucketSyncSchema,
|
||||
LaravelForgeSyncSchema
|
||||
LaravelForgeSyncSchema,
|
||||
ChefSyncSchema
|
||||
]);
|
||||
|
||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
@@ -123,7 +125,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
NetlifySyncListItemSchema,
|
||||
NorthflankSyncListItemSchema,
|
||||
BitbucketSyncListItemSchema,
|
||||
LaravelForgeSyncListItemSchema
|
||||
LaravelForgeSyncListItemSchema,
|
||||
ChefSyncListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -39,6 +39,7 @@ export enum AppConnection {
|
||||
Okta = "okta",
|
||||
Redis = "redis",
|
||||
LaravelForge = "laravel-forge",
|
||||
Chef = "chef",
|
||||
Northflank = "northflank"
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
} from "./bitbucket";
|
||||
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
|
||||
import { ChecklyConnectionMethod, getChecklyConnectionListItem, validateChecklyConnectionCredentials } from "./checkly";
|
||||
import { ChefConnectionMethod, getChefConnectionListItem, validateChefConnectionCredentials } from "./chef";
|
||||
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
|
||||
import {
|
||||
getCloudflareConnectionListItem,
|
||||
@@ -210,7 +211,8 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||
getNetlifyConnectionListItem(),
|
||||
getNorthflankConnectionListItem(),
|
||||
getOktaConnectionListItem(),
|
||||
getRedisConnectionListItem()
|
||||
getRedisConnectionListItem(),
|
||||
getChefConnectionListItem()
|
||||
]
|
||||
.filter((option) => {
|
||||
switch (projectType) {
|
||||
@@ -341,6 +343,7 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Northflank]: validateNorthflankConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Chef]: validateChefConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
@@ -409,6 +412,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case RenderConnectionMethod.ApiKey:
|
||||
case ChecklyConnectionMethod.ApiKey:
|
||||
return "API Key";
|
||||
case ChefConnectionMethod.UserKey:
|
||||
return "User Key";
|
||||
case SupabaseConnectionMethod.AccessToken:
|
||||
return "Access Token";
|
||||
default:
|
||||
@@ -483,7 +488,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Northflank]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Okta]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Redis]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported
|
||||
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Chef]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
export const enterpriseAppCheck = async (
|
||||
|
||||
@@ -41,6 +41,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Netlify]: "Netlify",
|
||||
[AppConnection.Okta]: "Okta",
|
||||
[AppConnection.Redis]: "Redis",
|
||||
[AppConnection.Chef]: "Chef",
|
||||
[AppConnection.Northflank]: "Northflank"
|
||||
};
|
||||
|
||||
@@ -85,5 +86,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Okta]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Redis]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Chef]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Northflank]: AppConnectionPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -67,6 +67,8 @@ import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
|
||||
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
||||
import { ValidateChecklyConnectionCredentialsSchema } from "./checkly";
|
||||
import { checklyConnectionService } from "./checkly/checkly-connection-service";
|
||||
import { ValidateChefConnectionCredentialsSchema } from "./chef";
|
||||
import { chefConnectionService } from "./chef/chef-connection-service";
|
||||
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
|
||||
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||
@@ -174,7 +176,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
|
||||
[AppConnection.Northflank]: ValidateNorthflankConnectionCredentialsSchema,
|
||||
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
|
||||
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema
|
||||
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema,
|
||||
[AppConnection.Chef]: ValidateChefConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -881,6 +884,7 @@ export const appConnectionServiceFactory = ({
|
||||
netlify: netlifyConnectionService(connectAppConnectionById),
|
||||
northflank: northflankConnectionService(connectAppConnectionById),
|
||||
okta: oktaConnectionService(connectAppConnectionById),
|
||||
laravelForge: laravelForgeConnectionService(connectAppConnectionById)
|
||||
laravelForge: laravelForgeConnectionService(connectAppConnectionById),
|
||||
chef: chefConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -82,6 +82,12 @@ import {
|
||||
TChecklyConnectionInput,
|
||||
TValidateChecklyConnectionCredentialsSchema
|
||||
} from "./checkly";
|
||||
import {
|
||||
TChefConnection,
|
||||
TChefConnectionConfig,
|
||||
TChefConnectionInput,
|
||||
TValidateChefConnectionCredentialsSchema
|
||||
} from "./chef";
|
||||
import {
|
||||
TCloudflareConnection,
|
||||
TCloudflareConnectionConfig,
|
||||
@@ -282,6 +288,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TNorthflankConnection
|
||||
| TOktaConnection
|
||||
| TRedisConnection
|
||||
| TChefConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@@ -330,6 +337,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TNorthflankConnectionInput
|
||||
| TOktaConnectionInput
|
||||
| TRedisConnectionInput
|
||||
| TChefConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput =
|
||||
@@ -395,7 +403,8 @@ export type TAppConnectionConfig =
|
||||
| TNetlifyConnectionConfig
|
||||
| TNorthflankConnectionConfig
|
||||
| TOktaConnectionConfig
|
||||
| TRedisConnectionConfig;
|
||||
| TRedisConnectionConfig
|
||||
| TChefConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@@ -438,7 +447,8 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateNetlifyConnectionCredentialsSchema
|
||||
| TValidateNorthflankConnectionCredentialsSchema
|
||||
| TValidateOktaConnectionCredentialsSchema
|
||||
| TValidateRedisConnectionCredentialsSchema;
|
||||
| TValidateRedisConnectionCredentialsSchema
|
||||
| TValidateChefConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum ChefConnectionMethod {
|
||||
UserKey = "user-key"
|
||||
}
|
||||
288
backend/src/services/app-connection/chef/chef-connection-fns.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import { AxiosError } from "axios";
|
||||
import crypto from "crypto";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { TChefDataBagItemContent } from "../../secret-sync/chef/chef-sync-types";
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { ChefConnectionMethod } from "./chef-connection-enums";
|
||||
import {
|
||||
TChefConnection,
|
||||
TChefConnectionConfig,
|
||||
TChefDataBag,
|
||||
TChefDataBagItem,
|
||||
TGetChefDataBagItem,
|
||||
TUpdateChefDataBagItem
|
||||
} from "./chef-connection-types";
|
||||
|
||||
export const getChefServerUrl = async (serverUrl?: string) => {
|
||||
const chefServerUrl = serverUrl ? removeTrailingSlash(serverUrl) : IntegrationUrls.CHEF_API_URL;
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(chefServerUrl);
|
||||
|
||||
return chefServerUrl;
|
||||
};
|
||||
|
||||
// Helper to ensure private key is in proper PEM format
|
||||
const formatPrivateKey = (key: string): string => {
|
||||
let formattedKey = key.trim();
|
||||
|
||||
// Ensure proper line breaks in PEM format (handle escaped newlines)
|
||||
formattedKey = formattedKey.replace(/\\n/g, "\n");
|
||||
|
||||
// Remove any extra whitespace between lines
|
||||
formattedKey = formattedKey.replace(/\n\s+/g, "\n");
|
||||
|
||||
// If key doesn't have headers, add PKCS#1 RSA headers
|
||||
if (!formattedKey.includes("BEGIN")) {
|
||||
formattedKey = `-----BEGIN RSA PRIVATE KEY-----\n${formattedKey}\n-----END RSA PRIVATE KEY-----`;
|
||||
}
|
||||
|
||||
// Ensure the key has proper line breaks after headers and before footers
|
||||
formattedKey = formattedKey.replace(/(-----BEGIN[^-]+-----)\s*/g, "$1\n").replace(/\s*(-----END[^-]+-----)/g, "\n$1");
|
||||
|
||||
// Remove any duplicate newlines
|
||||
formattedKey = formattedKey.replace(/\n{3,}/g, "\n\n");
|
||||
|
||||
return formattedKey;
|
||||
};
|
||||
|
||||
const getChefAuthHeaders = (
|
||||
method: string,
|
||||
path: string,
|
||||
body: string,
|
||||
userId: string,
|
||||
privateKey: string,
|
||||
apiVersion: "1.0" | "1.3" = "1.3"
|
||||
) => {
|
||||
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); // Remove milliseconds from timestamp
|
||||
|
||||
// Calculate content hash based on version
|
||||
let contentHash: string;
|
||||
if (apiVersion === "1.3") {
|
||||
contentHash = crypto.createHash("sha256").update(body).digest("base64");
|
||||
} else {
|
||||
contentHash = crypto.createHash("sha1").update(body).digest("base64");
|
||||
}
|
||||
|
||||
// Build canonical request based on version
|
||||
let canonicalRequest: string;
|
||||
if (apiVersion === "1.3") {
|
||||
canonicalRequest = [
|
||||
`Method:${method}`,
|
||||
`Path:${path}`,
|
||||
`X-Ops-Content-Hash:${contentHash}`,
|
||||
"X-Ops-Sign:version=1.3",
|
||||
`X-Ops-Timestamp:${timestamp}`,
|
||||
`X-Ops-UserId:${userId}`,
|
||||
"X-Ops-Server-API-Version:1"
|
||||
].join("\n");
|
||||
} else {
|
||||
const hashedPath = crypto.createHash("sha1").update(path).digest("base64");
|
||||
canonicalRequest = [
|
||||
`Method:${method}`,
|
||||
`Hashed Path:${hashedPath}`,
|
||||
`X-Ops-Content-Hash:${contentHash}`,
|
||||
`X-Ops-Timestamp:${timestamp}`,
|
||||
`X-Ops-UserId:${userId}`
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
// Format the private key properly
|
||||
const formattedKey = formatPrivateKey(privateKey);
|
||||
|
||||
// Sign the canonical request
|
||||
const sign = crypto.createSign(apiVersion === "1.3" ? "RSA-SHA256" : "RSA-SHA1");
|
||||
sign.update(canonicalRequest);
|
||||
const signature = sign.sign(formattedKey, "base64");
|
||||
|
||||
// Split signature into 60-character chunks
|
||||
const authHeaders: Record<string, string> = {};
|
||||
const signatureLines = signature.match(/.{1,60}/g) || [];
|
||||
signatureLines.forEach((line, index) => {
|
||||
authHeaders[`X-Ops-Authorization-${index + 1}`] = line;
|
||||
});
|
||||
|
||||
return {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"X-Chef-Version": "14.0.0",
|
||||
"X-Ops-Timestamp": timestamp,
|
||||
"X-Ops-UserId": userId,
|
||||
"X-Ops-Sign": apiVersion === "1.3" ? "version=1.3" : "algorithm=sha1;version=1.0",
|
||||
"X-Ops-Content-Hash": contentHash,
|
||||
...(apiVersion === "1.3" && { "X-Ops-Server-API-Version": "1" }),
|
||||
...authHeaders
|
||||
};
|
||||
};
|
||||
|
||||
export const getChefConnectionListItem = () => {
|
||||
return {
|
||||
name: "Chef" as const,
|
||||
app: AppConnection.Chef as const,
|
||||
methods: Object.values(ChefConnectionMethod) as [ChefConnectionMethod.UserKey]
|
||||
};
|
||||
};
|
||||
|
||||
export const validateChefConnectionCredentials = async (config: TChefConnectionConfig) => {
|
||||
const { credentials: inputCredentials } = config;
|
||||
|
||||
try {
|
||||
const path = `/organizations/${inputCredentials.orgName}/users/${inputCredentials.userName}`;
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(inputCredentials.serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("GET", path, "", inputCredentials.userName, inputCredentials.privateKey);
|
||||
|
||||
await request.get(`${hostServerUrl}${path}`, {
|
||||
headers
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate Chef credentials: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate Chef connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return inputCredentials;
|
||||
};
|
||||
|
||||
export const listChefDataBags = async (appConnection: TChefConnection): Promise<TChefDataBag[]> => {
|
||||
const {
|
||||
credentials: { serverUrl, userName, privateKey, orgName }
|
||||
} = appConnection;
|
||||
|
||||
try {
|
||||
const path = `/organizations/${orgName}/data`;
|
||||
const body = "";
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey);
|
||||
|
||||
const res = await request.get<Record<string, string>>(`${hostServerUrl}${path}`, {
|
||||
headers
|
||||
});
|
||||
|
||||
return Object.keys(res.data).map((name) => ({
|
||||
name
|
||||
}));
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to list Chef data bags: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list Chef data bags"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const listChefDataBagItems = async (
|
||||
appConnection: TChefConnection,
|
||||
dataBagName: string
|
||||
): Promise<TChefDataBagItem[]> => {
|
||||
const {
|
||||
credentials: { serverUrl, userName, privateKey, orgName }
|
||||
} = appConnection;
|
||||
|
||||
try {
|
||||
const path = `/organizations/${orgName}/data/${dataBagName}`;
|
||||
const body = "";
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey);
|
||||
|
||||
const res = await request.get<Record<string, string>>(`${hostServerUrl}${path}`, {
|
||||
headers
|
||||
});
|
||||
|
||||
return Object.keys(res.data).map((name) => ({
|
||||
name
|
||||
}));
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to list Chef data bag items: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list Chef data bag items"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getChefDataBagItem = async ({
|
||||
serverUrl,
|
||||
userName,
|
||||
privateKey,
|
||||
orgName,
|
||||
dataBagName,
|
||||
dataBagItemName
|
||||
}: TGetChefDataBagItem): Promise<TChefDataBagItemContent> => {
|
||||
try {
|
||||
const path = `/organizations/${orgName}/data/${dataBagName}/${dataBagItemName}`;
|
||||
const body = "";
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey);
|
||||
|
||||
const res = await request.get<TChefDataBagItemContent>(`${hostServerUrl}${path}`, {
|
||||
headers
|
||||
});
|
||||
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to get Chef data bag item: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to get Chef data bag item"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateChefDataBagItem = async ({
|
||||
serverUrl,
|
||||
userName,
|
||||
privateKey,
|
||||
orgName,
|
||||
dataBagName,
|
||||
dataBagItemName,
|
||||
data
|
||||
}: TUpdateChefDataBagItem): Promise<void> => {
|
||||
try {
|
||||
const path = `/organizations/${orgName}/data/${dataBagName}/${dataBagItemName}`;
|
||||
const body = JSON.stringify(data);
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("PUT", path, body, userName, privateKey);
|
||||
|
||||
await request.put(`${hostServerUrl}${path}`, data, {
|
||||
headers
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to update Chef data bag item: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to update Chef data bag item"
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { ChefConnectionMethod } from "./chef-connection-enums";
|
||||
|
||||
export const ChefConnectionUserKeyCredentialsSchema = z.object({
|
||||
serverUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.url("Valid Chef Server URL required")
|
||||
.optional()
|
||||
.describe(AppConnections.CREDENTIALS.CHEF.serverUrl),
|
||||
orgName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Organization name required")
|
||||
.max(256, "Organization name cannot exceed 256 characters")
|
||||
.describe(AppConnections.CREDENTIALS.CHEF.orgName),
|
||||
userName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "User name required")
|
||||
.max(256, "User name cannot exceed 256 characters")
|
||||
.describe(AppConnections.CREDENTIALS.CHEF.userName),
|
||||
privateKey: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Private key required")
|
||||
.max(16384, "Private key cannot exceed 16384 characters")
|
||||
.describe(AppConnections.CREDENTIALS.CHEF.privateKey)
|
||||
});
|
||||
|
||||
const BaseChefConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Chef) });
|
||||
|
||||
export const ChefConnectionSchema = BaseChefConnectionSchema.extend({
|
||||
method: z.literal(ChefConnectionMethod.UserKey),
|
||||
credentials: ChefConnectionUserKeyCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedChefConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseChefConnectionSchema.extend({
|
||||
method: z.literal(ChefConnectionMethod.UserKey),
|
||||
credentials: ChefConnectionUserKeyCredentialsSchema.pick({ serverUrl: true, orgName: true, userName: true })
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateChefConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(ChefConnectionMethod.UserKey).describe(AppConnections.CREATE(AppConnection.Chef).method),
|
||||
credentials: ChefConnectionUserKeyCredentialsSchema.describe(AppConnections.CREATE(AppConnection.Chef).credentials)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateChefConnectionSchema = ValidateChefConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.Chef)
|
||||
);
|
||||
|
||||
export const UpdateChefConnectionSchema = z
|
||||
.object({
|
||||
credentials: ChefConnectionUserKeyCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.Chef).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Chef));
|
||||
|
||||
export const ChefConnectionListItemSchema = z.object({
|
||||
name: z.literal("Chef"),
|
||||
app: z.literal(AppConnection.Chef),
|
||||
methods: z.nativeEnum(ChefConnectionMethod).array()
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ForbiddenRequestError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listChefDataBagItems, listChefDataBags } from "./chef-connection-fns";
|
||||
import { TChefConnection } from "./chef-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TChefConnection>;
|
||||
|
||||
export const chefConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listDataBags = async (appConnectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Chef, appConnectionId, actor);
|
||||
|
||||
if (!appConnection) {
|
||||
throw new ForbiddenRequestError({ message: "App connection not found" });
|
||||
}
|
||||
|
||||
return listChefDataBags(appConnection);
|
||||
};
|
||||
|
||||
const listDataBagItems = async (appConnectionId: string, dataBagName: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Chef, appConnectionId, actor);
|
||||
|
||||
if (!appConnection) {
|
||||
throw new ForbiddenRequestError({ message: "App connection not found" });
|
||||
}
|
||||
|
||||
return listChefDataBagItems(appConnection, dataBagName);
|
||||
};
|
||||
|
||||
return {
|
||||
listDataBags,
|
||||
listDataBagItems
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
import { TChefDataBagItemContent } from "@app/services/secret-sync/chef";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
ChefConnectionSchema,
|
||||
CreateChefConnectionSchema,
|
||||
ValidateChefConnectionCredentialsSchema
|
||||
} from "./chef-connection-schemas";
|
||||
|
||||
export type TChefConnection = z.infer<typeof ChefConnectionSchema>;
|
||||
|
||||
export type TChefConnectionInput = z.infer<typeof CreateChefConnectionSchema> & {
|
||||
app: AppConnection.Chef;
|
||||
};
|
||||
|
||||
export type TValidateChefConnectionCredentialsSchema = typeof ValidateChefConnectionCredentialsSchema;
|
||||
|
||||
export type TChefConnectionConfig = DiscriminativePick<TChefConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgName: string;
|
||||
};
|
||||
|
||||
export type TChefDataBag = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TChefDataBagItem = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TGetChefDataBagItem = {
|
||||
serverUrl?: string;
|
||||
userName: string;
|
||||
privateKey: string;
|
||||
orgName: string;
|
||||
dataBagName: string;
|
||||
dataBagItemName: string;
|
||||
};
|
||||
|
||||
export type TUpdateChefDataBagItem = {
|
||||
serverUrl?: string;
|
||||
userName: string;
|
||||
privateKey: string;
|
||||
orgName: string;
|
||||
dataBagName: string;
|
||||
dataBagItemName: string;
|
||||
data: TChefDataBagItemContent;
|
||||
};
|
||||
4
backend/src/services/app-connection/chef/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./chef-connection-enums";
|
||||
export * from "./chef-connection-fns";
|
||||
export * from "./chef-connection-schemas";
|
||||
export * from "./chef-connection-types";
|
||||
@@ -104,7 +104,8 @@ export enum IntegrationUrls {
|
||||
GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com",
|
||||
GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform",
|
||||
|
||||
GITHUB_USER_INSTALLATIONS = "https://api.github.com/user/installations"
|
||||
GITHUB_USER_INSTALLATIONS = "https://api.github.com/user/installations",
|
||||
CHEF_API_URL = "https://api.chef.io"
|
||||
}
|
||||
|
||||
export const getIntegrationOptions = async () => {
|
||||
|
||||
10
backend/src/services/secret-sync/chef/chef-sync-constants.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const CHEF_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "Chef",
|
||||
destination: SecretSync.Chef,
|
||||
connection: AppConnection.Chef,
|
||||
canImportSecrets: true
|
||||
};
|
||||
151
backend/src/services/secret-sync/chef/chef-sync-fns.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { getChefDataBagItem, updateChefDataBagItem } from "@app/services/app-connection/chef";
|
||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import {
|
||||
ChefSecret,
|
||||
TChefDataBagItemContent,
|
||||
TChefSecret,
|
||||
TChefSecrets,
|
||||
TChefSyncWithCredentials,
|
||||
TGetChefSecrets
|
||||
} from "./chef-sync-types";
|
||||
|
||||
const getChefSecretsRaw = async ({
|
||||
serverUrl,
|
||||
userName,
|
||||
privateKey,
|
||||
orgName,
|
||||
dataBagName,
|
||||
dataBagItemName
|
||||
}: TGetChefSecrets): Promise<TChefDataBagItemContent> => {
|
||||
const dataBagItem = await getChefDataBagItem({
|
||||
serverUrl,
|
||||
userName,
|
||||
privateKey,
|
||||
orgName,
|
||||
dataBagName,
|
||||
dataBagItemName
|
||||
});
|
||||
|
||||
// Ensure the data bag item has an id field
|
||||
if (!dataBagItem.id) {
|
||||
dataBagItem.id = dataBagItemName;
|
||||
}
|
||||
|
||||
return dataBagItem;
|
||||
};
|
||||
|
||||
const getChefSecrets = async (secretSync: TChefSyncWithCredentials): Promise<TChefSecrets> => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { dataBagName, dataBagItemName }
|
||||
} = secretSync;
|
||||
|
||||
const { serverUrl, userName, privateKey, orgName } = connection.credentials;
|
||||
|
||||
const dataBagItem = await getChefSecretsRaw({
|
||||
serverUrl,
|
||||
orgName,
|
||||
userName,
|
||||
privateKey,
|
||||
dataBagName,
|
||||
dataBagItemName
|
||||
});
|
||||
|
||||
const { id, ...existingSecrets } = dataBagItem;
|
||||
|
||||
// Convert data bag item to key-value pairs
|
||||
const secrets: ChefSecret[] = [];
|
||||
Object.entries(existingSecrets).forEach(([key, value]) => {
|
||||
if (key !== "id" && value !== null && value !== undefined) {
|
||||
secrets.push({ key, value: String(value) });
|
||||
}
|
||||
});
|
||||
|
||||
return { id, secrets };
|
||||
};
|
||||
|
||||
const updateChefSecrets = async (
|
||||
secretSync: TChefSyncWithCredentials,
|
||||
id: string,
|
||||
secrets: Record<string, TChefSecret>
|
||||
) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { dataBagName, dataBagItemName }
|
||||
} = secretSync;
|
||||
|
||||
const { serverUrl, userName, privateKey, orgName } = connection.credentials;
|
||||
|
||||
// Chef data bag items must have an 'id' field
|
||||
const dataBagItemContent: TChefDataBagItemContent = {
|
||||
id,
|
||||
...secrets
|
||||
};
|
||||
|
||||
await updateChefDataBagItem({
|
||||
serverUrl,
|
||||
orgName,
|
||||
userName,
|
||||
privateKey,
|
||||
dataBagName,
|
||||
dataBagItemName,
|
||||
data: dataBagItemContent
|
||||
});
|
||||
};
|
||||
|
||||
export const ChefSyncFns = {
|
||||
async syncSecrets(secretSync: TChefSyncWithCredentials, secretMap: TSecretMap) {
|
||||
const {
|
||||
environment,
|
||||
syncOptions: { disableSecretDeletion, keySchema }
|
||||
} = secretSync;
|
||||
|
||||
const { id, secrets } = await getChefSecrets(secretSync);
|
||||
|
||||
// Create a map of the existing secrets
|
||||
const updatedSecretsMap = new Map(secrets.map((secret) => [secret.key, secret.value]));
|
||||
|
||||
// Add/update new secrets
|
||||
for (const [key, { value }] of Object.entries(secretMap)) {
|
||||
updatedSecretsMap.set(key, value);
|
||||
}
|
||||
|
||||
// Delete secrets if not disabled
|
||||
if (!disableSecretDeletion) {
|
||||
secrets.forEach((secret) => {
|
||||
if (!matchesSchema(secret.key, environment?.slug || "", keySchema)) return;
|
||||
|
||||
if (!secretMap[secret.key]) {
|
||||
updatedSecretsMap.delete(secret.key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Convert map to object for Chef API
|
||||
const updatedSecrets = Object.fromEntries(updatedSecretsMap.entries());
|
||||
|
||||
await updateChefSecrets(secretSync, id, updatedSecrets);
|
||||
},
|
||||
|
||||
async getSecrets(secretSync: TChefSyncWithCredentials): Promise<TSecretMap> {
|
||||
const { secrets } = await getChefSecrets(secretSync);
|
||||
|
||||
return Object.fromEntries(secrets.map((secret) => [secret.key, { value: secret.value }]));
|
||||
},
|
||||
|
||||
async removeSecrets(secretSync: TChefSyncWithCredentials, secretMap: TSecretMap) {
|
||||
const { id, secrets: existingSecrets } = await getChefSecrets(secretSync);
|
||||
|
||||
const newSecrets = existingSecrets.filter((secret) => !Object.hasOwn(secretMap, secret.key));
|
||||
|
||||
if (newSecrets.length === existingSecrets.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedSecrets = Object.fromEntries(newSecrets.map((secret) => [secret.key, secret.value]));
|
||||
|
||||
await updateChefSecrets(secretSync, id, updatedSecrets);
|
||||
}
|
||||
};
|
||||
46
backend/src/services/secret-sync/chef/chef-sync-schemas.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const ChefSyncDestinationConfigSchema = z.object({
|
||||
dataBagName: z
|
||||
.string()
|
||||
.min(1, "Data Bag Name is required")
|
||||
.max(256, "Data Bag Name cannot exceed 256 characters")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.CHEF.dataBagName),
|
||||
dataBagItemName: z
|
||||
.string()
|
||||
.min(1, "Data Bag Item Name is required")
|
||||
.max(256, "Data Bag Item Name cannot exceed 256 characters")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.CHEF.dataBagItemName)
|
||||
});
|
||||
|
||||
const ChefSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const ChefSyncSchema = BaseSecretSyncSchema(SecretSync.Chef, ChefSyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.Chef),
|
||||
destinationConfig: ChefSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateChefSyncSchema = GenericCreateSecretSyncFieldsSchema(SecretSync.Chef, ChefSyncOptionsConfig).extend({
|
||||
destinationConfig: ChefSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateChefSyncSchema = GenericUpdateSecretSyncFieldsSchema(SecretSync.Chef, ChefSyncOptionsConfig).extend({
|
||||
destinationConfig: ChefSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const ChefSyncListItemSchema = z.object({
|
||||
name: z.literal("Chef"),
|
||||
connection: z.literal(AppConnection.Chef),
|
||||
destination: z.literal(SecretSync.Chef),
|
||||
canImportSecrets: z.literal(true)
|
||||
});
|
||||
41
backend/src/services/secret-sync/chef/chef-sync-types.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import z from "zod";
|
||||
|
||||
import { TChefConnection } from "@app/services/app-connection/chef";
|
||||
|
||||
import { ChefSyncListItemSchema, ChefSyncSchema, CreateChefSyncSchema } from "./chef-sync-schemas";
|
||||
|
||||
export type TChefSyncListItem = z.infer<typeof ChefSyncListItemSchema>;
|
||||
|
||||
export type TChefSync = z.infer<typeof ChefSyncSchema>;
|
||||
|
||||
export type TChefSyncInput = z.infer<typeof CreateChefSyncSchema>;
|
||||
|
||||
export type TChefSyncWithCredentials = TChefSync & {
|
||||
connection: TChefConnection;
|
||||
};
|
||||
|
||||
export type TGetChefSecrets = {
|
||||
serverUrl?: string;
|
||||
userName: string;
|
||||
privateKey: string;
|
||||
orgName: string;
|
||||
dataBagName: string;
|
||||
dataBagItemName: string;
|
||||
};
|
||||
|
||||
export type TChefSecret = string | number | boolean | null;
|
||||
|
||||
export type TChefDataBagItemContent = {
|
||||
id: string;
|
||||
[key: string]: TChefSecret;
|
||||
};
|
||||
|
||||
export type TChefSecrets = {
|
||||
id: string;
|
||||
secrets: ChefSecret[];
|
||||
};
|
||||
|
||||
export type ChefSecret = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
4
backend/src/services/secret-sync/chef/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./chef-sync-constants";
|
||||
export * from "./chef-sync-fns";
|
||||
export * from "./chef-sync-schemas";
|
||||
export * from "./chef-sync-types";
|
||||
@@ -30,7 +30,8 @@ export enum SecretSync {
|
||||
Netlify = "netlify",
|
||||
Northflank = "northflank",
|
||||
Bitbucket = "bitbucket",
|
||||
LaravelForge = "laravel-forge"
|
||||
LaravelForge = "laravel-forge",
|
||||
Chef = "chef"
|
||||
}
|
||||
|
||||
export enum SecretSyncInitialSyncBehavior {
|
||||
|
||||
@@ -34,6 +34,7 @@ import { BITBUCKET_SYNC_LIST_OPTION, BitbucketSyncFns } from "./bitbucket";
|
||||
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
|
||||
import { CHECKLY_SYNC_LIST_OPTION } from "./checkly/checkly-sync-constants";
|
||||
import { ChecklySyncFns } from "./checkly/checkly-sync-fns";
|
||||
import { CHEF_SYNC_LIST_OPTION, ChefSyncFns } from "./chef";
|
||||
import { CLOUDFLARE_PAGES_SYNC_LIST_OPTION } from "./cloudflare-pages/cloudflare-pages-constants";
|
||||
import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns";
|
||||
import { CLOUDFLARE_WORKERS_SYNC_LIST_OPTION, CloudflareWorkersSyncFns } from "./cloudflare-workers";
|
||||
@@ -49,8 +50,7 @@ import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault";
|
||||
import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku";
|
||||
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
||||
import { LARAVEL_FORGE_SYNC_LIST_OPTION } from "./laravel-forge";
|
||||
import { LaravelForgeSyncFns } from "./laravel-forge/laravel-forge-sync-fns";
|
||||
import { LARAVEL_FORGE_SYNC_LIST_OPTION, LaravelForgeSyncFns } from "./laravel-forge";
|
||||
import { NETLIFY_SYNC_LIST_OPTION, NetlifySyncFns } from "./netlify";
|
||||
import { NORTHFLANK_SYNC_LIST_OPTION, NorthflankSyncFns } from "./northflank";
|
||||
import { RAILWAY_SYNC_LIST_OPTION } from "./railway/railway-sync-constants";
|
||||
@@ -96,7 +96,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.Netlify]: NETLIFY_SYNC_LIST_OPTION,
|
||||
[SecretSync.Northflank]: NORTHFLANK_SYNC_LIST_OPTION,
|
||||
[SecretSync.Bitbucket]: BITBUCKET_SYNC_LIST_OPTION,
|
||||
[SecretSync.LaravelForge]: LARAVEL_FORGE_SYNC_LIST_OPTION
|
||||
[SecretSync.LaravelForge]: LARAVEL_FORGE_SYNC_LIST_OPTION,
|
||||
[SecretSync.Chef]: CHEF_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretSyncOptions = () => {
|
||||
@@ -286,6 +287,8 @@ export const SecretSyncFns = {
|
||||
return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.LaravelForge:
|
||||
return LaravelForgeSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Chef:
|
||||
return ChefSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -408,6 +411,9 @@ export const SecretSyncFns = {
|
||||
case SecretSync.LaravelForge:
|
||||
secretMap = await LaravelForgeSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.Chef:
|
||||
secretMap = await ChefSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -505,6 +511,8 @@ export const SecretSyncFns = {
|
||||
return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.LaravelForge:
|
||||
return LaravelForgeSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Chef:
|
||||
return ChefSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
|
||||
@@ -34,7 +34,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.Netlify]: "Netlify",
|
||||
[SecretSync.Northflank]: "Northflank",
|
||||
[SecretSync.Bitbucket]: "Bitbucket",
|
||||
[SecretSync.LaravelForge]: "Laravel Forge"
|
||||
[SecretSync.LaravelForge]: "Laravel Forge",
|
||||
[SecretSync.Chef]: "Chef"
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
@@ -69,7 +70,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.Netlify]: AppConnection.Netlify,
|
||||
[SecretSync.Northflank]: AppConnection.Northflank,
|
||||
[SecretSync.Bitbucket]: AppConnection.Bitbucket,
|
||||
[SecretSync.LaravelForge]: AppConnection.LaravelForge
|
||||
[SecretSync.LaravelForge]: AppConnection.LaravelForge,
|
||||
[SecretSync.Chef]: AppConnection.Chef
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
@@ -104,7 +106,8 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
[SecretSync.Netlify]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Northflank]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Bitbucket]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.LaravelForge]: SecretSyncPlanType.Regular
|
||||
[SecretSync.LaravelForge]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Chef]: SecretSyncPlanType.Regular
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_SKIP_FIELDS_MAP: Record<SecretSync, string[]> = {
|
||||
@@ -148,7 +151,8 @@ export const SECRET_SYNC_SKIP_FIELDS_MAP: Record<SecretSync, string[]> = {
|
||||
[SecretSync.Netlify]: ["accountName", "siteName"],
|
||||
[SecretSync.Northflank]: [],
|
||||
[SecretSync.Bitbucket]: [],
|
||||
[SecretSync.LaravelForge]: []
|
||||
[SecretSync.LaravelForge]: [],
|
||||
[SecretSync.Chef]: []
|
||||
};
|
||||
|
||||
const defaultDuplicateCheck: DestinationDuplicateCheckFn = () => true;
|
||||
@@ -209,5 +213,6 @@ export const DESTINATION_DUPLICATE_CHECK_MAP: Record<SecretSync, DestinationDupl
|
||||
[SecretSync.Netlify]: defaultDuplicateCheck,
|
||||
[SecretSync.Northflank]: defaultDuplicateCheck,
|
||||
[SecretSync.Bitbucket]: defaultDuplicateCheck,
|
||||
[SecretSync.LaravelForge]: defaultDuplicateCheck
|
||||
[SecretSync.LaravelForge]: defaultDuplicateCheck,
|
||||
[SecretSync.Chef]: defaultDuplicateCheck
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
TCamundaSyncListItem,
|
||||
TCamundaSyncWithCredentials
|
||||
} from "@app/services/secret-sync/camunda";
|
||||
import { TChefSync, TChefSyncInput, TChefSyncListItem, TChefSyncWithCredentials } from "@app/services/secret-sync/chef";
|
||||
import {
|
||||
TDatabricksSync,
|
||||
TDatabricksSyncInput,
|
||||
@@ -169,6 +170,7 @@ export type TSecretSync =
|
||||
| TGitHubSync
|
||||
| TGcpSync
|
||||
| TAzureKeyVaultSync
|
||||
| TChefSync
|
||||
| TAzureAppConfigurationSync
|
||||
| TAzureDevOpsSync
|
||||
| TDatabricksSync
|
||||
@@ -202,6 +204,7 @@ export type TSecretSyncWithCredentials =
|
||||
| TGitHubSyncWithCredentials
|
||||
| TGcpSyncWithCredentials
|
||||
| TAzureKeyVaultSyncWithCredentials
|
||||
| TChefSyncWithCredentials
|
||||
| TAzureAppConfigurationSyncWithCredentials
|
||||
| TAzureDevOpsSyncWithCredentials
|
||||
| TDatabricksSyncWithCredentials
|
||||
@@ -236,6 +239,7 @@ export type TSecretSyncInput =
|
||||
| TGitHubSyncInput
|
||||
| TGcpSyncInput
|
||||
| TAzureKeyVaultSyncInput
|
||||
| TChefSyncInput
|
||||
| TAzureAppConfigurationSyncInput
|
||||
| TAzureDevOpsSyncInput
|
||||
| TDatabricksSyncInput
|
||||
@@ -270,6 +274,7 @@ export type TSecretSyncListItem =
|
||||
| TGitHubSyncListItem
|
||||
| TGcpSyncListItem
|
||||
| TAzureKeyVaultSyncListItem
|
||||
| TChefSyncListItem
|
||||
| TAzureAppConfigurationSyncListItem
|
||||
| TAzureDevOpsSyncListItem
|
||||
| TDatabricksSyncListItem
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/chef/available"
|
||||
---
|
||||
10
docs/api-reference/endpoints/app-connections/chef/create.mdx
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/app-connections/chef"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [Chef
|
||||
Connections](/integrations/app-connections/chef) to learn how to obtain the
|
||||
required credentials.
|
||||
</Note>
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/chef/{connectionId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/chef/{connectionId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/chef/connection-name/{connectionName}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/chef"
|
||||
---
|
||||
10
docs/api-reference/endpoints/app-connections/chef/update.mdx
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/app-connections/chef/{connectionId}"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [Chef
|
||||
Connections](/integrations/app-connections/chef) to learn how to obtain the
|
||||
required credentials.
|
||||
</Note>
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/secret-syncs/chef"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/secret-syncs/chef/{syncId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/secret-syncs/chef/{syncId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/secret-syncs/chef/sync-name/{syncName}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Import Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/chef/{syncId}/import-secrets"
|
||||
---
|
||||
4
docs/api-reference/endpoints/secret-syncs/chef/list.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/secret-syncs/chef"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Remove Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/chef/{syncId}/remove-secrets"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Sync Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/chef/{syncId}/sync-secrets"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/secret-syncs/chef/{syncId}"
|
||||
---
|
||||
@@ -114,6 +114,7 @@
|
||||
"integrations/app-connections/bitbucket",
|
||||
"integrations/app-connections/camunda",
|
||||
"integrations/app-connections/checkly",
|
||||
"integrations/app-connections/chef",
|
||||
"integrations/app-connections/cloudflare",
|
||||
"integrations/app-connections/databricks",
|
||||
"integrations/app-connections/digital-ocean",
|
||||
@@ -540,6 +541,7 @@
|
||||
"integrations/secret-syncs/bitbucket",
|
||||
"integrations/secret-syncs/camunda",
|
||||
"integrations/secret-syncs/checkly",
|
||||
"integrations/secret-syncs/chef",
|
||||
"integrations/secret-syncs/cloudflare-pages",
|
||||
"integrations/secret-syncs/cloudflare-workers",
|
||||
"integrations/secret-syncs/databricks",
|
||||
@@ -1658,6 +1660,18 @@
|
||||
"api-reference/endpoints/app-connections/checkly/delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Chef",
|
||||
"pages": [
|
||||
"api-reference/endpoints/app-connections/chef/list",
|
||||
"api-reference/endpoints/app-connections/chef/available",
|
||||
"api-reference/endpoints/app-connections/chef/get-by-id",
|
||||
"api-reference/endpoints/app-connections/chef/get-by-name",
|
||||
"api-reference/endpoints/app-connections/chef/create",
|
||||
"api-reference/endpoints/app-connections/chef/update",
|
||||
"api-reference/endpoints/app-connections/chef/delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Cloudflare",
|
||||
"pages": [
|
||||
@@ -2149,6 +2163,20 @@
|
||||
"api-reference/endpoints/secret-syncs/checkly/remove-secrets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Chef",
|
||||
"pages": [
|
||||
"api-reference/endpoints/secret-syncs/chef/list",
|
||||
"api-reference/endpoints/secret-syncs/chef/get-by-id",
|
||||
"api-reference/endpoints/secret-syncs/chef/get-by-name",
|
||||
"api-reference/endpoints/secret-syncs/chef/create",
|
||||
"api-reference/endpoints/secret-syncs/chef/update",
|
||||
"api-reference/endpoints/secret-syncs/chef/delete",
|
||||
"api-reference/endpoints/secret-syncs/chef/sync-secrets",
|
||||
"api-reference/endpoints/secret-syncs/chef/import-secrets",
|
||||
"api-reference/endpoints/secret-syncs/chef/remove-secrets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Cloudflare Pages",
|
||||
"pages": [
|
||||
@@ -2304,6 +2332,7 @@
|
||||
"api-reference/endpoints/secret-syncs/laravel-forge/update",
|
||||
"api-reference/endpoints/secret-syncs/laravel-forge/delete",
|
||||
"api-reference/endpoints/secret-syncs/laravel-forge/sync-secrets",
|
||||
"api-reference/endpoints/secret-syncs/laravel-forge/import-secrets",
|
||||
"api-reference/endpoints/secret-syncs/laravel-forge/remove-secrets"
|
||||
]
|
||||
},
|
||||
|
||||
BIN
docs/images/app-connections/chef/app-connection-form.png
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
docs/images/app-connections/chef/app-connection-generated.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
docs/images/app-connections/chef/app-connection-option.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
docs/images/app-connections/chef/chef-connection-details.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
docs/images/app-connections/chef/chef-dashboard.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
docs/images/app-connections/chef/chef-folder.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/images/app-connections/chef/download-starter-kit.png
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
docs/images/app-connections/chef/extract-starter-kit.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/images/app-connections/chef/private-key-file.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/images/app-connections/chef/starter-kit.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
docs/images/secret-syncs/chef/select-option.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
docs/images/secret-syncs/chef/sync-created.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
docs/images/secret-syncs/chef/sync-destination.png
Normal file
|
After Width: | Height: | Size: 199 KiB |
BIN
docs/images/secret-syncs/chef/sync-details.png
Normal file
|
After Width: | Height: | Size: 199 KiB |
BIN
docs/images/secret-syncs/chef/sync-options.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
docs/images/secret-syncs/chef/sync-review.png
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
docs/images/secret-syncs/chef/sync-source.png
Normal file
|
After Width: | Height: | Size: 207 KiB |
142
docs/integrations/app-connections/chef.mdx
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
title: "Chef Connection"
|
||||
description: "Learn how to configure a Chef Connection for Infisical."
|
||||
---
|
||||
|
||||
Infisical supports the use of User Private Key to connect with Chef Server.
|
||||
|
||||
Please access your **starter kit** to get all the required information to create a Chef Connection.
|
||||
|
||||
<Accordion title="If you don't have a starter kit">
|
||||
<Warning>
|
||||
If you download a new starter kit, your previous private key/user key will
|
||||
no longer be valid. Please make sure to update all the places that use the
|
||||
previous private key.
|
||||
</Warning>
|
||||
<Steps>
|
||||
<Step title="Navigate to your Chef Server Dashboard, and click on the 'Organizations' tab">
|
||||

|
||||
</Step>
|
||||
<Step title="Click on the organization you want to connect to, and then click on the 'Starter Kit' button">
|
||||

|
||||
</Step>
|
||||
<Step title="Click on the 'Download Starter Kit' button to download the starter kit">
|
||||

|
||||
</Step>
|
||||
<Step title='Download the starter kit zip file and extract the contents'>
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
<Steps>
|
||||
<Step title="Open your starter kit's folder">
|
||||
Open your starter kit's folder(or `chef-repo`) and navigate to the `.chef`
|
||||
folder.
|
||||
<Note>
|
||||
Please make sure you have hidden files visible in your file explorer.
|
||||
</Note>
|
||||

|
||||
</Step>
|
||||
<Step title='Copy the private key'>
|
||||
In the `.chef` folder, you will find a `[your-username].pem` file. 
|
||||
|
||||
**Private Key:** Copy the content of the private key file.
|
||||
|
||||
</Step>
|
||||
<Step title='Open the config.rb file'>
|
||||
Open the `config.rb` file and copy the content of the file.
|
||||

|
||||
|
||||
**User Name(1):** The user name of the chef user.
|
||||
|
||||
**Server URL(2):** The server url of the chef server.
|
||||
|
||||
**Organization Name(3):** The organization name of the chef server.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Create a Chef Connection in Infisical via UI
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Infisical UI">
|
||||
<Steps>
|
||||
<Step title="Navigate to App Connections">
|
||||
In your Infisical dashboard, navigate to the **App Connections** page in the desired project.
|
||||

|
||||
</Step>
|
||||
<Step title="Select Chef Connection">
|
||||
Click **+ Add Connection** and choose **Chef** Connection from the list of integrations.
|
||||

|
||||
</Step>
|
||||
<Step title="Fill out the Chef Connection form">
|
||||
Complete the form by providing:
|
||||
- A descriptive name for the connection
|
||||
- An optional description
|
||||
- Server URL(optional): The URL of the Chef server to connect with (defaults to https://api.chef.io)
|
||||
- Organization short name
|
||||
- User name
|
||||
- Private key: Your Chef user's private key (.pem file)
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Connection created">
|
||||
After submitting the form, your **Chef Connection** will be successfully created and ready to use with your Infisical project.
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="API">
|
||||
To create a Chef Connection via API, send a request to the [Create Chef Connection](/api-reference/endpoints/app-connections/chef/create) endpoint.
|
||||
|
||||
### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --request POST \
|
||||
--url https://app.infisical.com/api/v1/app-connections/chef \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "my-chef-connection",
|
||||
"method": "user-key",
|
||||
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
|
||||
"credentials": {
|
||||
"orgName": "my-org",
|
||||
"userName": "my-user",
|
||||
"privateKey": "your-private-key"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Sample response
|
||||
|
||||
```bash Response
|
||||
{
|
||||
"appConnection": {
|
||||
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
|
||||
"name": "my-chef-connection",
|
||||
"description": null,
|
||||
"projectId": "7ffbb072-2575-495a-b5b0-127f88caef78",
|
||||
"version": 1,
|
||||
"orgId": "abcdef12-3456-7890-abcd-ef1234567890",
|
||||
"createdAt": "2025-10-13T10:15:00.000Z",
|
||||
"updatedAt": "2025-10-13T10:15:00.000Z",
|
||||
"isPlatformManagedCredentials": false,
|
||||
"credentialsHash": "d41d8cd98f00b204e9800998ecf8427e",
|
||||
"app": "chef",
|
||||
"method": "user-key",
|
||||
"credentials": {
|
||||
"orgName": "my-org",
|
||||
"userName": "my-user",
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
154
docs/integrations/secret-syncs/chef.mdx
Normal file
@@ -0,0 +1,154 @@
|
||||
---
|
||||
title: "Chef Sync"
|
||||
description: "Learn how to configure a Chef Sync for Infisical."
|
||||
---
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Create a [Chef Connection](/integrations/app-connections/chef)
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Infisical UI">
|
||||
<Steps>
|
||||
<Step title="Add Sync">
|
||||
Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button.
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Select 'Chef'">
|
||||

|
||||
</Step>
|
||||
<Step title="Configure source">
|
||||
Configure the **Source** from where secrets should be retrieved, then click **Next**.
|
||||
|
||||

|
||||
|
||||
- **Environment**: The project environment to retrieve secrets from.
|
||||
- **Secret Path**: The folder path to retrieve secrets from.
|
||||
|
||||
<Tip>
|
||||
If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports).
|
||||
</Tip>
|
||||
</Step>
|
||||
<Step title="Configure destination">
|
||||
Configure the **Destination** to where secrets should be deployed, then click **Next**.
|
||||
|
||||

|
||||
|
||||
- **Chef Connection**: The Chef Connection to authenticate with.
|
||||
- **Data Bag**: The Data Bag to sync secrets to.
|
||||
- **Data Bag Item**: The Data Bag Item to sync secrets to.
|
||||
</Step>
|
||||
<Step title="Configure Sync Options">
|
||||
Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
|
||||
|
||||

|
||||
|
||||
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
|
||||
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
||||
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Chef when keys conflict.
|
||||
- **Import Secrets (Prioritize Chef)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Chef over Infisical when keys conflict.
|
||||
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
|
||||
<Note>
|
||||
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
|
||||
</Note>
|
||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
|
||||
</Step>
|
||||
<Step title="Configure details">
|
||||
Configure the **Details** of your Chef Sync, then click **Next**.
|
||||
|
||||

|
||||
|
||||
- **Name**: The name of your sync. Must be slug-friendly.
|
||||
- **Description**: An optional description for your sync.
|
||||
</Step>
|
||||
<Step title="Review configuration">
|
||||
Review your Chef Sync configuration, then click **Create Sync**.
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Sync created">
|
||||
If enabled, your Chef Sync will begin syncing your secrets to the destination endpoint.
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="API">
|
||||
To create a **Chef Sync**, make an API request to the [Create Chef Sync](/api-reference/endpoints/secret-syncs/chef/create) API endpoint.
|
||||
|
||||
### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --request POST \
|
||||
--url https://app.infisical.com/api/v1/secret-syncs/chef \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "my-chef-sync",
|
||||
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"description": "sync to chef site",
|
||||
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"environment": "dev",
|
||||
"secretPath": "/",
|
||||
"isEnabled": true,
|
||||
"isAutoSyncEnabled": true,
|
||||
"syncOptions": {
|
||||
"initialSyncBehavior": "overwrite-destination",
|
||||
"disableSecretDeletion": false
|
||||
},
|
||||
"destinationConfig": {
|
||||
"dataBagName": "my-data-bag",
|
||||
"dataBagItemName": "my-data-bag-item"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Sample response
|
||||
|
||||
```bash Response
|
||||
{
|
||||
"secretSync": {
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"name": "my-chef-sync",
|
||||
"description": "sync to chef site",
|
||||
"folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"createdAt": "2025-07-19T12:00:00Z",
|
||||
"updatedAt": "2025-07-19T12:00:00Z",
|
||||
"syncStatus": "succeeded",
|
||||
"lastSyncJobId": "job-1234",
|
||||
"lastSyncMessage": null,
|
||||
"lastSyncedAt": "2025-07-19T12:00:00Z",
|
||||
"syncOptions": {
|
||||
"initialSyncBehavior": "overwrite-destination",
|
||||
"disableSecretDeletion": false
|
||||
},
|
||||
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"connection": {
|
||||
"app": "chef",
|
||||
"name": "my-chef-connection",
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
|
||||
},
|
||||
"environment": {
|
||||
"slug": "dev",
|
||||
"name": "Development",
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
|
||||
},
|
||||
"folder": {
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"path": "/"
|
||||
},
|
||||
"destination": "chef",
|
||||
"destinationConfig": {
|
||||
"dataBagName": "my-data-bag",
|
||||
"dataBagItemName": "my-data-bag-item"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -47,6 +47,7 @@ export const AppConnectionsBrowser = () => {
|
||||
{"name": "Auth0", "slug": "auth0", "path": "/integrations/app-connections/auth0", "description": "Learn how to connect your Auth0 to pull secrets from Infisical.", "category": "Identity & Auth"},
|
||||
{"name": "Okta", "slug": "okta", "path": "/integrations/app-connections/okta", "description": "Learn how to connect your Okta to pull secrets from Infisical.", "category": "Identity & Auth"},
|
||||
{"name": "Laravel Forge", "slug": "laravel-forge", "path": "/integrations/app-connections/laravel-forge", "description": "Learn how to connect your Laravel Forge to pull secrets from Infisical.", "category": "Hosting"},
|
||||
{"name": "Chef", "slug": "chef", "path": "/integrations/app-connections/chef", "description": "Learn how to connect your Chef to pull secrets from Infisical.", "category": "DevOps Tools"},
|
||||
{"name": "Northflank", "slug": "northflank", "path": "/integrations/app-connections/northflank", "description": "Learn how to connect your Northflank projects to pull secrets from Infisical.", "category": "Hosting"}
|
||||
].sort(function(a, b) {
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
|
||||
@@ -38,6 +38,7 @@ export const SecretSyncsBrowser = () => {
|
||||
{"name": "OCI Vault", "slug": "oci-vault", "path": "/integrations/secret-syncs/oci-vault", "description": "Learn how to sync secrets from Infisical to OCI Vault.", "category": "Cloud Providers"},
|
||||
{"name": "Zabbix", "slug": "zabbix", "path": "/integrations/secret-syncs/zabbix", "description": "Learn how to sync secrets from Infisical to Zabbix.", "category": "Monitoring"},
|
||||
{"name": "Laravel Forge", "slug": "laravel-forge", "path": "/integrations/secret-syncs/laravel-forge", "description": "Learn how to sync secrets from Infisical to Laravel Forge.", "category": "Hosting"},
|
||||
{"name": "Chef", "slug": "chef", "path": "/integrations/secret-syncs/chef", "description": "Learn how to sync secrets from Infisical to Chef.", "category": "DevOps Tools"},
|
||||
{"name": "Northflank", "slug": "northflank", "path": "/integrations/secret-syncs/northflank", "description": "Learn how to sync secrets from Infisical to Northflank projects.", "category": "Hosting"}
|
||||
].sort(function(a, b) {
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
|
||||
BIN
frontend/public/images/integrations/Chef.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,93 @@
|
||||
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 {
|
||||
TChefDataBag,
|
||||
TChefDataBagItem,
|
||||
useChefConnectionListDataBagItems,
|
||||
useChefConnectionListDataBags
|
||||
} from "@app/hooks/api/appConnections/chef";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
export const ChefSyncFields = () => {
|
||||
const { control, setValue } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.Chef }
|
||||
>();
|
||||
|
||||
const connectionId = useWatch({ name: "connection.id", control });
|
||||
const dataBagName = useWatch({ name: "destinationConfig.dataBagName", control });
|
||||
|
||||
const { data: dataBags, isLoading: isDataBagsLoading } = useChefConnectionListDataBags(
|
||||
connectionId,
|
||||
{
|
||||
enabled: Boolean(connectionId)
|
||||
}
|
||||
);
|
||||
|
||||
const { data: dataBagItems, isLoading: isDataBagItemsLoading } =
|
||||
useChefConnectionListDataBagItems(connectionId, dataBagName, {
|
||||
enabled: Boolean(connectionId && dataBagName)
|
||||
});
|
||||
|
||||
const handleChangeConnection = () => {
|
||||
setValue("destinationConfig.dataBagName", "");
|
||||
setValue("destinationConfig.dataBagItemName", "");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncConnectionField onChange={handleChangeConnection} />
|
||||
|
||||
<Controller
|
||||
name="destinationConfig.dataBagName"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Data Bag">
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isLoading={isDataBagsLoading && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={dataBags?.find((dataBag) => dataBag.name === value) ?? null}
|
||||
onChange={(option) => {
|
||||
const selectedDataBag = option as SingleValue<TChefDataBag>;
|
||||
onChange(selectedDataBag?.name ?? "");
|
||||
setValue("destinationConfig.dataBagItemName", "");
|
||||
}}
|
||||
options={dataBags}
|
||||
placeholder="Select a data bag..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="destinationConfig.dataBagItemName"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Data Bag Item">
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isLoading={isDataBagItemsLoading && Boolean(connectionId && dataBagName)}
|
||||
isDisabled={!connectionId || !dataBagName}
|
||||
value={dataBagItems?.find((dataBagItem) => dataBagItem.name === value) ?? null}
|
||||
onChange={(option) => {
|
||||
const selectedDataBagItem = option as SingleValue<TChefDataBagItem>;
|
||||
onChange(selectedDataBagItem?.name ?? "");
|
||||
}}
|
||||
options={dataBagItems}
|
||||
placeholder="Select a data bag item..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields";
|
||||
import { BitbucketSyncFields } from "./BitbucketSyncFields";
|
||||
import { CamundaSyncFields } from "./CamundaSyncFields";
|
||||
import { ChecklySyncFields } from "./ChecklySyncFields";
|
||||
import { ChefSyncFields } from "./ChefSyncFields";
|
||||
import { CloudflarePagesSyncFields } from "./CloudflarePagesSyncFields";
|
||||
import { CloudflareWorkersSyncFields } from "./CloudflareWorkersSyncFields";
|
||||
import { DatabricksSyncFields } from "./DatabricksSyncFields";
|
||||
@@ -104,6 +105,8 @@ export const SecretSyncDestinationFields = () => {
|
||||
return <BitbucketSyncFields />;
|
||||
case SecretSync.LaravelForge:
|
||||
return <LaravelForgeSyncFields />;
|
||||
case SecretSync.Chef:
|
||||
return <ChefSyncFields />;
|
||||
case SecretSync.Northflank:
|
||||
return <NorthflankSyncFields />;
|
||||
default:
|
||||
|
||||
@@ -71,6 +71,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
case SecretSync.Northflank:
|
||||
case SecretSync.Bitbucket:
|
||||
case SecretSync.LaravelForge:
|
||||
case SecretSync.Chef:
|
||||
AdditionalSyncOptionsFieldsComponent = null;
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const ChefSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Chef }>();
|
||||
const dataBagName = watch("destinationConfig.dataBagName");
|
||||
const dataBagItemName = watch("destinationConfig.dataBagItemName");
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Data Bag">{dataBagName}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Data Bag Item">{dataBagItemName}</GenericFieldLabel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -24,6 +24,7 @@ import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
|
||||
import { BitbucketSyncReviewFields } from "./BitbucketSyncReviewFields";
|
||||
import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields";
|
||||
import { ChecklySyncReviewFields } from "./ChecklySyncReviewFields";
|
||||
import { ChefSyncReviewFields } from "./ChefSyncReviewFields";
|
||||
import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields";
|
||||
import { CloudflareWorkersSyncReviewFields } from "./CloudflareWorkersReviewFields";
|
||||
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
|
||||
@@ -177,6 +178,9 @@ export const SecretSyncReviewFields = () => {
|
||||
case SecretSync.LaravelForge:
|
||||
DestinationFieldsComponent = <LaravelForgeSyncReviewFields />;
|
||||
break;
|
||||
case SecretSync.Chef:
|
||||
DestinationFieldsComponent = <ChefSyncReviewFields />;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const ChefSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.Chef),
|
||||
destinationConfig: z.object({
|
||||
dataBagName: z.string().trim().min(1, "Data Bag required"),
|
||||
dataBagItemName: z.string().trim().min(1, "Data Bag Item required")
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -9,6 +9,7 @@ import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-desti
|
||||
import { BitbucketSyncDestinationSchema } from "./bitbucket-sync-destination-schema";
|
||||
import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema";
|
||||
import { ChecklySyncDestinationSchema } from "./checkly-sync-destination-schema";
|
||||
import { ChefSyncDestinationSchema } from "./chef-sync-destination-schema";
|
||||
import { CloudflarePagesSyncDestinationSchema } from "./cloudflare-pages-sync-destination-schema";
|
||||
import { CloudflareWorkersSyncDestinationSchema } from "./cloudflare-workers-sync-destination-schema";
|
||||
import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema";
|
||||
@@ -65,7 +66,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
NetlifySyncDestinationSchema,
|
||||
NorthflankSyncDestinationSchema,
|
||||
BitbucketSyncDestinationSchema,
|
||||
LaravelForgeSyncDestinationSchema
|
||||
LaravelForgeSyncDestinationSchema,
|
||||
ChefSyncDestinationSchema
|
||||
]);
|
||||
|
||||
export const SecretSyncFormSchema = SecretSyncUnionSchema;
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
} from "@app/hooks/api/appConnections/types";
|
||||
import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection";
|
||||
import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection";
|
||||
import { ChefConnectionMethod } from "@app/hooks/api/appConnections/types/chef-connection";
|
||||
import { DigitalOceanConnectionMethod } from "@app/hooks/api/appConnections/types/digital-ocean";
|
||||
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
|
||||
import { LaravelForgeConnectionMethod } from "@app/hooks/api/appConnections/types/laravel-forge-connection";
|
||||
@@ -129,7 +130,8 @@ export const APP_CONNECTION_MAP: Record<
|
||||
name: "Laravel Forge",
|
||||
image: "Laravel Forge.png",
|
||||
size: 65
|
||||
}
|
||||
},
|
||||
[AppConnection.Chef]: { name: "Chef", image: "Chef.png" }
|
||||
};
|
||||
|
||||
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
|
||||
@@ -202,6 +204,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
||||
case RenderConnectionMethod.ApiKey:
|
||||
case ChecklyConnectionMethod.ApiKey:
|
||||
return { name: "API Key", icon: faKey };
|
||||
case ChefConnectionMethod.UserKey:
|
||||
return { name: "User Key", icon: faKey };
|
||||
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||
case AzureAppConfigurationConnectionMethod.ClientSecret:
|
||||
case AzureKeyVaultConnectionMethod.ClientSecret:
|
||||
|
||||
@@ -121,6 +121,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
|
||||
[SecretSync.LaravelForge]: {
|
||||
name: "Laravel Forge",
|
||||
image: "Laravel Forge.png"
|
||||
},
|
||||
[SecretSync.Chef]: {
|
||||
name: "Chef",
|
||||
image: "Chef.png"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -156,7 +160,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.Netlify]: AppConnection.Netlify,
|
||||
[SecretSync.Northflank]: AppConnection.Northflank,
|
||||
[SecretSync.Bitbucket]: AppConnection.Bitbucket,
|
||||
[SecretSync.LaravelForge]: AppConnection.LaravelForge
|
||||
[SecretSync.LaravelForge]: AppConnection.LaravelForge,
|
||||
[SecretSync.Chef]: AppConnection.Chef
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<
|
||||
|
||||
2
frontend/src/hooks/api/appConnections/chef/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
68
frontend/src/hooks/api/appConnections/chef/queries.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { appConnectionKeys } from "@app/hooks/api/appConnections";
|
||||
|
||||
import { TChefDataBag, TChefDataBagItem } from "./types";
|
||||
|
||||
const chefConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "chef"] as const,
|
||||
listDataBags: (connectionId: string) =>
|
||||
[...chefConnectionKeys.all, "data-bags", connectionId] as const,
|
||||
listDataBagItems: (connectionId: string, dataBagName: string) =>
|
||||
[...chefConnectionKeys.all, "data-bag-items", connectionId, dataBagName] as const
|
||||
};
|
||||
|
||||
export const useChefConnectionListDataBags = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TChefDataBag[],
|
||||
unknown,
|
||||
TChefDataBag[],
|
||||
ReturnType<typeof chefConnectionKeys.listDataBags>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: chefConnectionKeys.listDataBags(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TChefDataBag[]>(
|
||||
`/api/v1/app-connections/chef/${connectionId}/data-bags`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useChefConnectionListDataBagItems = (
|
||||
connectionId: string,
|
||||
dataBagName: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TChefDataBagItem[],
|
||||
unknown,
|
||||
TChefDataBagItem[],
|
||||
ReturnType<typeof chefConnectionKeys.listDataBagItems>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: chefConnectionKeys.listDataBagItems(connectionId, dataBagName),
|
||||
queryFn: async () => {
|
||||
const params = { dataBagName };
|
||||
const { data } = await apiRequest.get<TChefDataBagItem[]>(
|
||||
`/api/v1/app-connections/chef/${connectionId}/data-bag-items`,
|
||||
{ params }
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(connectionId && dataBagName),
|
||||
...options
|
||||
});
|
||||
};
|
||||
7
frontend/src/hooks/api/appConnections/chef/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type TChefDataBag = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TChefDataBagItem = {
|
||||
name: string;
|
||||
};
|
||||
@@ -39,5 +39,6 @@ export enum AppConnection {
|
||||
Northflank = "northflank",
|
||||
Okta = "okta",
|
||||
Redis = "redis",
|
||||
LaravelForge = "laravel-forge"
|
||||
LaravelForge = "laravel-forge",
|
||||
Chef = "chef"
|
||||
}
|
||||
|
||||
@@ -148,6 +148,10 @@ export type TChecklyConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Checkly;
|
||||
};
|
||||
|
||||
export type TChefConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Chef;
|
||||
};
|
||||
|
||||
export type TSupabaseConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Supabase;
|
||||
};
|
||||
@@ -220,7 +224,8 @@ export type TAppConnectionOption =
|
||||
| TNorthflankConnectionOption
|
||||
| TOktaConnectionOption
|
||||
| TAzureAdCsConnectionOption
|
||||
| TLaravelForgeConnectionOption;
|
||||
| TLaravelForgeConnectionOption
|
||||
| TChefConnectionOption;
|
||||
|
||||
export type TAppConnectionOptionMap = {
|
||||
[AppConnection.AWS]: TAwsConnectionOption;
|
||||
@@ -264,4 +269,5 @@ export type TAppConnectionOptionMap = {
|
||||
[AppConnection.AzureADCS]: TAzureAdCsConnectionOption;
|
||||
[AppConnection.Redis]: TRedisConnectionOption;
|
||||
[AppConnection.LaravelForge]: TLaravelForgeConnectionOption;
|
||||
[AppConnection.Chef]: TChefConnectionOption;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum ChefConnectionMethod {
|
||||
UserKey = "user-key"
|
||||
}
|
||||
|
||||
export type TChefConnection = TRootAppConnection & { app: AppConnection.Chef } & {
|
||||
method: ChefConnectionMethod.UserKey;
|
||||
credentials: {
|
||||
instanceUrl?: string;
|
||||
orgName: string;
|
||||
userName: string;
|
||||
privateKey: string;
|
||||
};
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import { TAzureKeyVaultConnection } from "./azure-key-vault-connection";
|
||||
import { TBitbucketConnection } from "./bitbucket-connection";
|
||||
import { TCamundaConnection } from "./camunda-connection";
|
||||
import { TChecklyConnection } from "./checkly-connection";
|
||||
import { TChefConnection } from "./chef-connection";
|
||||
import { TCloudflareConnection } from "./cloudflare-connection";
|
||||
import { TDatabricksConnection } from "./databricks-connection";
|
||||
import { TDigitalOceanConnection } from "./digital-ocean";
|
||||
@@ -53,6 +54,7 @@ export * from "./azure-key-vault-connection";
|
||||
export * from "./bitbucket-connection";
|
||||
export * from "./camunda-connection";
|
||||
export * from "./checkly-connection";
|
||||
export * from "./chef-connection";
|
||||
export * from "./cloudflare-connection";
|
||||
export * from "./databricks-connection";
|
||||
export * from "./flyio-connection";
|
||||
@@ -124,7 +126,8 @@ export type TAppConnection =
|
||||
| TNetlifyConnection
|
||||
| TNorthflankConnection
|
||||
| TOktaConnection
|
||||
| TRedisConnection;
|
||||
| TRedisConnection
|
||||
| TChefConnection;
|
||||
|
||||
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id" | "projectId">;
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ export enum SecretSync {
|
||||
Netlify = "netlify",
|
||||
Northflank = "northflank",
|
||||
Bitbucket = "bitbucket",
|
||||
LaravelForge = "laravel-forge"
|
||||
LaravelForge = "laravel-forge",
|
||||
Chef = "chef"
|
||||
}
|
||||
|
||||
export enum SecretSyncStatus {
|
||||
|
||||
16
frontend/src/hooks/api/secretSyncs/types/chef-sync.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
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 TChefSync = TRootSecretSync & {
|
||||
destination: SecretSync.Chef;
|
||||
destinationConfig: {
|
||||
dataBagName: string;
|
||||
dataBagItemName: string;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection.Chef;
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import { TAzureKeyVaultSync } from "./azure-key-vault-sync";
|
||||
import { TBitbucketSync } from "./bitbucket-sync";
|
||||
import { TCamundaSync } from "./camunda-sync";
|
||||
import { TChecklySync } from "./checkly-sync";
|
||||
import { TChefSync } from "./chef-sync";
|
||||
import { TCloudflarePagesSync } from "./cloudflare-pages-sync";
|
||||
import { TCloudflareWorkersSync } from "./cloudflare-workers-sync";
|
||||
import { TDatabricksSync } from "./databricks-sync";
|
||||
@@ -73,7 +74,8 @@ export type TSecretSync =
|
||||
| TNetlifySync
|
||||
| TNorthflankSync
|
||||
| TBitbucketSync
|
||||
| TLaravelForgeSync;
|
||||
| TLaravelForgeSync
|
||||
| TChefSync;
|
||||
|
||||
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm";
|
||||
import { BitbucketConnectionForm } from "./BitbucketConnectionForm";
|
||||
import { CamundaConnectionForm } from "./CamundaConnectionForm";
|
||||
import { ChecklyConnectionForm } from "./ChecklyConnectionForm";
|
||||
import { ChefConnectionForm } from "./ChefConnectionForm";
|
||||
import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
|
||||
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
|
||||
import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm";
|
||||
@@ -164,6 +165,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
|
||||
return <RailwayConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Checkly:
|
||||
return <ChecklyConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Chef:
|
||||
return <ChefConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Supabase:
|
||||
return <SupabaseConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.DigitalOcean:
|
||||
@@ -329,6 +332,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
return <RailwayConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Checkly:
|
||||
return <ChecklyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Chef:
|
||||
return <ChefConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Supabase:
|
||||
return <SupabaseConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.DigitalOcean:
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { ChefConnectionMethod, TChefConnection } from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type Props = {
|
||||
appConnection?: TChefConnection;
|
||||
onSubmit: (formData: FormData) => Promise<void>;
|
||||
};
|
||||
|
||||
const rootSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.Chef)
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [
|
||||
rootSchema.extend({
|
||||
method: z.literal(ChefConnectionMethod.UserKey),
|
||||
credentials: z.object({
|
||||
serverUrl: z.string().trim().url("Valid Chef Server URL required").optional(),
|
||||
orgName: z.string().trim().min(1, "Organization name required"),
|
||||
userName: z.string().trim().min(1, "User name required"),
|
||||
privateKey: z.string().trim().min(1, "Private key required")
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const ChefConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection ?? {
|
||||
app: AppConnection.Chef,
|
||||
method: ChefConnectionMethod.UserKey
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
<Controller
|
||||
name="credentials.serverUrl"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Chef Server URL (optional)"
|
||||
tooltipText="Will default to Chef Cloud if not specified."
|
||||
>
|
||||
<Input
|
||||
placeholder="https://api.chef.io"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText={`The method you would like to use to connect with ${
|
||||
APP_CONNECTION_MAP[AppConnection.Chef].name
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Method"
|
||||
>
|
||||
<Select
|
||||
isDisabled={isUpdate}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(ChefConnectionMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem value={method} key={method}>
|
||||
{getAppConnectionMethodDetails(method).name}{" "}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="credentials.orgName"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Organization Short Name"
|
||||
>
|
||||
<Input
|
||||
className="border border-mineshaft-500 bg-mineshaft-900"
|
||||
placeholder="your-org"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.userName"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="User Name"
|
||||
>
|
||||
<Input
|
||||
className="border border-mineshaft-500 bg-mineshaft-900"
|
||||
placeholder="your-username"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.privateKey"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Private Key"
|
||||
tooltipText="Your Chef user's private key (.pem file)"
|
||||
>
|
||||
<SecretInput
|
||||
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
{isUpdate ? "Update Credentials" : "Connect to Chef"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { TChefSync } from "@app/hooks/api/secretSyncs/types/chef-sync";
|
||||
|
||||
import { getSecretSyncDestinationColValues } from "../helpers";
|
||||
import { SecretSyncTableCell } from "../SecretSyncTableCell";
|
||||
|
||||
type Props = {
|
||||
secretSync: TChefSync;
|
||||
};
|
||||
|
||||
export const ChefSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
|
||||
|
||||
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncC
|
||||
import { BitbucketSyncDestinationCol } from "./BitbucketSyncDestinationCol";
|
||||
import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol";
|
||||
import { ChecklySyncDestinationCol } from "./ChecklySyncDestinationCol";
|
||||
import { ChefSyncDestinationCol } from "./ChefSyncDestinationCol";
|
||||
import { CloudflarePagesSyncDestinationCol } from "./CloudflarePagesSyncDestinationCol";
|
||||
import { CloudflareWorkersSyncDestinationCol } from "./CloudflareWorkersSyncDestinationCol";
|
||||
import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol";
|
||||
@@ -103,6 +104,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
return <BitbucketSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.LaravelForge:
|
||||
return <LaravelForgeSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.Chef:
|
||||
return <ChefSyncDestinationCol secretSync={secretSync} />;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
|
||||
|
||||
@@ -202,6 +202,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
|
||||
primaryText = destinationConfig.siteName || destinationConfig.siteId;
|
||||
secondaryText = destinationConfig.orgName || destinationConfig.orgSlug;
|
||||
break;
|
||||
case SecretSync.Chef:
|
||||
primaryText = destinationConfig.dataBagName;
|
||||
secondaryText = destinationConfig.dataBagItemName;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Col Values ${destination}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TChefSync } from "@app/hooks/api/secretSyncs/types/chef-sync";
|
||||
|
||||
type Props = {
|
||||
secretSync: TChefSync;
|
||||
};
|
||||
|
||||
export const ChefSyncDestinationSection = ({ secretSync }: Props) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Data Bag">{destinationConfig.dataBagName}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Data Bag Item">
|
||||
{destinationConfig.dataBagItemName}
|
||||
</GenericFieldLabel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -20,6 +20,7 @@ import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinat
|
||||
import { BitbucketSyncDestinationSection } from "./BitbucketSyncDestinationSection";
|
||||
import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection";
|
||||
import { ChecklySyncDestinationSection } from "./ChecklySyncDestinationSection";
|
||||
import { ChefSyncDestinationSection } from "./ChefSyncDestinationSection";
|
||||
import { CloudflarePagesSyncDestinationSection } from "./CloudflarePagesSyncDestinationSection";
|
||||
import { CloudflareWorkersSyncDestinationSection } from "./CloudflareWorkersSyncDestinationSection";
|
||||
import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection";
|
||||
@@ -156,6 +157,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
|
||||
case SecretSync.LaravelForge:
|
||||
DestinationComponents = <LaravelForgeSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
case SecretSync.Chef:
|
||||
DestinationComponents = <ChefSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Section components: ${destination}`);
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
||||
case SecretSync.Northflank:
|
||||
case SecretSync.Bitbucket:
|
||||
case SecretSync.LaravelForge:
|
||||
case SecretSync.Chef:
|
||||
AdditionalSyncOptionsComponent = null;
|
||||
break;
|
||||
default:
|
||||
|
||||