Finish preliminary Vault integration, made docs for Vault and Checkly

This commit is contained in:
Tuan Dang
2023-06-09 15:36:37 +01:00
parent c51b194ba6
commit 00dfcfcf4e
48 changed files with 680 additions and 63 deletions
@@ -213,6 +213,8 @@ const IntegrationTile = ({
};
if (!integrationApp && integration.integration !== "checkly") return <div />;
const isSelected = integration.integration === 'hashicorp-vault' ? `${integration.app} - path: ${integration.path}` : integrationApp;
return (
<div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md bg-mineshaft-800 border border-mineshaft-600 p-6">
@@ -245,13 +247,15 @@ const IntegrationTile = ({
</div>
<div className="mr-2">
<div className="mb-2 text-xs font-semibold text-gray-400">APP</div>
{integrationApp ? <div title={integrationApp}><ListBox
{integrationApp ? <div title={integrationApp}>
<ListBox
data={!integration.isActive ? apps.map((app) => app.name) : null}
isSelected={integrationApp}
isSelected={isSelected}
onChange={(app) => {
setIntegrationApp(app);
}}
/></div> : <div className='w-52 h-10 rounded-md bg-mineshaft-600 animate-pulse px-4 font-bold py-2'>-</div>}
/>
</div> : <div className='w-52 h-10 rounded-md bg-mineshaft-600 animate-pulse px-4 font-bold py-2'>-</div>}
</div>
{renderIntegrationSpecificParams(integration)}
</div>
@@ -1,10 +1,12 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
workspaceId: string | null;
integration: string | undefined;
accessId: string | null;
accessToken: string;
workspaceId: string | null;
integration: string | undefined;
accessId: string | null;
accessToken: string;
url: string | null;
namespace: string | null;
}
/**
* This route creates a new integration authorization for integration [integration]
@@ -15,13 +17,17 @@ interface Props {
* @param {String} obj.workspaceId - id of workspace to authorize integration for
* @param {String} obj.integration - integration
* @param {String} obj.accessToken - access token to save
* @param {String} obj.url - URL of the Vault instance
* @param {String} obj.namespace - Vault-specific namespace param
* @returns
*/
const saveIntegrationAccessToken = ({
workspaceId,
integration,
accessId,
accessToken
accessToken,
url,
namespace
}: Props) =>
SecurityClient.fetchCall(`/api/v1/integration-auth/access-token`, {
method: 'POST',
@@ -32,7 +38,9 @@ const saveIntegrationAccessToken = ({
workspaceId,
integration,
accessId,
accessToken
accessToken,
url,
namespace
})
}).then(async (res) => {
if (res && res.status === 200) {
+6
View File
@@ -216,6 +216,9 @@ export default function Integrations() {
case 'railway':
link = `${window.location.origin}/integrations/railway/authorize`;
break;
case 'hashicorp-vault':
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
break;
default:
break;
}
@@ -277,6 +280,9 @@ export default function Integrations() {
case 'railway':
link = `${window.location.origin}/integrations/railway/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'hashicorp-vault':
link = `${window.location.origin}/integrations/hashicorp-vault/create?integrationAuthId=${integrationAuth._id}`;
break;
default:
break;
}
@@ -34,7 +34,9 @@ export default function AWSParameterStoreAuthorizeIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'aws-parameter-store',
accessId: accessKey,
accessToken: accessSecretKey
accessToken: accessSecretKey,
url: null,
namespace: null
});
setAccessKey('');
@@ -58,7 +60,11 @@ export default function AWSParameterStoreAuthorizeIntegrationPage() {
errorText={accessKeyErrorText}
isError={accessKeyErrorText !== '' ?? false}
>
<Input placeholder="" value={accessKey} onChange={(e) => setAccessKey(e.target.value)} />
<Input
placeholder=""
value={accessKey}
onChange={(e) => setAccessKey(e.target.value)}
/>
</FormControl>
<FormControl
label="Secret Access Key"
@@ -34,7 +34,9 @@ export default function AWSSecretManagerCreateIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'aws-secret-manager',
accessId: accessKey,
accessToken: accessSecretKey
accessToken: accessSecretKey,
url: null,
namespace: null
});
setAccessKey('');
@@ -24,7 +24,9 @@ export default function ChecklyCreateIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'checkly',
accessId: null,
accessToken
accessToken,
url: null,
namespace: null
});
setIsLoading(false);
@@ -24,7 +24,9 @@ export default function CircleCICreateIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'circleci',
accessToken: apiKey,
accessId: null
accessId: null,
url: null,
namespace:null
});
setIsLoading(false);
@@ -24,7 +24,9 @@ export default function FlyioCreateIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'flyio',
accessId: null,
accessToken
accessToken,
url: null,
namespace: null
});
setIsLoading(false);
@@ -0,0 +1,129 @@
import { useState } from 'react';
import { useRouter } from 'next/router';
import { Button, Card, CardTitle, FormControl, Input } from '../../../components/v2';
import saveIntegrationAccessToken from '../../api/integrations/saveIntegrationAccessToken';
export default function HashiCorpVaultAuthorizeIntegrationPage() {
const router = useRouter();
const [vaultURL, setVaultURL] = useState('');
const [vaultURLErrorText, setVaultURLErrorText] = useState('');
const [vaultNamespace, setVaultNamespace] = useState('');
const [vaultNamespaceErrorText, setVaultNamespaceErrorText] = useState('');
const [vaultRoleID, setVaultRoleID] = useState('');
const [vaultRoleIDErrorText, setVaultRoleIDErrorText] = useState('');
const [vaultSecretID, setVaultSecretID] = useState('');
const [vaultSecretIDErrorText, setVaultSecretIDErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
if (vaultURL.length === 0) {
setVaultURLErrorText('Vault Cluster URL cannot be blank');
} else {
setVaultURLErrorText('');
}
if (vaultNamespace.length === 0) {
setVaultNamespaceErrorText('Vault Namespace cannot be blank');
} else {
setVaultNamespaceErrorText('');
}
if (vaultRoleID.length === 0) {
setVaultRoleIDErrorText('Vault Role ID cannot be blank');
} else {
setVaultRoleIDErrorText('');
}
if (vaultSecretID.length === 0) {
setVaultSecretIDErrorText('Vault Secret ID cannot be blank');
} else {
setVaultSecretIDErrorText('');
}
if (
vaultURL.length === 0 ||
vaultNamespace.length === 0 ||
vaultRoleID.length === 0 ||
vaultSecretID.length === 0
) {
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem('projectData.id'),
integration: 'hashicorp-vault',
accessId: vaultRoleID,
accessToken: vaultSecretID,
url: vaultURL,
namespace: vaultNamespace
});
setIsLoading(false);
router.push(`/integrations/hashicorp-vault/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Vault Integration</CardTitle>
<FormControl
label="Vault Cluster URL"
errorText={vaultURLErrorText}
isError={vaultURLErrorText !== '' ?? false}
>
<Input placeholder="" value={vaultURL} onChange={(e) => setVaultURL(e.target.value)} />
</FormControl>
<FormControl
label="Vault Namespace"
errorText={vaultNamespaceErrorText}
isError={vaultNamespaceErrorText !== '' ?? false}
>
<Input
placeholder="admin/education"
value={vaultNamespace}
onChange={(e) => setVaultNamespace(e.target.value)}
/>
</FormControl>
<FormControl
label="Vault RoleID"
errorText={vaultRoleIDErrorText}
isError={vaultRoleIDErrorText !== '' ?? false}
>
<Input placeholder="" value={vaultRoleID} onChange={(e) => setVaultRoleID(e.target.value)} />
</FormControl>
<FormControl
label="Vault SecretID"
errorText={vaultSecretIDErrorText}
isError={vaultSecretIDErrorText !== '' ?? false}
>
<Input
placeholder=""
value={vaultSecretID}
onChange={(e) => setVaultSecretID(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
>
Connect to Vault
</Button>
</Card>
</div>
);
}
HashiCorpVaultAuthorizeIntegrationPage.requireAuth = true;
@@ -0,0 +1,136 @@
import { useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Input, Select, SelectItem } from '../../../components/v2';
import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from '../../api/integrations/createIntegration';
export default function HashiCorpVaultCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
const [vaultEnginePath, setVaultEnginePath] = useState('');
const [vaultEnginePathErrorText, setVaultEnginePathErrorText ] = useState('');
const [vaultSecretPath, setVaultSecretPath] = useState('');
const [vaultSecretPathErrorText, setVaultSecretPathErrorText] = useState('');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [isLoading, setIsLoading] = useState(false);
const isValidVaultPath = (secretPath: string) => {
return !(
secretPath.length === 0 ||
secretPath.startsWith('/') ||
secretPath.endsWith('/')
);
};
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
if (!isValidVaultPath(vaultEnginePath)) {
setVaultEnginePathErrorText('Vault KV Secrets Engine Path must be valid like kv');
} else {
setVaultEnginePathErrorText('');
}
if (!isValidVaultPath(vaultSecretPath)) {
setVaultSecretPathErrorText('Vault Secret(s) Path must be valid like machine/dev');
} else {
setVaultSecretPathErrorText('');
}
if (!isValidVaultPath || !isValidVaultPath(vaultSecretPath)) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: vaultEnginePath,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: vaultSecretPath,
region: null
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem('projectData.id')}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth && workspace ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Vault Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`vault-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Vault KV Secrets Engine Path"
errorText={vaultEnginePathErrorText}
isError={vaultEnginePathErrorText !== '' ?? false}
>
<Input
placeholder="kv"
value={vaultEnginePath}
onChange={(e) => setVaultEnginePath(e.target.value)}
/>
</FormControl>
<FormControl
label="Vault Secret(s) Path"
errorText={vaultSecretPathErrorText}
isError={vaultSecretPathErrorText !== '' ?? false}
>
<Input
placeholder="machine/dev"
value={vaultSecretPath}
onChange={(e) => setVaultSecretPath(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={!(isValidVaultPath(vaultEnginePath) && isValidVaultPath(vaultSecretPath))}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
HashiCorpVaultCreateIntegrationPage.requireAuth = true;
@@ -24,7 +24,9 @@ export default function RailwayAuthorizeIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'railway',
accessId: null,
accessToken: apiKey
accessToken: apiKey,
url: null,
namespace: null
});
setIsLoading(false);
@@ -24,7 +24,9 @@ export default function RenderCreateIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'render',
accessId: null,
accessToken: apiKey
accessToken: apiKey,
url: null,
namespace: null
});
setIsLoading(false);
@@ -24,7 +24,9 @@ export default function SupabaseCreateIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'supabase',
accessToken: apiKey,
accessId: null
accessId: null,
url: null,
namespace: null
});
setIsLoading(false);
@@ -24,7 +24,9 @@ export default function TravisCICreateIntegrationPage() {
workspaceId: localStorage.getItem('projectData.id'),
integration: 'travisci',
accessToken: apiKey,
accessId: null
accessId: null,
url: null,
namespace: null
});
setIsLoading(false);