feat: finished integration sync for rundeck

This commit is contained in:
Sheen Capadngan
2024-06-04 21:10:24 +08:00
parent 2a538d9560
commit 378d6c259b
11 changed files with 96 additions and 5 deletions
+1
View File
@@ -661,6 +661,7 @@ export const INTEGRATION = {
targetServiceId: targetServiceId:
"The service based grouping identifier ID of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank", "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.", 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", path: "Path to save the synced secrets. Used by Gitlab, AWS Parameter Store, Vault",
region: "AWS region to sync secrets to.", region: "AWS region to sync secrets to.",
scope: "Scope of the provider. Used by Github, Qovery", scope: "Scope of the provider. Used by Github, Qovery",
@@ -42,6 +42,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
targetService: z.string().trim().optional().describe(INTEGRATION.CREATE.targetService), targetService: z.string().trim().optional().describe(INTEGRATION.CREATE.targetService),
targetServiceId: z.string().trim().optional().describe(INTEGRATION.CREATE.targetServiceId), targetServiceId: z.string().trim().optional().describe(INTEGRATION.CREATE.targetServiceId),
owner: z.string().trim().optional().describe(INTEGRATION.CREATE.owner), 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), path: z.string().trim().optional().describe(INTEGRATION.CREATE.path),
region: z.string().trim().optional().describe(INTEGRATION.CREATE.region), region: z.string().trim().optional().describe(INTEGRATION.CREATE.region),
scope: z.string().trim().optional().describe(INTEGRATION.CREATE.scope), scope: z.string().trim().optional().describe(INTEGRATION.CREATE.scope),
@@ -199,6 +199,7 @@ export const integrationAuthServiceFactory = ({
projectId, projectId,
namespace, namespace,
integration, integration,
url,
algorithm: SecretEncryptionAlgo.AES_256_GCM, algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8, keyEncoding: SecretKeyEncoding.UTF8,
...(integration === Integrations.GCP_SECRET_MANAGER ...(integration === Integrations.GCP_SECRET_MANAGER
@@ -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<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
interface RundeckSecretResource {
name: string;
}
interface RundeckSecretsGetRes {
resources: RundeckSecretResource[];
}
let existingRundeckSecrets: string[] = [];
try {
const listResult = await request.get<RundeckSecretsGetRes>(
`${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] * Sync/push [secrets] to [app] in integration named [integration]
* *
@@ -3621,6 +3689,13 @@ export const syncIntegrationSecrets = async ({
accessToken accessToken
}); });
break; break;
case Integrations.RUNDECK:
await syncSecretsRundeck({
integration,
secrets,
accessToken
});
break;
default: default:
throw new BadRequestError({ message: "Invalid integration" }); throw new BadRequestError({ message: "Invalid integration" });
} }
@@ -43,6 +43,7 @@ export const integrationServiceFactory = ({
scope, scope,
actorId, actorId,
region, region,
url,
isActive, isActive,
metadata, metadata,
secretPath, secretPath,
@@ -87,6 +88,7 @@ export const integrationServiceFactory = ({
region, region,
scope, scope,
owner, owner,
url,
appId, appId,
path, path,
app, app,
@@ -12,6 +12,7 @@ export type TCreateIntegrationDTO = {
targetService?: string; targetService?: string;
targetServiceId?: string; targetServiceId?: string;
owner?: string; owner?: string;
url?: string;
path?: string; path?: string;
region?: string; region?: string;
scope?: string; scope?: string;
+2 -1
View File
@@ -32,7 +32,8 @@ const integrationSlugNameMapping: Mapping = {
northflank: "Northflank", northflank: "Northflank",
windmill: "Windmill", windmill: "Windmill",
"gcp-secret-manager": "GCP Secret Manager", "gcp-secret-manager": "GCP Secret Manager",
"hasura-cloud": "Hasura Cloud" "hasura-cloud": "Hasura Cloud",
rundeck: "Rundeck"
}; };
const envMapping: Mapping = { const envMapping: Mapping = {
@@ -7,6 +7,7 @@ export type IntegrationAuth = {
updatedAt: string; updatedAt: string;
algorithm: string; algorithm: string;
keyEncoding: string; keyEncoding: string;
url?: string;
teamId?: string; teamId?: string;
}; };
@@ -41,6 +41,7 @@ export const useCreateIntegration = () => {
owner, owner,
path, path,
region, region,
url,
scope, scope,
secretPath, secretPath,
metadata metadata
@@ -56,6 +57,7 @@ export const useCreateIntegration = () => {
targetService?: string; targetService?: string;
targetServiceId?: string; targetServiceId?: string;
owner?: string; owner?: string;
url?: string;
path?: string; path?: string;
region?: string; region?: string;
scope?: string; scope?: string;
@@ -85,6 +87,7 @@ export const useCreateIntegration = () => {
targetEnvironmentId, targetEnvironmentId,
targetService, targetService,
targetServiceId, targetServiceId,
url,
owner, owner,
path, path,
scope, scope,
@@ -38,7 +38,10 @@ export default function RundeckCreateIntegrationPage() {
watch, watch,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<TFormSchema>({ } = useForm<TFormSchema>({
resolver: zodResolver(schema) resolver: zodResolver(schema),
defaultValues: {
secretPath: "/"
}
}); });
const router = useRouter(); const router = useRouter();
const { mutateAsync } = useCreateIntegration(); const { mutateAsync } = useCreateIntegration();
@@ -60,6 +63,7 @@ export default function RundeckCreateIntegrationPage() {
isActive: true, isActive: true,
path: keyStoragePath, path: keyStoragePath,
sourceEnvironment, sourceEnvironment,
url: integrationAuth.url,
secretPath secretPath
}); });
@@ -158,7 +162,7 @@ export default function RundeckCreateIntegrationPage() {
placeholder={`keys/project/${workspace.name placeholder={`keys/project/${workspace.name
.toLowerCase() .toLowerCase()
.replace(/ /g, "-")}/${selectedSourceEnvironment}`} .replace(/ /g, "-")}/${selectedSourceEnvironment}`}
value={field.value} {...field}
/> />
</FormControl> </FormControl>
)} )}
@@ -141,7 +141,8 @@ export const IntegrationsSection = ({
label={ label={
(integration.integration === "qovery" && integration?.scope) || (integration.integration === "qovery" && integration?.scope) ||
(integration.integration === "aws-secret-manager" && "Secret") || (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?.integration === "terraform-cloud" && "Project") ||
(integration?.scope === "github-org" && "Organization") || (integration?.scope === "github-org" && "Organization") ||
(["github-repo", "github-env"].includes(integration?.scope as string) && (["github-repo", "github-env"].includes(integration?.scope as string) &&
@@ -153,7 +154,7 @@ export const IntegrationsSection = ({
{(integration.integration === "hashicorp-vault" && {(integration.integration === "hashicorp-vault" &&
`${integration.app} - path: ${integration.path}`) || `${integration.app} - path: ${integration.path}`) ||
(integration.scope === "github-org" && `${integration.owner}`) || (integration.scope === "github-org" && `${integration.owner}`) ||
(integration.integration === "aws-parameter-store" && (["aws-parameter-store", "rundeck"].includes(integration.integration) &&
`${integration.path}`) || `${integration.path}`) ||
(integration.scope?.startsWith("github-") && (integration.scope?.startsWith("github-") &&
`${integration.owner}/${integration.app}`) || `${integration.owner}/${integration.app}`) ||