Finish first iteration of Railway integration

This commit is contained in:
Tuan Dang
2023-04-09 01:31:40 +03:00
parent 57ee287dd2
commit cbf05b7c31
24 changed files with 300 additions and 32 deletions

View File

@ -13,7 +13,8 @@ import {
revokeAccess
} from '../../integrations';
import {
INTEGRATION_VERCEL_API_URL
INTEGRATION_VERCEL_API_URL,
INTEGRATION_RAILWAY_API_URL
} from '../../variables';
import request from '../../config/request';
@ -203,7 +204,8 @@ export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
}
/**
* Return list of available Vercel (preview) branches
* Return list of available Vercel (preview) branches for Vercel project with
* id [appId]
* @param req
* @param res
*/
@ -246,6 +248,73 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon
});
}
/**
* Return list of available Railway environments for Railway project with
* id [appId]
* @param req
* @param res
*/
export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: Response) => {
const { integrationAuthId } = req.params;
const appId = req.query.appId as string;
interface RailwayEnvironment {
node: {
id: string;
name: string;
isEphemeral: boolean;
}
}
interface Environment {
environmentId: string;
name: string;
}
let environments: Environment[] = [];
if (appId && appId !== '') {
const query = `
query GetEnvironments($projectId: String!, $after: String, $before: String, $first: Int, $isEphemeral: Boolean, $last: Int) {
environments(projectId: $projectId, after: $after, before: $before, first: $first, isEphemeral: $isEphemeral, last: $last) {
edges {
node {
id
name
isEphemeral
}
}
}
}
`;
const variables = {
projectId: appId
}
const { data: { data: { environments: { edges } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables,
}, {
headers: {
'Authorization': `Bearer ${req.accessToken}`,
'Content-Type': 'application/json',
},
});
environments = edges.map((e: RailwayEnvironment) => {
return ({
name: e.node.name,
environmentId: e.node.id
});
});
}
return res.status(200).send({
environments
});
}
/**
* Delete integration authorization with id [integrationAuthId]
* @param req

View File

@ -24,6 +24,7 @@ export const createIntegration = async (req: Request, res: Response) => {
isActive,
sourceEnvironment,
targetEnvironment,
targetEnvironmentId,
owner,
path,
region
@ -39,6 +40,7 @@ export const createIntegration = async (req: Request, res: Response) => {
app,
appId,
targetEnvironment,
targetEnvironmentId,
owner,
path,
region,

View File

@ -21,6 +21,7 @@ import {
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
@ -29,11 +30,13 @@ import {
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
} from "../variables";
import request from '../config/request';
import axios from "axios";
/**
* Sync/push [secrets] to [app] in integration named [integration]
@ -126,6 +129,13 @@ const syncSecrets = async ({
accessToken,
});
break;
case INTEGRATION_RAILWAY:
await syncSecretsRailway({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_FLYIO:
await syncSecretsFlyio({
integration,
@ -1152,6 +1162,55 @@ const syncSecretsRender = async ({
}
};
/**
* Sync/push [secrets] to Railway project with id [integration.appId]
* @param {Object} obj
* @param {IIntegration} 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 Railway integration
*/
const syncSecretsRailway = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
const query = `
mutation UpsertVariables($input: VariableCollectionUpsertInput!) {
variableCollectionUpsert(input: $input)
}
`;
const input = {
projectId: integration.appId,
environmentId: integration.targetEnvironmentId,
replace: true,
variables: secrets
};
await request.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables: {
input,
},
}, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to Railway");
}
}
/**
* Sync/push [secrets] to Fly.io app
* @param {Object} obj

View File

@ -23,6 +23,7 @@ export interface IIntegration {
app: string;
owner: string;
targetEnvironment: string;
targetEnvironmentId: string;
appId: string;
path: string;
region: string;
@ -73,6 +74,10 @@ const integrationSchema = new Schema<IIntegration>(
type: String,
default: null,
},
targetEnvironmentId: {
type: String,
default: null
},
owner: {
// github-specific repo owner-login
type: String,

View File

@ -30,6 +30,7 @@ router.post( // new: add new integration for integration auth
body('appId').trim(),
body('sourceEnvironment').trim(),
body('targetEnvironment').trim(),
body('targetEnvironmentId').trim(),
body('owner').trim(),
body('path').trim(),
body('region').trim(),

View File

@ -111,6 +111,20 @@ router.get(
integrationAuthController.getIntegrationAuthVercelBranches
);
router.get(
'/:integrationAuthId/railway/environments',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationAuthId').exists().isString(),
query('appId').exists().isString(),
validateRequest,
integrationAuthController.getIntegrationAuthRailwayEnvironments
);
router.delete(
'/:integrationAuthId',
requireAuth({

View File

@ -2,5 +2,6 @@ export {
useGetIntegrationAuthApps,
useGetIntegrationAuthById,
useGetIntegrationAuthTeams,
useGetIntegrationAuthVercelBranches
useGetIntegrationAuthVercelBranches,
useGetRailwayEnvironments
} from './queries';

View File

@ -4,6 +4,7 @@ import { apiRequest } from "@app/config/request";
import {
App,
Environment,
IntegrationAuth,
Team
} from './types';
@ -18,7 +19,14 @@ const integrationAuthKeys = {
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, 'integrationAuthVercelBranches']
}) => [{ integrationAuthId, appId }, 'integrationAuthVercelBranches'] as const,
getIntegrationAuthRailwayEnvironments: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, 'integrationAuthRailwayEnvironments'] as const,
}
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
@ -62,6 +70,22 @@ const fetchIntegrationAuthVercelBranches = async ({
return branches;
};
const fetchIntegrationAuthRailwayEnvironments = async ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => {
const { data: { environments } } = await apiRequest.get<{ environments: Environment[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/environments`, {
params: {
appId
}
});
return environments;
}
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
@ -114,3 +138,23 @@ export const useGetIntegrationAuthVercelBranches = ({
enabled: true
});
}
export const useGetRailwayEnvironments = ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthRailwayEnvironments({
integrationAuthId,
appId,
}),
queryFn: () => fetchIntegrationAuthRailwayEnvironments({
integrationAuthId,
appId,
}),
enabled: true
});
}

View File

@ -15,4 +15,9 @@ export type App = {
export type Team = {
name: string;
teamId: string;
}
export type Environment = {
name: string;
environmentId: string;
}

View File

@ -7,6 +7,7 @@ interface Props {
appId: string | null;
sourceEnvironment: string;
targetEnvironment: string | null;
targetEnvironmentId: string | null;
owner: string | null;
path: string | null;
region: string | null;
@ -24,6 +25,7 @@ const createIntegration = ({
appId,
sourceEnvironment,
targetEnvironment,
targetEnvironmentId,
owner,
path,
region
@ -40,6 +42,7 @@ const createIntegration = ({
appId,
sourceEnvironment,
targetEnvironment,
targetEnvironmentId,
owner,
path,
region

View File

@ -208,7 +208,6 @@ export default function Integrations() {
link = `${window.location.origin}/integrations/travisci/authorize`;
break;
case 'railway':
console.log('handleUnauthorized Railway: ', integrationOption);
link = `${window.location.origin}/integrations/railway/authorize`;
break;
default:
@ -264,7 +263,7 @@ export default function Integrations() {
link = `${window.location.origin}/integrations/travisci/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'railway':
console.log('handleAuthorized Railway: ', integrationAuth);
link = `${window.location.origin}/integrations/railway/create?integrationAuthId=${integrationAuth._id}`;
break;
default:
break;

View File

@ -98,6 +98,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
owner: null,
path,
region: selectedAWSRegion

View File

@ -97,6 +97,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
owner: null,
path: null,
region: selectedAWSRegion

View File

@ -62,6 +62,7 @@ export default function AzureKeyVaultCreateIntegrationPage() {
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
owner: null,
path: null,
region: null

View File

@ -60,6 +60,7 @@ export default function CircleCICreateIntegrationPage() {
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
owner: null,
path: null,
region: null,

View File

@ -61,6 +61,7 @@ export default function FlyioCreateIntegrationPage() {
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
owner: null,
path: null,
region: null

View File

@ -64,6 +64,7 @@ export default function GitHubCreateIntegrationPage() {
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
owner: targetApp.owner,
path: null,
region: null

View File

@ -89,6 +89,7 @@ export default function GitLabCreateIntegrationPage() {
appId: targetAppId,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
owner: null,
path: null,
region: null

View File

@ -60,6 +60,7 @@ export default function HerokuCreateIntegrationPage() {
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
owner: null,
path: null,
region: null

View File

@ -69,6 +69,7 @@ export default function NetlifyCreateIntegrationPage() {
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
targetEnvironmentId: null,
owner: null,
path: null,
region: null

View File

@ -11,13 +11,19 @@ import {
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById,
useGetRailwayEnvironments
} from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
export default function RailwayCreateIntegrationPage() {
const router = useRouter();
const [targetAppId, setTargetAppId] = useState('');
const [targetEnvironmentId, setTargetEnvironmentId] = useState('');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [isLoading, setIsLoading] = useState(false);
@ -27,8 +33,12 @@ export default function RailwayCreateIntegrationPage() {
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: integrationAuthId as string ?? ''
});
const { data: targetEnvironments } = useGetRailwayEnvironments({
integrationAuthId: integrationAuthId as string ?? '',
appId: targetAppId
});
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
@ -45,25 +55,51 @@ export default function RailwayCreateIntegrationPage() {
}
}, [integrationAuthApps]);
useEffect(() => {
if (targetEnvironments) {
if (targetEnvironments.length > 0) {
setTargetEnvironmentId(targetEnvironments[0].environmentId);
} else {
setTargetEnvironmentId('none');
}
}
}, [targetEnvironments]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
// const targetApp = integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId))?.name;
const targetApp = integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId);
const targetEnvironment = targetEnvironments?.find((environment) => environment.environmentId === targetEnvironmentId);
console.log('handleButtonClick');
if (!targetApp || !targetApp.appId || !targetEnvironment) return;
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp.name,
appId: targetApp.appId,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: targetEnvironment.name,
targetEnvironmentId: targetEnvironment.environmentId,
owner: null,
path: null,
region: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
console.log('integrationAuthApps', integrationAuthApps);
return workspace && selectedSourceEnvironment && integrationAuthApps ? (
return workspace && selectedSourceEnvironment && integrationAuthApps && targetEnvironments ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className="text-center">Railway Integration</CardTitle>
@ -84,24 +120,43 @@ export default function RailwayCreateIntegrationPage() {
</Select>
</FormControl>
<FormControl label="Railway Project">
<Select
value={targetAppId}
onValueChange={(val) => setTargetAppId(val)}
className='w-full border border-mineshaft-500'
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.appId as string} key={`target-app-${integrationAuthApp.appId as string}`}>
{integrationAuthApp.name}
<Select
value={targetAppId}
onValueChange={(val) => setTargetAppId(val)}
className='w-full border border-mineshaft-500'
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.appId as string} key={`target-app-${integrationAuthApp.appId as string}`}>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
)}
</Select>
)}
</Select>
</FormControl>
<FormControl label="Railway Project Environment">
<Select
value={targetEnvironmentId}
onValueChange={(val) => setTargetEnvironmentId(val)}
className='w-full border border-mineshaft-500'
>
{targetEnvironments.length > 0 ? (
targetEnvironments.map((targetEnvironment) => (
<SelectItem value={targetEnvironment.environmentId as string} key={`target-environment-${targetEnvironment.environmentId as string}`}>
{targetEnvironment.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-environment-none">
No environments found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}

View File

@ -38,7 +38,6 @@ export default function RenderCreateIntegrationPage() {
}, [workspace]);
useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name);
@ -61,6 +60,7 @@ export default function RenderCreateIntegrationPage() {
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
owner: null,
path: null,
region: null

View File

@ -60,6 +60,7 @@ export default function TravisCICreateIntegrationPage() {
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
owner: null,
path: null,
region: null,

View File

@ -87,6 +87,7 @@ export default function VercelCreateIntegrationPage() {
appId: targetApp.appId,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
targetEnvironmentId: null,
owner: null,
path,
region: null