Add support for service variables to Railway integration, add docs for Railway

This commit is contained in:
Tuan Dang
2023-04-09 15:00:40 +03:00
parent f6e23127ac
commit 710364e3a1
35 changed files with 307 additions and 16 deletions

View File

@@ -249,7 +249,7 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon
}
/**
* Return list of available Railway environments for Railway project with
* Return list of Railway environments for Railway project with
* id [appId]
* @param req
* @param res
@@ -315,6 +315,84 @@ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: R
});
}
/**
* Return list of Railway services for Railway project with id
* [appId]
* @param req
* @param res
*/
export const getIntegrationAuthRailwayServices = async (req: Request, res: Response) => {
const { integrationAuthId } = req.params;
const appId = req.query.appId as string;
interface RailwayService {
node: {
id: string;
name: string;
}
}
interface Service {
name: string;
serviceId: string;
}
let services: Service[] = [];
const query = `
query project($id: String!) {
project(id: $id) {
createdAt
deletedAt
id
description
expiredAt
isPublic
isTempProject
isUpdatable
name
prDeploys
teamId
updatedAt
upstreamUrl
services {
edges {
node {
id
name
}
}
}
}
}
`;
if (appId && appId !== '') {
const variables = {
id: appId
}
const { data: { data: { project: { services: { edges } } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables
}, {
headers: {
'Authorization': `Bearer ${req.accessToken}`,
'Content-Type': 'application/json',
},
});
services = edges.map((e: RailwayService) => ({
name: e.node.name,
serviceId: e.node.id
}));
}
return res.status(200).send({
services
});
}
/**
* Delete integration authorization with id [integrationAuthId]
* @param req

View File

@@ -25,6 +25,8 @@ export const createIntegration = async (req: Request, res: Response) => {
sourceEnvironment,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region
@@ -41,6 +43,8 @@ export const createIntegration = async (req: Request, res: Response) => {
appId,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region,

View File

@@ -355,10 +355,7 @@ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
}
`;
const variables = {
// userId: '<USER_ID_GOES_HERE>', // Replace with the desired user ID or remove if not needed
// teamId: '<TEAM_ID_GOES_HERE>', // Replace with the desired team ID or remove if not needed
};
const variables = {};
const { data: { data: { projects: { edges }}} } = await request.post(INTEGRATION_RAILWAY_API_URL, {
query,
@@ -367,6 +364,7 @@ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Accept-Encoding': 'application/json'
},
});

View File

@@ -1179,6 +1179,7 @@ const syncSecretsRailway = async ({
accessToken: string;
}) => {
try {
const query = `
mutation UpsertVariables($input: VariableCollectionUpsertInput!) {
variableCollectionUpsert(input: $input)
@@ -1188,6 +1189,7 @@ const syncSecretsRailway = async ({
const input = {
projectId: integration.appId,
environmentId: integration.targetEnvironmentId,
...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}),
replace: true,
variables: secrets
};
@@ -1201,6 +1203,7 @@ const syncSecretsRailway = async ({
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Accept-Encoding': 'application/json'
},
});

View File

@@ -21,10 +21,12 @@ export interface IIntegration {
environment: string;
isActive: boolean;
app: string;
appId: string;
owner: string;
targetEnvironment: string;
targetEnvironmentId: string;
appId: string;
targetService: string;
targetServiceId: string;
path: string;
region: string;
integration:
@@ -78,6 +80,16 @@ const integrationSchema = new Schema<IIntegration>(
type: String,
default: null
},
targetService: {
// railway-specific service
type: String,
default: null
},
targetServiceId: {
// railway-specific service
type: String,
default: null
},
owner: {
// github-specific repo owner-login
type: String,

View File

@@ -31,6 +31,8 @@ router.post( // new: add new integration for integration auth
body('sourceEnvironment').trim(),
body('targetEnvironment').trim(),
body('targetEnvironmentId').trim(),
body('targetService').trim(),
body('targetServiceId').trim(),
body('owner').trim(),
body('path').trim(),
body('region').trim(),

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,54 @@
---
title: "Railway"
description: "How to automatically sync secrets from Infisical into your Railway projects and services"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png)
## Enter your Railway API Token
Obtain a Railway API Token in your Railway [Account Settings > Tokens](https://railway.app/account/tokens).
![integrations railway dashboard](../../images/integrations-railway-dashboard.png)
![integrations railway token](../../images/integrations-railway-token.png)
<Note>
If this is your first time creating a Railway API token, then you'll be prompted to join
Railway's Private Boarding Beta program on the Railway Account Settings > Tokens page.
Note that Railway project tokens will not work for this integration since they don't work with
Railway's Public API.
</Note>
Press on the Railway tile and input your Railway API Key to grant Infisical access to your Railway account.
![integrations railway authorization](../../images/integrations-railway-authorization.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>
## Start integration
Select which Infisical environment secrets you want to sync to which Railway project and environment (and optionally service). Lastly, press create integration to start syncing secrets to Railway.
![integrations create railway](../../images/integrations-railway-create.png)
<Note>
Infisical integrates with both Railway's [shared variables](https://blog.railway.app/p/shared-variables-release) at the project environment level as well as service variables at the service level.
To sync secrets to a specific service in a project, you can select a service from the Railway Service dropdown; otherwise, leaving it empty will sync secrets to the shared variables of that project.
</Note>
![integrations railway](../../images/integrations-railway.png)

View File

@@ -18,6 +18,7 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
| [Vercel](/integrations/cloud/vercel) | Cloud | Available |
| [Netlify](/integrations/cloud/netlify) | Cloud | Available |
| [Render](/integrations/cloud/render) | Cloud | Available |
| [Railway](/integrations/cloud/railway) | Cloud | Available |
| [Fly.io](/integrations/cloud/flyio) | Cloud | Available |
| [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available |
| [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available |

View File

@@ -149,6 +149,7 @@
"integrations/cloud/vercel",
"integrations/cloud/netlify",
"integrations/cloud/render",
"integrations/cloud/railway",
"integrations/cloud/flyio",
"integrations/cloud/azure-key-vault",
"integrations/cicd/githubactions",

View File

@@ -12,6 +12,7 @@ const integrationSlugNameMapping: Mapping = {
'github': 'GitHub',
'gitlab': 'GitLab',
'render': 'Render',
'railway': 'Railway',
'flyio': 'Fly.io',
'circleci': 'CircleCI',
'travisci': 'TravisCI'

View File

@@ -45,6 +45,7 @@ type Props = {
handleDeleteIntegration: (args: { integration: Integration }) => void;
};
// TODO: refactor
const IntegrationTile = ({
integration,
integrations,
@@ -55,7 +56,6 @@ const IntegrationTile = ({
handleDeleteIntegration
}: Props) => {
// set initial environment. This find will only execute when component is mounting
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
environments.find(({ slug }) => slug === integration?.environment) || {
name: '',
@@ -176,6 +176,21 @@ const IntegrationTile = ({
/>
</div>
);
case 'railway':
return (
<div>
<div className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</div>
<ListBox
data={
!integration.isActive
? ['Production', 'Deploy previews', 'Branch deploys', 'Local development']
: null
}
isSelected={integration.targetEnvironment}
onChange={setIntegrationTargetEnvironment}
/>
</div>
);
default:
return <div />;
}

View File

@@ -1,7 +1,7 @@
export {
useGetIntegrationAuthApps,
useGetIntegrationAuthById,
useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices,
useGetIntegrationAuthTeams,
useGetIntegrationAuthVercelBranches,
useGetRailwayEnvironments
} from './queries';
useGetIntegrationAuthVercelBranches} from './queries';

View File

@@ -6,6 +6,7 @@ import {
App,
Environment,
IntegrationAuth,
Service,
Team
} from './types';
@@ -27,6 +28,13 @@ const integrationAuthKeys = {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, 'integrationAuthRailwayEnvironments'] as const,
getIntegrationAuthRailwayServices: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, 'integrationAuthRailwayServices'] as const
}
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
@@ -86,6 +94,22 @@ const fetchIntegrationAuthRailwayEnvironments = async ({
return environments;
}
const fetchIntegrationAuthRailwayServices = async ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => {
const { data: { services } } = await apiRequest.get<{ services: Service[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/services`, {
params: {
appId
}
});
return services;
}
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
@@ -139,7 +163,7 @@ export const useGetIntegrationAuthVercelBranches = ({
});
}
export const useGetRailwayEnvironments = ({
export const useGetIntegrationAuthRailwayEnvironments = ({
integrationAuthId,
appId
}: {
@@ -158,3 +182,23 @@ export const useGetRailwayEnvironments = ({
enabled: true
});
}
export const useGetIntegrationAuthRailwayServices = ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthRailwayServices({
integrationAuthId,
appId,
}),
queryFn: () => fetchIntegrationAuthRailwayServices({
integrationAuthId,
appId,
}),
enabled: true
});
}

View File

@@ -21,3 +21,8 @@ export type Environment = {
name: string;
environmentId: string;
}
export type Service = {
name: string;
serviceId: string;
}

View File

@@ -8,6 +8,8 @@ interface Props {
sourceEnvironment: string;
targetEnvironment: string | null;
targetEnvironmentId: string | null;
targetService: string | null;
targetServiceId: string | null;
owner: string | null;
path: string | null;
region: string | null;
@@ -26,6 +28,8 @@ const createIntegration = ({
sourceEnvironment,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region
@@ -43,6 +47,8 @@ const createIntegration = ({
sourceEnvironment,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region

View File

@@ -99,6 +99,8 @@ export default function AWSParameterStoreCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path,
region: selectedAWSRegion

View File

@@ -98,6 +98,8 @@ export default function AWSSecretManagerCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: selectedAWSRegion

View File

@@ -63,6 +63,8 @@ export default function AzureKeyVaultCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null

View File

@@ -61,6 +61,8 @@ export default function CircleCICreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null,

View File

@@ -62,6 +62,8 @@ export default function FlyioCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null

View File

@@ -65,6 +65,8 @@ export default function GitHubCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: targetApp.owner,
path: null,
region: null

View File

@@ -90,6 +90,8 @@ export default function GitLabCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null

View File

@@ -61,6 +61,8 @@ export default function HerokuCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null

View File

@@ -70,6 +70,8 @@ export default function NetlifyCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null

View File

@@ -14,7 +14,8 @@ import {
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById,
useGetRailwayEnvironments
useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices
} from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
@@ -24,6 +25,8 @@ export default function RailwayCreateIntegrationPage() {
const [targetAppId, setTargetAppId] = useState('');
const [targetEnvironmentId, setTargetEnvironmentId] = useState('');
const [targetServiceId, setTargetServiceId] = useState('');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [isLoading, setIsLoading] = useState(false);
@@ -33,8 +36,11 @@ export default function RailwayCreateIntegrationPage() {
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: integrationAuthId as string ?? ''
});
const { data: targetEnvironments } = useGetRailwayEnvironments({
const { data: targetEnvironments } = useGetIntegrationAuthRailwayEnvironments({
integrationAuthId: integrationAuthId as string ?? '',
appId: targetAppId
});
const { data: targetServices } = useGetIntegrationAuthRailwayServices({
integrationAuthId: integrationAuthId as string ?? '',
appId: targetAppId
});
@@ -65,6 +71,12 @@ export default function RailwayCreateIntegrationPage() {
}
}, [targetEnvironments]);
const filteredServices = targetServices
?.concat({
name: '',
serviceId: ''
});
const handleButtonClick = async () => {
try {
setIsLoading(true);
@@ -76,6 +88,8 @@ export default function RailwayCreateIntegrationPage() {
if (!targetApp || !targetApp.appId || !targetEnvironment) return;
const targetService = targetServices?.find((service) => service.serviceId === targetServiceId);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
@@ -84,6 +98,8 @@ export default function RailwayCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: targetEnvironment.name,
targetEnvironmentId: targetEnvironment.environmentId,
targetService: targetService ? targetService.name : null,
targetServiceId: targetService ? targetService.serviceId : null,
owner: null,
path: null,
region: null
@@ -99,7 +115,7 @@ export default function RailwayCreateIntegrationPage() {
}
}
return workspace && selectedSourceEnvironment && integrationAuthApps && targetEnvironments ? (
return workspace && selectedSourceEnvironment && integrationAuthApps && targetEnvironments && filteredServices ? (
<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>
@@ -158,6 +174,19 @@ export default function RailwayCreateIntegrationPage() {
)}
</Select>
</FormControl>
<FormControl label="Railway Service (Optional)">
<Select
value={targetServiceId}
onValueChange={(val) => setTargetServiceId(val)}
className='w-full border border-mineshaft-500'
>
{filteredServices.map((targetService) => (
<SelectItem value={targetService.serviceId as string} key={`target-service-${targetService.serviceId as string}`}>
{targetService.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"

View File

@@ -61,6 +61,8 @@ export default function RenderCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null

View File

@@ -61,6 +61,8 @@ export default function TravisCICreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null,

View File

@@ -88,6 +88,8 @@ export default function VercelCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path,
region: null