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: {
|
LARAVEL_FORGE: {
|
||||||
apiToken: "The API token used to authenticate with 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.",
|
siteId: "The ID of the Netlify site to sync secrets to.",
|
||||||
context: "The Netlify context 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: {
|
NORTHFLANK: {
|
||||||
projectId: "The ID of the Northflank project to sync secrets to.",
|
projectId: "The ID of the Northflank project to sync secrets to.",
|
||||||
projectName: "The name 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,
|
ChecklyConnectionListItemSchema,
|
||||||
SanitizedChecklyConnectionSchema
|
SanitizedChecklyConnectionSchema
|
||||||
} from "@app/services/app-connection/checkly";
|
} from "@app/services/app-connection/checkly";
|
||||||
|
import { ChefConnectionListItemSchema, SanitizedChefConnectionSchema } from "@app/services/app-connection/chef";
|
||||||
import {
|
import {
|
||||||
CloudflareConnectionListItemSchema,
|
CloudflareConnectionListItemSchema,
|
||||||
SanitizedCloudflareConnectionSchema
|
SanitizedCloudflareConnectionSchema
|
||||||
@@ -168,7 +169,8 @@ const SanitizedAppConnectionSchema = z.union([
|
|||||||
...SanitizedOktaConnectionSchema.options,
|
...SanitizedOktaConnectionSchema.options,
|
||||||
...SanitizedAzureADCSConnectionSchema.options,
|
...SanitizedAzureADCSConnectionSchema.options,
|
||||||
...SanitizedRedisConnectionSchema.options,
|
...SanitizedRedisConnectionSchema.options,
|
||||||
...SanitizedLaravelForgeConnectionSchema.options
|
...SanitizedLaravelForgeConnectionSchema.options,
|
||||||
|
...SanitizedChefConnectionSchema.options
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||||
@@ -212,7 +214,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
|||||||
OktaConnectionListItemSchema,
|
OktaConnectionListItemSchema,
|
||||||
AzureADCSConnectionListItemSchema,
|
AzureADCSConnectionListItemSchema,
|
||||||
RedisConnectionListItemSchema,
|
RedisConnectionListItemSchema,
|
||||||
LaravelForgeConnectionListItemSchema
|
LaravelForgeConnectionListItemSchema,
|
||||||
|
ChefConnectionListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
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 { registerBitbucketConnectionRouter } from "./bitbucket-connection-router";
|
||||||
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
||||||
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
|
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
|
||||||
|
import { registerChefConnectionRouter } from "./chef-connection-router";
|
||||||
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
||||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||||
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-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.Netlify]: registerNetlifyConnectionRouter,
|
||||||
[AppConnection.Northflank]: registerNorthflankConnectionRouter,
|
[AppConnection.Northflank]: registerNorthflankConnectionRouter,
|
||||||
[AppConnection.Okta]: registerOktaConnectionRouter,
|
[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 { registerBitbucketSyncRouter } from "./bitbucket-sync-router";
|
||||||
import { registerCamundaSyncRouter } from "./camunda-sync-router";
|
import { registerCamundaSyncRouter } from "./camunda-sync-router";
|
||||||
import { registerChecklySyncRouter } from "./checkly-sync-router";
|
import { registerChecklySyncRouter } from "./checkly-sync-router";
|
||||||
|
import { registerChefSyncRouter } from "./chef-sync-router";
|
||||||
import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router";
|
import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router";
|
||||||
import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router";
|
import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router";
|
||||||
import { registerDatabricksSyncRouter } from "./databricks-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.Netlify]: registerNetlifySyncRouter,
|
||||||
[SecretSync.Northflank]: registerNorthflankSyncRouter,
|
[SecretSync.Northflank]: registerNorthflankSyncRouter,
|
||||||
[SecretSync.Bitbucket]: registerBitbucketSyncRouter,
|
[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 { BitbucketSyncListItemSchema, BitbucketSyncSchema } from "@app/services/secret-sync/bitbucket";
|
||||||
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
|
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
|
||||||
import { ChecklySyncListItemSchema, ChecklySyncSchema } from "@app/services/secret-sync/checkly/checkly-sync-schemas";
|
import { ChecklySyncListItemSchema, ChecklySyncSchema } from "@app/services/secret-sync/checkly/checkly-sync-schemas";
|
||||||
|
import { ChefSyncListItemSchema, ChefSyncSchema } from "@app/services/secret-sync/chef";
|
||||||
import {
|
import {
|
||||||
CloudflarePagesSyncListItemSchema,
|
CloudflarePagesSyncListItemSchema,
|
||||||
CloudflarePagesSyncSchema
|
CloudflarePagesSyncSchema
|
||||||
@@ -88,7 +89,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
|||||||
NetlifySyncSchema,
|
NetlifySyncSchema,
|
||||||
NorthflankSyncSchema,
|
NorthflankSyncSchema,
|
||||||
BitbucketSyncSchema,
|
BitbucketSyncSchema,
|
||||||
LaravelForgeSyncSchema
|
LaravelForgeSyncSchema,
|
||||||
|
ChefSyncSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||||
@@ -123,7 +125,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
|||||||
NetlifySyncListItemSchema,
|
NetlifySyncListItemSchema,
|
||||||
NorthflankSyncListItemSchema,
|
NorthflankSyncListItemSchema,
|
||||||
BitbucketSyncListItemSchema,
|
BitbucketSyncListItemSchema,
|
||||||
LaravelForgeSyncListItemSchema
|
LaravelForgeSyncListItemSchema,
|
||||||
|
ChefSyncListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export enum AppConnection {
|
|||||||
Okta = "okta",
|
Okta = "okta",
|
||||||
Redis = "redis",
|
Redis = "redis",
|
||||||
LaravelForge = "laravel-forge",
|
LaravelForge = "laravel-forge",
|
||||||
|
Chef = "chef",
|
||||||
Northflank = "northflank"
|
Northflank = "northflank"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ import {
|
|||||||
} from "./bitbucket";
|
} from "./bitbucket";
|
||||||
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
|
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
|
||||||
import { ChecklyConnectionMethod, getChecklyConnectionListItem, validateChecklyConnectionCredentials } from "./checkly";
|
import { ChecklyConnectionMethod, getChecklyConnectionListItem, validateChecklyConnectionCredentials } from "./checkly";
|
||||||
|
import { ChefConnectionMethod, getChefConnectionListItem, validateChefConnectionCredentials } from "./chef";
|
||||||
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
|
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
|
||||||
import {
|
import {
|
||||||
getCloudflareConnectionListItem,
|
getCloudflareConnectionListItem,
|
||||||
@@ -210,7 +211,8 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
|||||||
getNetlifyConnectionListItem(),
|
getNetlifyConnectionListItem(),
|
||||||
getNorthflankConnectionListItem(),
|
getNorthflankConnectionListItem(),
|
||||||
getOktaConnectionListItem(),
|
getOktaConnectionListItem(),
|
||||||
getRedisConnectionListItem()
|
getRedisConnectionListItem(),
|
||||||
|
getChefConnectionListItem()
|
||||||
]
|
]
|
||||||
.filter((option) => {
|
.filter((option) => {
|
||||||
switch (projectType) {
|
switch (projectType) {
|
||||||
@@ -341,6 +343,7 @@ export const validateAppConnectionCredentials = async (
|
|||||||
[AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Northflank]: validateNorthflankConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Northflank]: validateNorthflankConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.Chef]: validateChefConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator
|
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -409,6 +412,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
case RenderConnectionMethod.ApiKey:
|
case RenderConnectionMethod.ApiKey:
|
||||||
case ChecklyConnectionMethod.ApiKey:
|
case ChecklyConnectionMethod.ApiKey:
|
||||||
return "API Key";
|
return "API Key";
|
||||||
|
case ChefConnectionMethod.UserKey:
|
||||||
|
return "User Key";
|
||||||
case SupabaseConnectionMethod.AccessToken:
|
case SupabaseConnectionMethod.AccessToken:
|
||||||
return "Access Token";
|
return "Access Token";
|
||||||
default:
|
default:
|
||||||
@@ -483,7 +488,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
|||||||
[AppConnection.Northflank]: platformManagedCredentialsNotSupported,
|
[AppConnection.Northflank]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Okta]: platformManagedCredentialsNotSupported,
|
[AppConnection.Okta]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Redis]: platformManagedCredentialsNotSupported,
|
[AppConnection.Redis]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported
|
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported,
|
||||||
|
[AppConnection.Chef]: platformManagedCredentialsNotSupported
|
||||||
};
|
};
|
||||||
|
|
||||||
export const enterpriseAppCheck = async (
|
export const enterpriseAppCheck = async (
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
|||||||
[AppConnection.Netlify]: "Netlify",
|
[AppConnection.Netlify]: "Netlify",
|
||||||
[AppConnection.Okta]: "Okta",
|
[AppConnection.Okta]: "Okta",
|
||||||
[AppConnection.Redis]: "Redis",
|
[AppConnection.Redis]: "Redis",
|
||||||
|
[AppConnection.Chef]: "Chef",
|
||||||
[AppConnection.Northflank]: "Northflank"
|
[AppConnection.Northflank]: "Northflank"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,5 +86,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
|||||||
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
|
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Okta]: AppConnectionPlanType.Regular,
|
[AppConnection.Okta]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Redis]: AppConnectionPlanType.Regular,
|
[AppConnection.Redis]: AppConnectionPlanType.Regular,
|
||||||
|
[AppConnection.Chef]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Northflank]: AppConnectionPlanType.Regular
|
[AppConnection.Northflank]: AppConnectionPlanType.Regular
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
|
|||||||
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
||||||
import { ValidateChecklyConnectionCredentialsSchema } from "./checkly";
|
import { ValidateChecklyConnectionCredentialsSchema } from "./checkly";
|
||||||
import { checklyConnectionService } from "./checkly/checkly-connection-service";
|
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 { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
|
||||||
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
||||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||||
@@ -174,7 +176,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
|||||||
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
|
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
|
||||||
[AppConnection.Northflank]: ValidateNorthflankConnectionCredentialsSchema,
|
[AppConnection.Northflank]: ValidateNorthflankConnectionCredentialsSchema,
|
||||||
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
|
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
|
||||||
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema
|
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema,
|
||||||
|
[AppConnection.Chef]: ValidateChefConnectionCredentialsSchema
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appConnectionServiceFactory = ({
|
export const appConnectionServiceFactory = ({
|
||||||
@@ -881,6 +884,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
netlify: netlifyConnectionService(connectAppConnectionById),
|
netlify: netlifyConnectionService(connectAppConnectionById),
|
||||||
northflank: northflankConnectionService(connectAppConnectionById),
|
northflank: northflankConnectionService(connectAppConnectionById),
|
||||||
okta: oktaConnectionService(connectAppConnectionById),
|
okta: oktaConnectionService(connectAppConnectionById),
|
||||||
laravelForge: laravelForgeConnectionService(connectAppConnectionById)
|
laravelForge: laravelForgeConnectionService(connectAppConnectionById),
|
||||||
|
chef: chefConnectionService(connectAppConnectionById)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ import {
|
|||||||
TChecklyConnectionInput,
|
TChecklyConnectionInput,
|
||||||
TValidateChecklyConnectionCredentialsSchema
|
TValidateChecklyConnectionCredentialsSchema
|
||||||
} from "./checkly";
|
} from "./checkly";
|
||||||
|
import {
|
||||||
|
TChefConnection,
|
||||||
|
TChefConnectionConfig,
|
||||||
|
TChefConnectionInput,
|
||||||
|
TValidateChefConnectionCredentialsSchema
|
||||||
|
} from "./chef";
|
||||||
import {
|
import {
|
||||||
TCloudflareConnection,
|
TCloudflareConnection,
|
||||||
TCloudflareConnectionConfig,
|
TCloudflareConnectionConfig,
|
||||||
@@ -282,6 +288,7 @@ export type TAppConnection = { id: string } & (
|
|||||||
| TNorthflankConnection
|
| TNorthflankConnection
|
||||||
| TOktaConnection
|
| TOktaConnection
|
||||||
| TRedisConnection
|
| TRedisConnection
|
||||||
|
| TChefConnection
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||||
@@ -330,6 +337,7 @@ export type TAppConnectionInput = { id: string } & (
|
|||||||
| TNorthflankConnectionInput
|
| TNorthflankConnectionInput
|
||||||
| TOktaConnectionInput
|
| TOktaConnectionInput
|
||||||
| TRedisConnectionInput
|
| TRedisConnectionInput
|
||||||
|
| TChefConnectionInput
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TSqlConnectionInput =
|
export type TSqlConnectionInput =
|
||||||
@@ -395,7 +403,8 @@ export type TAppConnectionConfig =
|
|||||||
| TNetlifyConnectionConfig
|
| TNetlifyConnectionConfig
|
||||||
| TNorthflankConnectionConfig
|
| TNorthflankConnectionConfig
|
||||||
| TOktaConnectionConfig
|
| TOktaConnectionConfig
|
||||||
| TRedisConnectionConfig;
|
| TRedisConnectionConfig
|
||||||
|
| TChefConnectionConfig;
|
||||||
|
|
||||||
export type TValidateAppConnectionCredentialsSchema =
|
export type TValidateAppConnectionCredentialsSchema =
|
||||||
| TValidateAwsConnectionCredentialsSchema
|
| TValidateAwsConnectionCredentialsSchema
|
||||||
@@ -438,7 +447,8 @@ export type TValidateAppConnectionCredentialsSchema =
|
|||||||
| TValidateNetlifyConnectionCredentialsSchema
|
| TValidateNetlifyConnectionCredentialsSchema
|
||||||
| TValidateNorthflankConnectionCredentialsSchema
|
| TValidateNorthflankConnectionCredentialsSchema
|
||||||
| TValidateOktaConnectionCredentialsSchema
|
| TValidateOktaConnectionCredentialsSchema
|
||||||
| TValidateRedisConnectionCredentialsSchema;
|
| TValidateRedisConnectionCredentialsSchema
|
||||||
|
| TValidateChefConnectionCredentialsSchema;
|
||||||
|
|
||||||
export type TListAwsConnectionKmsKeys = {
|
export type TListAwsConnectionKmsKeys = {
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export enum ChefConnectionMethod {
|
||||||
|
UserKey = "user-key"
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com",
|
||||||
GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform",
|
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 () => {
|
export const getIntegrationOptions = async () => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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)
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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",
|
Netlify = "netlify",
|
||||||
Northflank = "northflank",
|
Northflank = "northflank",
|
||||||
Bitbucket = "bitbucket",
|
Bitbucket = "bitbucket",
|
||||||
LaravelForge = "laravel-forge"
|
LaravelForge = "laravel-forge",
|
||||||
|
Chef = "chef"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SecretSyncInitialSyncBehavior {
|
export enum SecretSyncInitialSyncBehavior {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { BITBUCKET_SYNC_LIST_OPTION, BitbucketSyncFns } from "./bitbucket";
|
|||||||
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
|
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
|
||||||
import { CHECKLY_SYNC_LIST_OPTION } from "./checkly/checkly-sync-constants";
|
import { CHECKLY_SYNC_LIST_OPTION } from "./checkly/checkly-sync-constants";
|
||||||
import { ChecklySyncFns } from "./checkly/checkly-sync-fns";
|
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 { CLOUDFLARE_PAGES_SYNC_LIST_OPTION } from "./cloudflare-pages/cloudflare-pages-constants";
|
||||||
import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns";
|
import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns";
|
||||||
import { CLOUDFLARE_WORKERS_SYNC_LIST_OPTION, CloudflareWorkersSyncFns } from "./cloudflare-workers";
|
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 { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku";
|
||||||
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||||
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
||||||
import { LARAVEL_FORGE_SYNC_LIST_OPTION } from "./laravel-forge";
|
import { LARAVEL_FORGE_SYNC_LIST_OPTION, LaravelForgeSyncFns } from "./laravel-forge";
|
||||||
import { LaravelForgeSyncFns } from "./laravel-forge/laravel-forge-sync-fns";
|
|
||||||
import { NETLIFY_SYNC_LIST_OPTION, NetlifySyncFns } from "./netlify";
|
import { NETLIFY_SYNC_LIST_OPTION, NetlifySyncFns } from "./netlify";
|
||||||
import { NORTHFLANK_SYNC_LIST_OPTION, NorthflankSyncFns } from "./northflank";
|
import { NORTHFLANK_SYNC_LIST_OPTION, NorthflankSyncFns } from "./northflank";
|
||||||
import { RAILWAY_SYNC_LIST_OPTION } from "./railway/railway-sync-constants";
|
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.Netlify]: NETLIFY_SYNC_LIST_OPTION,
|
||||||
[SecretSync.Northflank]: NORTHFLANK_SYNC_LIST_OPTION,
|
[SecretSync.Northflank]: NORTHFLANK_SYNC_LIST_OPTION,
|
||||||
[SecretSync.Bitbucket]: BITBUCKET_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 = () => {
|
export const listSecretSyncOptions = () => {
|
||||||
@@ -286,6 +287,8 @@ export const SecretSyncFns = {
|
|||||||
return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||||
case SecretSync.LaravelForge:
|
case SecretSync.LaravelForge:
|
||||||
return LaravelForgeSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
return LaravelForgeSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||||
|
case SecretSync.Chef:
|
||||||
|
return ChefSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||||
@@ -408,6 +411,9 @@ export const SecretSyncFns = {
|
|||||||
case SecretSync.LaravelForge:
|
case SecretSync.LaravelForge:
|
||||||
secretMap = await LaravelForgeSyncFns.getSecrets(secretSync);
|
secretMap = await LaravelForgeSyncFns.getSecrets(secretSync);
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.Chef:
|
||||||
|
secretMap = await ChefSyncFns.getSecrets(secretSync);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||||
@@ -505,6 +511,8 @@ export const SecretSyncFns = {
|
|||||||
return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||||
case SecretSync.LaravelForge:
|
case SecretSync.LaravelForge:
|
||||||
return LaravelForgeSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
return LaravelForgeSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||||
|
case SecretSync.Chef:
|
||||||
|
return ChefSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
`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.Netlify]: "Netlify",
|
||||||
[SecretSync.Northflank]: "Northflank",
|
[SecretSync.Northflank]: "Northflank",
|
||||||
[SecretSync.Bitbucket]: "Bitbucket",
|
[SecretSync.Bitbucket]: "Bitbucket",
|
||||||
[SecretSync.LaravelForge]: "Laravel Forge"
|
[SecretSync.LaravelForge]: "Laravel Forge",
|
||||||
|
[SecretSync.Chef]: "Chef"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
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.Netlify]: AppConnection.Netlify,
|
||||||
[SecretSync.Northflank]: AppConnection.Northflank,
|
[SecretSync.Northflank]: AppConnection.Northflank,
|
||||||
[SecretSync.Bitbucket]: AppConnection.Bitbucket,
|
[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> = {
|
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.Netlify]: SecretSyncPlanType.Regular,
|
||||||
[SecretSync.Northflank]: SecretSyncPlanType.Regular,
|
[SecretSync.Northflank]: SecretSyncPlanType.Regular,
|
||||||
[SecretSync.Bitbucket]: 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[]> = {
|
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.Netlify]: ["accountName", "siteName"],
|
||||||
[SecretSync.Northflank]: [],
|
[SecretSync.Northflank]: [],
|
||||||
[SecretSync.Bitbucket]: [],
|
[SecretSync.Bitbucket]: [],
|
||||||
[SecretSync.LaravelForge]: []
|
[SecretSync.LaravelForge]: [],
|
||||||
|
[SecretSync.Chef]: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultDuplicateCheck: DestinationDuplicateCheckFn = () => true;
|
const defaultDuplicateCheck: DestinationDuplicateCheckFn = () => true;
|
||||||
@@ -209,5 +213,6 @@ export const DESTINATION_DUPLICATE_CHECK_MAP: Record<SecretSync, DestinationDupl
|
|||||||
[SecretSync.Netlify]: defaultDuplicateCheck,
|
[SecretSync.Netlify]: defaultDuplicateCheck,
|
||||||
[SecretSync.Northflank]: defaultDuplicateCheck,
|
[SecretSync.Northflank]: defaultDuplicateCheck,
|
||||||
[SecretSync.Bitbucket]: defaultDuplicateCheck,
|
[SecretSync.Bitbucket]: defaultDuplicateCheck,
|
||||||
[SecretSync.LaravelForge]: defaultDuplicateCheck
|
[SecretSync.LaravelForge]: defaultDuplicateCheck,
|
||||||
|
[SecretSync.Chef]: defaultDuplicateCheck
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
TCamundaSyncListItem,
|
TCamundaSyncListItem,
|
||||||
TCamundaSyncWithCredentials
|
TCamundaSyncWithCredentials
|
||||||
} from "@app/services/secret-sync/camunda";
|
} from "@app/services/secret-sync/camunda";
|
||||||
|
import { TChefSync, TChefSyncInput, TChefSyncListItem, TChefSyncWithCredentials } from "@app/services/secret-sync/chef";
|
||||||
import {
|
import {
|
||||||
TDatabricksSync,
|
TDatabricksSync,
|
||||||
TDatabricksSyncInput,
|
TDatabricksSyncInput,
|
||||||
@@ -169,6 +170,7 @@ export type TSecretSync =
|
|||||||
| TGitHubSync
|
| TGitHubSync
|
||||||
| TGcpSync
|
| TGcpSync
|
||||||
| TAzureKeyVaultSync
|
| TAzureKeyVaultSync
|
||||||
|
| TChefSync
|
||||||
| TAzureAppConfigurationSync
|
| TAzureAppConfigurationSync
|
||||||
| TAzureDevOpsSync
|
| TAzureDevOpsSync
|
||||||
| TDatabricksSync
|
| TDatabricksSync
|
||||||
@@ -202,6 +204,7 @@ export type TSecretSyncWithCredentials =
|
|||||||
| TGitHubSyncWithCredentials
|
| TGitHubSyncWithCredentials
|
||||||
| TGcpSyncWithCredentials
|
| TGcpSyncWithCredentials
|
||||||
| TAzureKeyVaultSyncWithCredentials
|
| TAzureKeyVaultSyncWithCredentials
|
||||||
|
| TChefSyncWithCredentials
|
||||||
| TAzureAppConfigurationSyncWithCredentials
|
| TAzureAppConfigurationSyncWithCredentials
|
||||||
| TAzureDevOpsSyncWithCredentials
|
| TAzureDevOpsSyncWithCredentials
|
||||||
| TDatabricksSyncWithCredentials
|
| TDatabricksSyncWithCredentials
|
||||||
@@ -236,6 +239,7 @@ export type TSecretSyncInput =
|
|||||||
| TGitHubSyncInput
|
| TGitHubSyncInput
|
||||||
| TGcpSyncInput
|
| TGcpSyncInput
|
||||||
| TAzureKeyVaultSyncInput
|
| TAzureKeyVaultSyncInput
|
||||||
|
| TChefSyncInput
|
||||||
| TAzureAppConfigurationSyncInput
|
| TAzureAppConfigurationSyncInput
|
||||||
| TAzureDevOpsSyncInput
|
| TAzureDevOpsSyncInput
|
||||||
| TDatabricksSyncInput
|
| TDatabricksSyncInput
|
||||||
@@ -270,6 +274,7 @@ export type TSecretSyncListItem =
|
|||||||
| TGitHubSyncListItem
|
| TGitHubSyncListItem
|
||||||
| TGcpSyncListItem
|
| TGcpSyncListItem
|
||||||
| TAzureKeyVaultSyncListItem
|
| TAzureKeyVaultSyncListItem
|
||||||
|
| TChefSyncListItem
|
||||||
| TAzureAppConfigurationSyncListItem
|
| TAzureAppConfigurationSyncListItem
|
||||||
| TAzureDevOpsSyncListItem
|
| TAzureDevOpsSyncListItem
|
||||||
| TDatabricksSyncListItem
|
| TDatabricksSyncListItem
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Available"
|
||||||
|
openapi: "GET /api/v1/app-connections/chef/available"
|
||||||
|
---
|
||||||
@@ -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"
|
||||||
|
---
|
||||||
@@ -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"
|
||||||
|
---
|
||||||
@@ -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/bitbucket",
|
||||||
"integrations/app-connections/camunda",
|
"integrations/app-connections/camunda",
|
||||||
"integrations/app-connections/checkly",
|
"integrations/app-connections/checkly",
|
||||||
|
"integrations/app-connections/chef",
|
||||||
"integrations/app-connections/cloudflare",
|
"integrations/app-connections/cloudflare",
|
||||||
"integrations/app-connections/databricks",
|
"integrations/app-connections/databricks",
|
||||||
"integrations/app-connections/digital-ocean",
|
"integrations/app-connections/digital-ocean",
|
||||||
@@ -540,6 +541,7 @@
|
|||||||
"integrations/secret-syncs/bitbucket",
|
"integrations/secret-syncs/bitbucket",
|
||||||
"integrations/secret-syncs/camunda",
|
"integrations/secret-syncs/camunda",
|
||||||
"integrations/secret-syncs/checkly",
|
"integrations/secret-syncs/checkly",
|
||||||
|
"integrations/secret-syncs/chef",
|
||||||
"integrations/secret-syncs/cloudflare-pages",
|
"integrations/secret-syncs/cloudflare-pages",
|
||||||
"integrations/secret-syncs/cloudflare-workers",
|
"integrations/secret-syncs/cloudflare-workers",
|
||||||
"integrations/secret-syncs/databricks",
|
"integrations/secret-syncs/databricks",
|
||||||
@@ -1658,6 +1660,18 @@
|
|||||||
"api-reference/endpoints/app-connections/checkly/delete"
|
"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",
|
"group": "Cloudflare",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -2149,6 +2163,20 @@
|
|||||||
"api-reference/endpoints/secret-syncs/checkly/remove-secrets"
|
"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",
|
"group": "Cloudflare Pages",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -2304,6 +2332,7 @@
|
|||||||
"api-reference/endpoints/secret-syncs/laravel-forge/update",
|
"api-reference/endpoints/secret-syncs/laravel-forge/update",
|
||||||
"api-reference/endpoints/secret-syncs/laravel-forge/delete",
|
"api-reference/endpoints/secret-syncs/laravel-forge/delete",
|
||||||
"api-reference/endpoints/secret-syncs/laravel-forge/sync-secrets",
|
"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"
|
"api-reference/endpoints/secret-syncs/laravel-forge/remove-secrets"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 207 KiB |
@@ -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>
|
||||||
@@ -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": "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": "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": "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"}
|
{"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) {
|
].sort(function(a, b) {
|
||||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
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": "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": "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": "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"}
|
{"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) {
|
].sort(function(a, b) {
|
||||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||||
|
|||||||
|
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 { BitbucketSyncFields } from "./BitbucketSyncFields";
|
||||||
import { CamundaSyncFields } from "./CamundaSyncFields";
|
import { CamundaSyncFields } from "./CamundaSyncFields";
|
||||||
import { ChecklySyncFields } from "./ChecklySyncFields";
|
import { ChecklySyncFields } from "./ChecklySyncFields";
|
||||||
|
import { ChefSyncFields } from "./ChefSyncFields";
|
||||||
import { CloudflarePagesSyncFields } from "./CloudflarePagesSyncFields";
|
import { CloudflarePagesSyncFields } from "./CloudflarePagesSyncFields";
|
||||||
import { CloudflareWorkersSyncFields } from "./CloudflareWorkersSyncFields";
|
import { CloudflareWorkersSyncFields } from "./CloudflareWorkersSyncFields";
|
||||||
import { DatabricksSyncFields } from "./DatabricksSyncFields";
|
import { DatabricksSyncFields } from "./DatabricksSyncFields";
|
||||||
@@ -104,6 +105,8 @@ export const SecretSyncDestinationFields = () => {
|
|||||||
return <BitbucketSyncFields />;
|
return <BitbucketSyncFields />;
|
||||||
case SecretSync.LaravelForge:
|
case SecretSync.LaravelForge:
|
||||||
return <LaravelForgeSyncFields />;
|
return <LaravelForgeSyncFields />;
|
||||||
|
case SecretSync.Chef:
|
||||||
|
return <ChefSyncFields />;
|
||||||
case SecretSync.Northflank:
|
case SecretSync.Northflank:
|
||||||
return <NorthflankSyncFields />;
|
return <NorthflankSyncFields />;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
|||||||
case SecretSync.Northflank:
|
case SecretSync.Northflank:
|
||||||
case SecretSync.Bitbucket:
|
case SecretSync.Bitbucket:
|
||||||
case SecretSync.LaravelForge:
|
case SecretSync.LaravelForge:
|
||||||
|
case SecretSync.Chef:
|
||||||
AdditionalSyncOptionsFieldsComponent = null;
|
AdditionalSyncOptionsFieldsComponent = null;
|
||||||
break;
|
break;
|
||||||
default:
|
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 { BitbucketSyncReviewFields } from "./BitbucketSyncReviewFields";
|
||||||
import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields";
|
import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields";
|
||||||
import { ChecklySyncReviewFields } from "./ChecklySyncReviewFields";
|
import { ChecklySyncReviewFields } from "./ChecklySyncReviewFields";
|
||||||
|
import { ChefSyncReviewFields } from "./ChefSyncReviewFields";
|
||||||
import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields";
|
import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields";
|
||||||
import { CloudflareWorkersSyncReviewFields } from "./CloudflareWorkersReviewFields";
|
import { CloudflareWorkersSyncReviewFields } from "./CloudflareWorkersReviewFields";
|
||||||
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
|
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
|
||||||
@@ -177,6 +178,9 @@ export const SecretSyncReviewFields = () => {
|
|||||||
case SecretSync.LaravelForge:
|
case SecretSync.LaravelForge:
|
||||||
DestinationFieldsComponent = <LaravelForgeSyncReviewFields />;
|
DestinationFieldsComponent = <LaravelForgeSyncReviewFields />;
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.Chef:
|
||||||
|
DestinationFieldsComponent = <ChefSyncReviewFields />;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
|
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 { BitbucketSyncDestinationSchema } from "./bitbucket-sync-destination-schema";
|
||||||
import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema";
|
import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema";
|
||||||
import { ChecklySyncDestinationSchema } from "./checkly-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 { CloudflarePagesSyncDestinationSchema } from "./cloudflare-pages-sync-destination-schema";
|
||||||
import { CloudflareWorkersSyncDestinationSchema } from "./cloudflare-workers-sync-destination-schema";
|
import { CloudflareWorkersSyncDestinationSchema } from "./cloudflare-workers-sync-destination-schema";
|
||||||
import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema";
|
import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema";
|
||||||
@@ -65,7 +66,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
|||||||
NetlifySyncDestinationSchema,
|
NetlifySyncDestinationSchema,
|
||||||
NorthflankSyncDestinationSchema,
|
NorthflankSyncDestinationSchema,
|
||||||
BitbucketSyncDestinationSchema,
|
BitbucketSyncDestinationSchema,
|
||||||
LaravelForgeSyncDestinationSchema
|
LaravelForgeSyncDestinationSchema,
|
||||||
|
ChefSyncDestinationSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const SecretSyncFormSchema = SecretSyncUnionSchema;
|
export const SecretSyncFormSchema = SecretSyncUnionSchema;
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
} from "@app/hooks/api/appConnections/types";
|
} from "@app/hooks/api/appConnections/types";
|
||||||
import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection";
|
import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection";
|
||||||
import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-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 { DigitalOceanConnectionMethod } from "@app/hooks/api/appConnections/types/digital-ocean";
|
||||||
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
|
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
|
||||||
import { LaravelForgeConnectionMethod } from "@app/hooks/api/appConnections/types/laravel-forge-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",
|
name: "Laravel Forge",
|
||||||
image: "Laravel Forge.png",
|
image: "Laravel Forge.png",
|
||||||
size: 65
|
size: 65
|
||||||
}
|
},
|
||||||
|
[AppConnection.Chef]: { name: "Chef", image: "Chef.png" }
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
|
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
|
||||||
@@ -202,6 +204,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
|||||||
case RenderConnectionMethod.ApiKey:
|
case RenderConnectionMethod.ApiKey:
|
||||||
case ChecklyConnectionMethod.ApiKey:
|
case ChecklyConnectionMethod.ApiKey:
|
||||||
return { name: "API Key", icon: faKey };
|
return { name: "API Key", icon: faKey };
|
||||||
|
case ChefConnectionMethod.UserKey:
|
||||||
|
return { name: "User Key", icon: faKey };
|
||||||
case AzureClientSecretsConnectionMethod.ClientSecret:
|
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||||
case AzureAppConfigurationConnectionMethod.ClientSecret:
|
case AzureAppConfigurationConnectionMethod.ClientSecret:
|
||||||
case AzureKeyVaultConnectionMethod.ClientSecret:
|
case AzureKeyVaultConnectionMethod.ClientSecret:
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
|
|||||||
[SecretSync.LaravelForge]: {
|
[SecretSync.LaravelForge]: {
|
||||||
name: "Laravel Forge",
|
name: "Laravel Forge",
|
||||||
image: "Laravel Forge.png"
|
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.Netlify]: AppConnection.Netlify,
|
||||||
[SecretSync.Northflank]: AppConnection.Northflank,
|
[SecretSync.Northflank]: AppConnection.Northflank,
|
||||||
[SecretSync.Bitbucket]: AppConnection.Bitbucket,
|
[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<
|
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./queries";
|
||||||
|
export * from "./types";
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export type TChefDataBag = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TChefDataBagItem = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
@@ -39,5 +39,6 @@ export enum AppConnection {
|
|||||||
Northflank = "northflank",
|
Northflank = "northflank",
|
||||||
Okta = "okta",
|
Okta = "okta",
|
||||||
Redis = "redis",
|
Redis = "redis",
|
||||||
LaravelForge = "laravel-forge"
|
LaravelForge = "laravel-forge",
|
||||||
|
Chef = "chef"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,10 @@ export type TChecklyConnectionOption = TAppConnectionOptionBase & {
|
|||||||
app: AppConnection.Checkly;
|
app: AppConnection.Checkly;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TChefConnectionOption = TAppConnectionOptionBase & {
|
||||||
|
app: AppConnection.Chef;
|
||||||
|
};
|
||||||
|
|
||||||
export type TSupabaseConnectionOption = TAppConnectionOptionBase & {
|
export type TSupabaseConnectionOption = TAppConnectionOptionBase & {
|
||||||
app: AppConnection.Supabase;
|
app: AppConnection.Supabase;
|
||||||
};
|
};
|
||||||
@@ -220,7 +224,8 @@ export type TAppConnectionOption =
|
|||||||
| TNorthflankConnectionOption
|
| TNorthflankConnectionOption
|
||||||
| TOktaConnectionOption
|
| TOktaConnectionOption
|
||||||
| TAzureAdCsConnectionOption
|
| TAzureAdCsConnectionOption
|
||||||
| TLaravelForgeConnectionOption;
|
| TLaravelForgeConnectionOption
|
||||||
|
| TChefConnectionOption;
|
||||||
|
|
||||||
export type TAppConnectionOptionMap = {
|
export type TAppConnectionOptionMap = {
|
||||||
[AppConnection.AWS]: TAwsConnectionOption;
|
[AppConnection.AWS]: TAwsConnectionOption;
|
||||||
@@ -264,4 +269,5 @@ export type TAppConnectionOptionMap = {
|
|||||||
[AppConnection.AzureADCS]: TAzureAdCsConnectionOption;
|
[AppConnection.AzureADCS]: TAzureAdCsConnectionOption;
|
||||||
[AppConnection.Redis]: TRedisConnectionOption;
|
[AppConnection.Redis]: TRedisConnectionOption;
|
||||||
[AppConnection.LaravelForge]: TLaravelForgeConnectionOption;
|
[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 { TBitbucketConnection } from "./bitbucket-connection";
|
||||||
import { TCamundaConnection } from "./camunda-connection";
|
import { TCamundaConnection } from "./camunda-connection";
|
||||||
import { TChecklyConnection } from "./checkly-connection";
|
import { TChecklyConnection } from "./checkly-connection";
|
||||||
|
import { TChefConnection } from "./chef-connection";
|
||||||
import { TCloudflareConnection } from "./cloudflare-connection";
|
import { TCloudflareConnection } from "./cloudflare-connection";
|
||||||
import { TDatabricksConnection } from "./databricks-connection";
|
import { TDatabricksConnection } from "./databricks-connection";
|
||||||
import { TDigitalOceanConnection } from "./digital-ocean";
|
import { TDigitalOceanConnection } from "./digital-ocean";
|
||||||
@@ -53,6 +54,7 @@ export * from "./azure-key-vault-connection";
|
|||||||
export * from "./bitbucket-connection";
|
export * from "./bitbucket-connection";
|
||||||
export * from "./camunda-connection";
|
export * from "./camunda-connection";
|
||||||
export * from "./checkly-connection";
|
export * from "./checkly-connection";
|
||||||
|
export * from "./chef-connection";
|
||||||
export * from "./cloudflare-connection";
|
export * from "./cloudflare-connection";
|
||||||
export * from "./databricks-connection";
|
export * from "./databricks-connection";
|
||||||
export * from "./flyio-connection";
|
export * from "./flyio-connection";
|
||||||
@@ -124,7 +126,8 @@ export type TAppConnection =
|
|||||||
| TNetlifyConnection
|
| TNetlifyConnection
|
||||||
| TNorthflankConnection
|
| TNorthflankConnection
|
||||||
| TOktaConnection
|
| TOktaConnection
|
||||||
| TRedisConnection;
|
| TRedisConnection
|
||||||
|
| TChefConnection;
|
||||||
|
|
||||||
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id" | "projectId">;
|
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id" | "projectId">;
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export enum SecretSync {
|
|||||||
Netlify = "netlify",
|
Netlify = "netlify",
|
||||||
Northflank = "northflank",
|
Northflank = "northflank",
|
||||||
Bitbucket = "bitbucket",
|
Bitbucket = "bitbucket",
|
||||||
LaravelForge = "laravel-forge"
|
LaravelForge = "laravel-forge",
|
||||||
|
Chef = "chef"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SecretSyncStatus {
|
export enum SecretSyncStatus {
|
||||||
|
|||||||
@@ -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 { TBitbucketSync } from "./bitbucket-sync";
|
||||||
import { TCamundaSync } from "./camunda-sync";
|
import { TCamundaSync } from "./camunda-sync";
|
||||||
import { TChecklySync } from "./checkly-sync";
|
import { TChecklySync } from "./checkly-sync";
|
||||||
|
import { TChefSync } from "./chef-sync";
|
||||||
import { TCloudflarePagesSync } from "./cloudflare-pages-sync";
|
import { TCloudflarePagesSync } from "./cloudflare-pages-sync";
|
||||||
import { TCloudflareWorkersSync } from "./cloudflare-workers-sync";
|
import { TCloudflareWorkersSync } from "./cloudflare-workers-sync";
|
||||||
import { TDatabricksSync } from "./databricks-sync";
|
import { TDatabricksSync } from "./databricks-sync";
|
||||||
@@ -73,7 +74,8 @@ export type TSecretSync =
|
|||||||
| TNetlifySync
|
| TNetlifySync
|
||||||
| TNorthflankSync
|
| TNorthflankSync
|
||||||
| TBitbucketSync
|
| TBitbucketSync
|
||||||
| TLaravelForgeSync;
|
| TLaravelForgeSync
|
||||||
|
| TChefSync;
|
||||||
|
|
||||||
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
|
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm";
|
|||||||
import { BitbucketConnectionForm } from "./BitbucketConnectionForm";
|
import { BitbucketConnectionForm } from "./BitbucketConnectionForm";
|
||||||
import { CamundaConnectionForm } from "./CamundaConnectionForm";
|
import { CamundaConnectionForm } from "./CamundaConnectionForm";
|
||||||
import { ChecklyConnectionForm } from "./ChecklyConnectionForm";
|
import { ChecklyConnectionForm } from "./ChecklyConnectionForm";
|
||||||
|
import { ChefConnectionForm } from "./ChefConnectionForm";
|
||||||
import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
|
import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
|
||||||
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
|
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
|
||||||
import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm";
|
import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm";
|
||||||
@@ -164,6 +165,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
|
|||||||
return <RailwayConnectionForm onSubmit={onSubmit} />;
|
return <RailwayConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.Checkly:
|
case AppConnection.Checkly:
|
||||||
return <ChecklyConnectionForm onSubmit={onSubmit} />;
|
return <ChecklyConnectionForm onSubmit={onSubmit} />;
|
||||||
|
case AppConnection.Chef:
|
||||||
|
return <ChefConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.Supabase:
|
case AppConnection.Supabase:
|
||||||
return <SupabaseConnectionForm onSubmit={onSubmit} />;
|
return <SupabaseConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.DigitalOcean:
|
case AppConnection.DigitalOcean:
|
||||||
@@ -329,6 +332,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
|||||||
return <RailwayConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
return <RailwayConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||||
case AppConnection.Checkly:
|
case AppConnection.Checkly:
|
||||||
return <ChecklyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
return <ChecklyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||||
|
case AppConnection.Chef:
|
||||||
|
return <ChefConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||||
case AppConnection.Supabase:
|
case AppConnection.Supabase:
|
||||||
return <SupabaseConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
return <SupabaseConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||||
case AppConnection.DigitalOcean:
|
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 { BitbucketSyncDestinationCol } from "./BitbucketSyncDestinationCol";
|
||||||
import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol";
|
import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol";
|
||||||
import { ChecklySyncDestinationCol } from "./ChecklySyncDestinationCol";
|
import { ChecklySyncDestinationCol } from "./ChecklySyncDestinationCol";
|
||||||
|
import { ChefSyncDestinationCol } from "./ChefSyncDestinationCol";
|
||||||
import { CloudflarePagesSyncDestinationCol } from "./CloudflarePagesSyncDestinationCol";
|
import { CloudflarePagesSyncDestinationCol } from "./CloudflarePagesSyncDestinationCol";
|
||||||
import { CloudflareWorkersSyncDestinationCol } from "./CloudflareWorkersSyncDestinationCol";
|
import { CloudflareWorkersSyncDestinationCol } from "./CloudflareWorkersSyncDestinationCol";
|
||||||
import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol";
|
import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol";
|
||||||
@@ -103,6 +104,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
|
|||||||
return <BitbucketSyncDestinationCol secretSync={secretSync} />;
|
return <BitbucketSyncDestinationCol secretSync={secretSync} />;
|
||||||
case SecretSync.LaravelForge:
|
case SecretSync.LaravelForge:
|
||||||
return <LaravelForgeSyncDestinationCol secretSync={secretSync} />;
|
return <LaravelForgeSyncDestinationCol secretSync={secretSync} />;
|
||||||
|
case SecretSync.Chef:
|
||||||
|
return <ChefSyncDestinationCol secretSync={secretSync} />;
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
|
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
|
||||||
|
|||||||
@@ -202,6 +202,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
|
|||||||
primaryText = destinationConfig.siteName || destinationConfig.siteId;
|
primaryText = destinationConfig.siteName || destinationConfig.siteId;
|
||||||
secondaryText = destinationConfig.orgName || destinationConfig.orgSlug;
|
secondaryText = destinationConfig.orgName || destinationConfig.orgSlug;
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.Chef:
|
||||||
|
primaryText = destinationConfig.dataBagName;
|
||||||
|
secondaryText = destinationConfig.dataBagItemName;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled Destination Col Values ${destination}`);
|
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 { BitbucketSyncDestinationSection } from "./BitbucketSyncDestinationSection";
|
||||||
import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection";
|
import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection";
|
||||||
import { ChecklySyncDestinationSection } from "./ChecklySyncDestinationSection";
|
import { ChecklySyncDestinationSection } from "./ChecklySyncDestinationSection";
|
||||||
|
import { ChefSyncDestinationSection } from "./ChefSyncDestinationSection";
|
||||||
import { CloudflarePagesSyncDestinationSection } from "./CloudflarePagesSyncDestinationSection";
|
import { CloudflarePagesSyncDestinationSection } from "./CloudflarePagesSyncDestinationSection";
|
||||||
import { CloudflareWorkersSyncDestinationSection } from "./CloudflareWorkersSyncDestinationSection";
|
import { CloudflareWorkersSyncDestinationSection } from "./CloudflareWorkersSyncDestinationSection";
|
||||||
import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection";
|
import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection";
|
||||||
@@ -156,6 +157,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
|
|||||||
case SecretSync.LaravelForge:
|
case SecretSync.LaravelForge:
|
||||||
DestinationComponents = <LaravelForgeSyncDestinationSection secretSync={secretSync} />;
|
DestinationComponents = <LaravelForgeSyncDestinationSection secretSync={secretSync} />;
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.Chef:
|
||||||
|
DestinationComponents = <ChefSyncDestinationSection secretSync={secretSync} />;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled Destination Section components: ${destination}`);
|
throw new Error(`Unhandled Destination Section components: ${destination}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
|||||||
case SecretSync.Northflank:
|
case SecretSync.Northflank:
|
||||||
case SecretSync.Bitbucket:
|
case SecretSync.Bitbucket:
|
||||||
case SecretSync.LaravelForge:
|
case SecretSync.LaravelForge:
|
||||||
|
case SecretSync.Chef:
|
||||||
AdditionalSyncOptionsComponent = null;
|
AdditionalSyncOptionsComponent = null;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||