mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 16:26:16 +00:00
Merge pull request #4548 from ThallesP/main
fix: use upsert collection instead of upserting each variable on Railway
This commit is contained in:
@@ -75,7 +75,7 @@ class RailwayPublicClient {
|
|||||||
async send<T extends TRailwayResponse>(
|
async send<T extends TRailwayResponse>(
|
||||||
query: string,
|
query: string,
|
||||||
options: RailwaySendReqOptions,
|
options: RailwaySendReqOptions,
|
||||||
variables: Record<string, string | Record<string, string>> = {},
|
variables: Record<string, unknown> = {},
|
||||||
retryAttempt: number = 0
|
retryAttempt: number = 0
|
||||||
): Promise<T["data"] | undefined> {
|
): Promise<T["data"] | undefined> {
|
||||||
const body = {
|
const body = {
|
||||||
@@ -117,6 +117,25 @@ class RailwayPublicClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDeployments(
|
||||||
|
config: RailwaySendReqOptions,
|
||||||
|
variables: { input: { serviceId: string; environmentId: string }; first?: number }
|
||||||
|
) {
|
||||||
|
return this.send<TRailwayResponse<{ deployments: { edges: { node: { id: string } }[] } }>>(
|
||||||
|
`query deployments($input: DeploymentListInput!, $first: Int) { deployments(first: $first, input: $input) { edges { node { id } } } }`,
|
||||||
|
config,
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async redeployDeployment(config: RailwaySendReqOptions, variables: { input: { deploymentId: string } }) {
|
||||||
|
return this.send<TRailwayResponse<{ deploymentRedeploy: { id: string } }>>(
|
||||||
|
`mutation deploymentRedeploy($deploymentId: String!) { deploymentRedeploy(id: $deploymentId) { id } }`,
|
||||||
|
config,
|
||||||
|
{ deploymentId: variables.input.deploymentId }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getSubscriptionType(config: RailwaySendReqOptions & { projectId: string }) {
|
async getSubscriptionType(config: RailwaySendReqOptions & { projectId: string }) {
|
||||||
const res = await this.send(
|
const res = await this.send(
|
||||||
`query project($projectId: String!) { project(id: $projectId) { subscriptionType }}`,
|
`query project($projectId: String!) { project(id: $projectId) { subscriptionType }}`,
|
||||||
@@ -213,7 +232,9 @@ class RailwayPublicClient {
|
|||||||
|
|
||||||
async deleteVariable(
|
async deleteVariable(
|
||||||
config: RailwaySendReqOptions,
|
config: RailwaySendReqOptions,
|
||||||
variables: { input: { projectId: string; environmentId: string; name: string; serviceId?: string } }
|
variables: {
|
||||||
|
input: { projectId: string; environmentId: string; name: string; skipDeploys?: boolean; serviceId?: string };
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
await this.send<TRailwayResponse<{ variables: Record<string, string> }>>(
|
await this.send<TRailwayResponse<{ variables: Record<string, string> }>>(
|
||||||
`mutation variableDelete($input: VariableDeleteInput!) { variableDelete(input: $input) }`,
|
`mutation variableDelete($input: VariableDeleteInput!) { variableDelete(input: $input) }`,
|
||||||
@@ -222,6 +243,26 @@ class RailwayPublicClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async upsertCollection(
|
||||||
|
config: RailwaySendReqOptions,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
projectId: string;
|
||||||
|
environmentId: string;
|
||||||
|
variables: Record<string, string>;
|
||||||
|
skipDeploys?: boolean;
|
||||||
|
serviceId?: string;
|
||||||
|
replace?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return this.send<TRailwayResponse<boolean>>(
|
||||||
|
`mutation variableCollectionUpsert($input: VariableCollectionUpsertInput!) { variableCollectionUpsert(input: $input) }`,
|
||||||
|
config,
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async upsertVariable(
|
async upsertVariable(
|
||||||
config: RailwaySendReqOptions,
|
config: RailwaySendReqOptions,
|
||||||
variables: { input: { projectId: string; environmentId: string; name: string; value: string; serviceId?: string } }
|
variables: { input: { projectId: string; environmentId: string; name: string; value: string; serviceId?: string } }
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export const RailwaySyncFns = {
|
|||||||
async getSecrets(secretSync: TRailwaySyncWithCredentials): Promise<TSecretMap> {
|
async getSecrets(secretSync: TRailwaySyncWithCredentials): Promise<TSecretMap> {
|
||||||
try {
|
try {
|
||||||
const config = secretSync.destinationConfig;
|
const config = secretSync.destinationConfig;
|
||||||
|
const { keySchema } = secretSync.syncOptions;
|
||||||
|
const { environment } = secretSync;
|
||||||
|
|
||||||
const variables = await RailwayPublicAPI.getVariables(secretSync.connection, {
|
const variables = await RailwayPublicAPI.getVariables(secretSync.connection, {
|
||||||
projectId: config.projectId,
|
projectId: config.projectId,
|
||||||
@@ -26,6 +28,10 @@ export const RailwaySyncFns = {
|
|||||||
// eslint-disable-next-line no-continue
|
// eslint-disable-next-line no-continue
|
||||||
if (key.startsWith("RAILWAY_")) continue;
|
if (key.startsWith("RAILWAY_")) continue;
|
||||||
|
|
||||||
|
// Check if key matches the schema
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
||||||
|
|
||||||
entries[key] = {
|
entries[key] = {
|
||||||
value
|
value
|
||||||
};
|
};
|
||||||
@@ -40,60 +46,73 @@ export const RailwaySyncFns = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs secrets to Railway and redeploys the service if needed.
|
||||||
|
*
|
||||||
|
* Gets existing Railway vars, merges with new secrets (keeping Railway vars if deletion is disabled),
|
||||||
|
* then replaces every variable with the new values, if variable is not in the secretMap, it is deleted.
|
||||||
|
* If there's a service, triggers a redeploy to pick up the changes.
|
||||||
|
*/
|
||||||
async syncSecrets(secretSync: TRailwaySyncWithCredentials, secretMap: TSecretMap) {
|
async syncSecrets(secretSync: TRailwaySyncWithCredentials, secretMap: TSecretMap) {
|
||||||
const {
|
try {
|
||||||
environment,
|
const {
|
||||||
syncOptions: { disableSecretDeletion, keySchema }
|
syncOptions: { disableSecretDeletion }
|
||||||
} = secretSync;
|
} = secretSync;
|
||||||
const railwaySecrets = await this.getSecrets(secretSync);
|
const railwaySecrets = await this.getSecrets(secretSync);
|
||||||
const config = secretSync.destinationConfig;
|
const config = secretSync.destinationConfig;
|
||||||
|
|
||||||
for await (const key of Object.keys(secretMap)) {
|
const railwaySecretsMap = Object.fromEntries(
|
||||||
try {
|
Object.entries(railwaySecrets).map(([key, secret]) => [key, secret.value])
|
||||||
const existing = railwaySecrets[key];
|
);
|
||||||
|
const secretMapMap = Object.fromEntries(Object.entries(secretMap).map(([key, secret]) => [key, secret.value]));
|
||||||
|
|
||||||
if (existing === undefined || existing.value !== secretMap[key].value) {
|
const toReplace = disableSecretDeletion ? { ...railwaySecretsMap, ...secretMapMap } : secretMapMap;
|
||||||
await RailwayPublicAPI.upsertVariable(secretSync.connection, {
|
|
||||||
input: {
|
const upserted = await RailwayPublicAPI.upsertCollection(secretSync.connection, {
|
||||||
projectId: config.projectId,
|
input: {
|
||||||
environmentId: config.environmentId,
|
projectId: config.projectId,
|
||||||
serviceId: config.serviceId || undefined,
|
environmentId: config.environmentId,
|
||||||
name: key,
|
serviceId: config.serviceId || undefined,
|
||||||
value: secretMap[key].value ?? ""
|
skipDeploys: true,
|
||||||
}
|
variables: toReplace,
|
||||||
});
|
replace: true
|
||||||
}
|
}
|
||||||
} catch (error) {
|
});
|
||||||
|
|
||||||
|
if (!upserted)
|
||||||
throw new SecretSyncError({
|
throw new SecretSyncError({
|
||||||
error,
|
message: "Failed to upsert secrets to Railway"
|
||||||
secretKey: key
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disableSecretDeletion) return;
|
if (!config.serviceId) return;
|
||||||
|
|
||||||
for await (const key of Object.keys(railwaySecrets)) {
|
const latestDeployment = await RailwayPublicAPI.getDeployments(secretSync.connection, {
|
||||||
try {
|
input: {
|
||||||
// eslint-disable-next-line no-continue
|
serviceId: config.serviceId,
|
||||||
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
environmentId: config.environmentId
|
||||||
|
},
|
||||||
|
first: 1
|
||||||
|
});
|
||||||
|
|
||||||
if (!secretMap[key]) {
|
const latestDeploymentId = latestDeployment?.deployments.edges[0].node.id;
|
||||||
await RailwayPublicAPI.deleteVariable(secretSync.connection, {
|
|
||||||
input: {
|
if (!latestDeploymentId)
|
||||||
projectId: config.projectId,
|
throw new SecretSyncError({
|
||||||
environmentId: config.environmentId,
|
message: "Failed to get latest deployment from Railway"
|
||||||
serviceId: config.serviceId || undefined,
|
});
|
||||||
name: key
|
|
||||||
}
|
await RailwayPublicAPI.redeployDeployment(secretSync.connection, {
|
||||||
});
|
input: {
|
||||||
|
deploymentId: latestDeploymentId
|
||||||
}
|
}
|
||||||
} catch (error) {
|
});
|
||||||
throw new SecretSyncError({
|
} catch (error) {
|
||||||
error,
|
if (error instanceof SecretSyncError) throw error;
|
||||||
secretKey: key
|
|
||||||
});
|
throw new SecretSyncError({
|
||||||
}
|
error,
|
||||||
|
message: "Failed to sync secrets to Railway"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -101,24 +120,37 @@ export const RailwaySyncFns = {
|
|||||||
const existing = await this.getSecrets(secretSync);
|
const existing = await this.getSecrets(secretSync);
|
||||||
const config = secretSync.destinationConfig;
|
const config = secretSync.destinationConfig;
|
||||||
|
|
||||||
for await (const secret of Object.keys(existing)) {
|
// Create a new variables object excluding secrets that exist in secretMap
|
||||||
try {
|
const remainingVariables = Object.fromEntries(
|
||||||
if (secret in secretMap) {
|
Object.entries(existing)
|
||||||
await RailwayPublicAPI.deleteVariable(secretSync.connection, {
|
.filter(([key]) => !(key in secretMap))
|
||||||
input: {
|
.map(([key, secret]) => [key, secret.value])
|
||||||
projectId: config.projectId,
|
);
|
||||||
environmentId: config.environmentId,
|
|
||||||
serviceId: config.serviceId || undefined,
|
try {
|
||||||
name: secret
|
const upserted = await RailwayPublicAPI.upsertCollection(secretSync.connection, {
|
||||||
}
|
input: {
|
||||||
});
|
projectId: config.projectId,
|
||||||
|
environmentId: config.environmentId,
|
||||||
|
serviceId: config.serviceId || undefined,
|
||||||
|
skipDeploys: true,
|
||||||
|
variables: remainingVariables,
|
||||||
|
replace: true
|
||||||
}
|
}
|
||||||
} catch (error) {
|
});
|
||||||
|
|
||||||
|
if (!upserted) {
|
||||||
throw new SecretSyncError({
|
throw new SecretSyncError({
|
||||||
error,
|
message: "Failed to remove secrets from Railway"
|
||||||
secretKey: secret
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof SecretSyncError) throw error;
|
||||||
|
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
message: "Failed to remove secrets from Railway"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user