Merge remote-tracking branch 'origin' into cert-mgmt

This commit is contained in:
Tuan Dang
2024-06-06 23:20:17 -04:00
22 changed files with 504 additions and 8 deletions

View File

@@ -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",

View File

@@ -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),

View File

@@ -199,6 +199,7 @@ export const integrationAuthServiceFactory = ({
projectId,
namespace,
integration,
url,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8,
...(integration === Integrations.GCP_SECRET_MANAGER

View File

@@ -30,7 +30,8 @@ export enum Integrations {
DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platform",
CLOUD_66 = "cloud-66",
NORTHFLANK = "northflank",
HASURA_CLOUD = "hasura-cloud"
HASURA_CLOUD = "hasura-cloud",
RUNDECK = "rundeck"
}
export enum IntegrationType {
@@ -368,6 +369,15 @@ export const getIntegrationOptions = async () => {
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "Rundeck",
slug: "rundeck",
image: "Rundeck.svg",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
}
];

View File

@@ -3355,6 +3355,82 @@ 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");
}
try {
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.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.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
}
});
}
}
} catch (err: unknown) {
throw new Error(
`Ensure that the provided Rundeck URL is accessible by Infisical and that the linked API token has sufficient permissions.\n\n${
(err as Error).message
}`
);
}
};
/**
* Sync/push [secrets] to [app] in integration named [integration]
*
@@ -3621,6 +3697,13 @@ export const syncIntegrationSecrets = async ({
accessToken
});
break;
case Integrations.RUNDECK:
await syncSecretsRundeck({
integration,
secrets,
accessToken
});
break;
default:
throw new BadRequestError({ message: "Invalid integration" });
}

View File

@@ -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,

View File

@@ -12,6 +12,7 @@ export type TCreateIntegrationDTO = {
targetService?: string;
targetServiceId?: string;
owner?: string;
url?: string;
path?: string;
region?: string;
scope?: string;

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 KiB

View File

@@ -0,0 +1,39 @@
---
title: "Rundeck"
description: "How to sync secrets from Infisical to Rundeck"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
<Steps>
<Step title="Authorize Infisical for Rundeck">
Obtain a User API Token in the Profile settings of Rundeck
![integrations rundeck token](../../images/integrations/rundeck/integrations-rundeck-token.png)
Navigate to your project's integrations tab in Infisical.
![integrations](../../images/integrations.png)
Press on the Rundeck tile and input your Rundeck instance Base URL and User API token to grant Infisical access to manage Rundeck keys
![integrations rundeck authorization](../../images/integrations/rundeck/integrations-rundeck-auth.png)
<Info>
If this is your project's first cloud integration, then you'll have to grant
Infisical access to your project's environment variables. Although this step
breaks E2EE, it's necessary for Infisical to sync the environment variables to
the cloud platform.
</Info>
</Step>
<Step title="Start integration">
Select which Infisical environment secrets you want to sync to a Rundeck Key Storage Path and press create integration to start syncing secrets to Rundeck.
![create integration rundeck](../../images/integrations/rundeck/integrations-rundeck-create.png)
![integrations rundeck](../../images/integrations/rundeck/integrations-rundeck.png)
</Step>
</Steps>

View File

@@ -26,14 +26,14 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi
| [Supabase](/integrations/cloud/supabase) | Cloud | Available |
| [Northflank](/integrations/cloud/northflank) | Cloud | Available |
| [Cloudflare Pages](/integrations/cloud/cloudflare-pages) | Cloud | Available |
| [Cloudflare Workers](/integrations/cloud/cloudflare-workers) | Cloud | Available |
| [Cloudflare Workers](/integrations/cloud/cloudflare-workers) | Cloud | Available |
| [Checkly](/integrations/cloud/checkly) | Cloud | Available |
| [Qovery](/integrations/cloud/qovery) | Cloud | Available |
| [Qovery](/integrations/cloud/qovery) | Cloud | Available |
| [HashiCorp Vault](/integrations/cloud/hashicorp-vault) | Cloud | Available |
| [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available |
| [AWS Secrets Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available |
| [AWS Secrets Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available |
| [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available |
| [GCP Secret Manager](/integrations/cloud/gcp-secret-manager) | Cloud | Available |
| [GCP Secret Manager](/integrations/cloud/gcp-secret-manager) | Cloud | Available |
| [Windmill](/integrations/cloud/windmill) | Cloud | Available |
| [BitBucket](/integrations/cicd/bitbucket) | CI/CD | Available |
| [Codefresh](/integrations/cicd/codefresh) | CI/CD | Available |
@@ -41,6 +41,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi
| [GitLab](/integrations/cicd/gitlab) | CI/CD | Available |
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Available |
| [Travis CI](/integrations/cicd/travisci) | CI/CD | Available |
| [Rundeck](/integrations/cicd/rundeck) | CI/CD | Available |
| [React](/integrations/frameworks/react) | Framework | Available |
| [Vue](/integrations/frameworks/vue) | Framework | Available |
| [Express](/integrations/frameworks/express) | Framework | Available |

View File

@@ -344,6 +344,7 @@
"pages": [
"integrations/cicd/circleci",
"integrations/cicd/travisci",
"integrations/cicd/rundeck",
"integrations/cicd/codefresh",
"integrations/cloud/checkly"
]

View File

@@ -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 = {

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="45.359 114.637 60.122 58.576"><path d="M46.83 113.864l7.608 12.01H92.5l-7.543-12.01zm15.26 23.98l3.684 5.754-3.968 6.32h38.4l3.815-6.017-3.815-5.907h-38.04zm-7.826 24.13l-7.455 11.77v.24h38.148l7.564-12.012z" fill="#f91629"/></svg>

After

Width:  |  Height:  |  Size: 303 B

View File

@@ -7,6 +7,7 @@ export type IntegrationAuth = {
updatedAt: string;
algorithm: string;
keyEncoding: string;
url?: string;
teamId?: string;
};

View File

@@ -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,

View File

@@ -0,0 +1,129 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod";
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
const schema = z.object({
authToken: z.string().trim().min(1, { message: "Rundeck Auth Token is required" }),
rundeckURL: z.string().trim().min(1, {
message: "Rundeck URL is required"
})
});
type FormData = z.infer<typeof schema>;
export default function RundeckAuthorizeIntegrationPage() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const { mutateAsync } = useSaveIntegrationAccessToken();
const { control, handleSubmit } = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
authToken: "",
rundeckURL: ""
}
});
const onFormSubmit = async ({ authToken, rundeckURL }: FormData) => {
try {
setIsLoading(true);
const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id"),
integration: "rundeck",
accessToken: authToken,
url: rundeckURL.trim()
});
setIsLoading(false);
router.push(`/integrations/rundeck/create?integrationAuthId=${integrationAuth.id}`);
} catch (err) {
setIsLoading(false);
console.error(err);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<Head>
<title>Authorize Rundeck Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
<CardTitle
className="px-6 text-left text-xl"
subTitle="After adding your URL and auth token, you will be prompted to set up an integration for a particular Infisical project and environment."
>
<div className="flex flex-row items-center">
<div className="flex items-center pb-0.5">
<Image
src="/images/integrations/Rundeck.svg"
height={30}
width={30}
alt="Rundeck logo"
/>
</div>
<span className="ml-2.5">Rundeck Integration </span>
<Link href="https://infisical.com/docs/integrations/cicd/rundeck" passHref>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
</CardTitle>
<form onSubmit={handleSubmit(onFormSubmit)} className="px-6 pb-8 text-right">
<Controller
control={control}
name="rundeckURL"
render={({ field, fieldState: { error } }) => (
<FormControl label="URL" errorText={error?.message} isError={Boolean(error)}>
<Input {...field} placeholder="https://self-hosted-rundeck.com" />
</FormControl>
)}
/>
<Controller
control={control}
name="authToken"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Rundeck Auth Token"
errorText={error?.message}
isError={Boolean(error)}
>
<Input {...field} placeholder="" />
</FormControl>
)}
/>
<Button
colorSchema="primary"
variant="outline_bg"
className="mt-2 w-min"
size="sm"
type="submit"
isLoading={isLoading}
>
Connect to Rundeck
</Button>
</form>
</Card>
</div>
);
}
RundeckAuthorizeIntegrationPage.requireAuth = true;

View File

@@ -0,0 +1,217 @@
import { Controller, useForm } from "react-hook-form";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import queryString from "query-string";
import { z } from "zod";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useCreateIntegration } from "@app/hooks/api";
import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth";
import { useGetWorkspaceById } from "@app/hooks/api/workspace";
const schema = z.object({
keyStoragePath: z.string().trim().min(1, { message: "Rundeck Key Storage path is required" }),
secretPath: z.string().trim().min(1, { message: "Secret path is required" }),
sourceEnvironment: z.string().trim().min(1, { message: "Source environment is required" })
});
type TFormSchema = z.infer<typeof schema>;
export default function RundeckCreateIntegrationPage() {
const {
control,
handleSubmit,
watch,
formState: { isSubmitting }
} = useForm<TFormSchema>({
resolver: zodResolver(schema),
defaultValues: {
secretPath: "/"
}
});
const router = useRouter();
const { mutateAsync } = useCreateIntegration();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth, isLoading: isIntegrationAuthLoading } = useGetIntegrationAuthById(
(integrationAuthId as string) ?? ""
);
const selectedSourceEnvironment = watch("sourceEnvironment");
const onFormSubmit = async ({ secretPath, sourceEnvironment, keyStoragePath }: TFormSchema) => {
try {
if (!integrationAuth?.id) return;
await mutateAsync({
integrationAuthId: integrationAuth?.id,
isActive: true,
path: keyStoragePath,
sourceEnvironment,
url: integrationAuth.url,
secretPath
});
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth && workspace ? (
<div className="flex h-full w-full flex-col items-center justify-center">
<Head>
<title>Set Up Rundeck Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<Card className="max-w-lg rounded-md border border-mineshaft-600">
<CardTitle
className="px-6 text-left text-xl"
subTitle="Choose which environment or folder in Infisical you want to sync to the Rundeck Key Storage."
>
<div className="flex flex-row items-center">
<div className="flex items-center pb-0.5">
<Image
src="/images/integrations/Rundeck.svg"
height={30}
width={30}
alt="Rundeck logo"
/>
</div>
<span className="ml-2.5">Rundeck Integration </span>
<Link href="https://infisical.com/docs/integrations/cloud/flyio" passHref>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
</CardTitle>
<form onSubmit={handleSubmit(onFormSubmit)} className="flex w-full flex-col px-6">
<Controller
control={control}
name="sourceEnvironment"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project Environment"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
className="w-full border border-mineshaft-500"
value={field.value}
onValueChange={(val) => {
field.onChange(val);
}}
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl label="Secrets Path" errorText={error?.message} isError={Boolean(error)}>
<SecretPathInput {...field} environment={selectedSourceEnvironment} />
</FormControl>
)}
/>
<Controller
control={control}
name="keyStoragePath"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Rundeck Key Storage Path"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
placeholder={`keys/project/${workspace.name
.toLowerCase()
.replace(/ /g, "-")}/${selectedSourceEnvironment}`}
{...field}
/>
</FormControl>
)}
/>
<Button
type="submit"
color="mineshaft"
variant="outline_bg"
className="mb-6 mt-2 ml-auto"
isLoading={isSubmitting}
>
Create Integration
</Button>
</form>
</Card>
</div>
) : (
<div className="flex h-full w-full items-center justify-center">
<Head>
<title>Set Up Rundeck Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
{isIntegrationAuthLoading ? (
<img
src="/images/loading/loading.gif"
height={70}
width={120}
alt="infisical loading indicator"
/>
) : (
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
<p>
Something went wrong. Please contact{" "}
<a
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
target="_blank"
rel="noopener noreferrer"
href="mailto:support@infisical.com"
>
support@infisical.com
</a>{" "}
if the issue persists.
</p>
</div>
)}
</div>
);
}
RundeckCreateIntegrationPage.requireAuth = true;

View File

@@ -128,6 +128,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "hasura-cloud":
link = `${window.location.origin}/integrations/hasura-cloud/authorize`;
break;
case "rundeck":
link = `${window.location.origin}/integrations/rundeck/authorize`;
break;
default:
break;
}

View File

@@ -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}`) ||