From 378d6c259b8489ecb28667bbaa19eb764e796043 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 4 Jun 2024 21:10:24 +0800 Subject: [PATCH] feat: finished integration sync for rundeck --- backend/src/lib/api-docs/constants.ts | 1 + .../server/routes/v1/integration-router.ts | 1 + .../integration-auth-service.ts | 1 + .../integration-sync-secret.ts | 75 +++++++++++++++++++ .../integration/integration-service.ts | 2 + .../services/integration/integration-types.ts | 1 + frontend/public/data/frequentConstants.ts | 3 +- .../src/hooks/api/integrationAuth/types.ts | 1 + .../src/hooks/api/integrations/queries.tsx | 3 + .../src/pages/integrations/rundeck/create.tsx | 8 +- .../IntegrationsSection.tsx | 5 +- 11 files changed, 96 insertions(+), 5 deletions(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 70f5ed608..da82016f1 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -661,6 +661,7 @@ export const INTEGRATION = { targetServiceId: "The service based grouping identifier ID of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank", owner: "External integration providers service entity owner. Used in Github.", + url: "The self-hosted URL of the platform to integrate with", path: "Path to save the synced secrets. Used by Gitlab, AWS Parameter Store, Vault", region: "AWS region to sync secrets to.", scope: "Scope of the provider. Used by Github, Qovery", diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index f23abc45b..bdb58aa8b 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -42,6 +42,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { targetService: z.string().trim().optional().describe(INTEGRATION.CREATE.targetService), targetServiceId: z.string().trim().optional().describe(INTEGRATION.CREATE.targetServiceId), owner: z.string().trim().optional().describe(INTEGRATION.CREATE.owner), + url: z.string().trim().optional().describe(INTEGRATION.CREATE.url), path: z.string().trim().optional().describe(INTEGRATION.CREATE.path), region: z.string().trim().optional().describe(INTEGRATION.CREATE.region), scope: z.string().trim().optional().describe(INTEGRATION.CREATE.scope), diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index 74d881d26..02091d88c 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -199,6 +199,7 @@ export const integrationAuthServiceFactory = ({ projectId, namespace, integration, + url, algorithm: SecretEncryptionAlgo.AES_256_GCM, keyEncoding: SecretKeyEncoding.UTF8, ...(integration === Integrations.GCP_SECRET_MANAGER diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 12c691085..fadcd9ad3 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -3355,6 +3355,74 @@ const syncSecretsHasuraCloud = async ({ } }; +/** Sync/push [secrets] to Rundeck + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for Rundeck integration + */ +const syncSecretsRundeck = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + interface RundeckSecretResource { + name: string; + } + interface RundeckSecretsGetRes { + resources: RundeckSecretResource[]; + } + + let existingRundeckSecrets: string[] = []; + + try { + const listResult = await request.get( + `${integration.url}/api/44/storage/${integration.path}`, + { + headers: { + "X-Rundeck-Auth-Token": accessToken + } + } + ); + + existingRundeckSecrets = listResult.data.resources.map((res) => res.name); + } catch (err) { + logger.info("No existing rundeck secrets"); + } + + for await (const [key, value] of Object.entries(secrets)) { + if (existingRundeckSecrets.includes(key)) { + await request.put(`${integration.url}/api/44/storage/${integration.path}/${key}`, value, { + headers: { + "X-Rundeck-Auth-Token": accessToken, + "Content-Type": "application/x-rundeck-data-password" + } + }); + } else { + await request.post(`${integration.url}/api/44/storage/${integration.path}/${key}`, value, { + headers: { + "X-Rundeck-Auth-Token": accessToken, + "Content-Type": "application/x-rundeck-data-password" + } + }); + } + } + + for await (const existingSecret of existingRundeckSecrets) { + if (!(existingSecret in secrets)) { + await request.delete(`${integration.url}/api/44/storage/${integration.path}/${existingSecret}`, { + headers: { + "X-Rundeck-Auth-Token": accessToken + } + }); + } + } +}; + /** * Sync/push [secrets] to [app] in integration named [integration] * @@ -3621,6 +3689,13 @@ export const syncIntegrationSecrets = async ({ accessToken }); break; + case Integrations.RUNDECK: + await syncSecretsRundeck({ + integration, + secrets, + accessToken + }); + break; default: throw new BadRequestError({ message: "Invalid integration" }); } diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index 821267dfb..da9cfc71f 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -43,6 +43,7 @@ export const integrationServiceFactory = ({ scope, actorId, region, + url, isActive, metadata, secretPath, @@ -87,6 +88,7 @@ export const integrationServiceFactory = ({ region, scope, owner, + url, appId, path, app, diff --git a/backend/src/services/integration/integration-types.ts b/backend/src/services/integration/integration-types.ts index 1c8772478..9c75cad2d 100644 --- a/backend/src/services/integration/integration-types.ts +++ b/backend/src/services/integration/integration-types.ts @@ -12,6 +12,7 @@ export type TCreateIntegrationDTO = { targetService?: string; targetServiceId?: string; owner?: string; + url?: string; path?: string; region?: string; scope?: string; diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index 451890ef9..cf90ef659 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -32,7 +32,8 @@ const integrationSlugNameMapping: Mapping = { northflank: "Northflank", windmill: "Windmill", "gcp-secret-manager": "GCP Secret Manager", - "hasura-cloud": "Hasura Cloud" + "hasura-cloud": "Hasura Cloud", + rundeck: "Rundeck" }; const envMapping: Mapping = { diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts index 4a4c5e281..b73528384 100644 --- a/frontend/src/hooks/api/integrationAuth/types.ts +++ b/frontend/src/hooks/api/integrationAuth/types.ts @@ -7,6 +7,7 @@ export type IntegrationAuth = { updatedAt: string; algorithm: string; keyEncoding: string; + url?: string; teamId?: string; }; diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 7325dc4a3..3aa8f3ed1 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -41,6 +41,7 @@ export const useCreateIntegration = () => { owner, path, region, + url, scope, secretPath, metadata @@ -56,6 +57,7 @@ export const useCreateIntegration = () => { targetService?: string; targetServiceId?: string; owner?: string; + url?: string; path?: string; region?: string; scope?: string; @@ -85,6 +87,7 @@ export const useCreateIntegration = () => { targetEnvironmentId, targetService, targetServiceId, + url, owner, path, scope, diff --git a/frontend/src/pages/integrations/rundeck/create.tsx b/frontend/src/pages/integrations/rundeck/create.tsx index 79ea4eacd..543d9f4b0 100644 --- a/frontend/src/pages/integrations/rundeck/create.tsx +++ b/frontend/src/pages/integrations/rundeck/create.tsx @@ -38,7 +38,10 @@ export default function RundeckCreateIntegrationPage() { watch, formState: { isSubmitting } } = useForm({ - resolver: zodResolver(schema) + resolver: zodResolver(schema), + defaultValues: { + secretPath: "/" + } }); const router = useRouter(); const { mutateAsync } = useCreateIntegration(); @@ -60,6 +63,7 @@ export default function RundeckCreateIntegrationPage() { isActive: true, path: keyStoragePath, sourceEnvironment, + url: integrationAuth.url, secretPath }); @@ -158,7 +162,7 @@ export default function RundeckCreateIntegrationPage() { placeholder={`keys/project/${workspace.name .toLowerCase() .replace(/ /g, "-")}/${selectedSourceEnvironment}`} - value={field.value} + {...field} /> )} diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx index 267ff8580..d56010278 100644 --- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx @@ -141,7 +141,8 @@ export const IntegrationsSection = ({ label={ (integration.integration === "qovery" && integration?.scope) || (integration.integration === "aws-secret-manager" && "Secret") || - (integration.integration === "aws-parameter-store" && "Path") || + (["aws-parameter-store", "rundeck"].includes(integration.integration) && + "Path") || (integration?.integration === "terraform-cloud" && "Project") || (integration?.scope === "github-org" && "Organization") || (["github-repo", "github-env"].includes(integration?.scope as string) && @@ -153,7 +154,7 @@ export const IntegrationsSection = ({ {(integration.integration === "hashicorp-vault" && `${integration.app} - path: ${integration.path}`) || (integration.scope === "github-org" && `${integration.owner}`) || - (integration.integration === "aws-parameter-store" && + (["aws-parameter-store", "rundeck"].includes(integration.integration) && `${integration.path}`) || (integration.scope?.startsWith("github-") && `${integration.owner}/${integration.app}`) ||