mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 20:27:12 +00:00
feat: added user status signs for replication failure etc
This commit is contained in:
+6
@@ -6,6 +6,9 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
if (await knex.schema.hasTable(TableName.SecretImport)) {
|
if (await knex.schema.hasTable(TableName.SecretImport)) {
|
||||||
await knex.schema.alterTable(TableName.SecretImport, (t) => {
|
await knex.schema.alterTable(TableName.SecretImport, (t) => {
|
||||||
t.boolean("isReplication").defaultTo(false);
|
t.boolean("isReplication").defaultTo(false);
|
||||||
|
t.boolean("isReplicationSuccess").nullable();
|
||||||
|
t.text("replicationStatus").nullable();
|
||||||
|
t.datetime("lastReplicated").nullable();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +41,9 @@ export async function down(knex: Knex): Promise<void> {
|
|||||||
if (await knex.schema.hasTable(TableName.SecretImport)) {
|
if (await knex.schema.hasTable(TableName.SecretImport)) {
|
||||||
await knex.schema.alterTable(TableName.SecretImport, (t) => {
|
await knex.schema.alterTable(TableName.SecretImport, (t) => {
|
||||||
t.dropColumns("isReplication");
|
t.dropColumns("isReplication");
|
||||||
|
t.dropColumns("isReplicationSuccess");
|
||||||
|
t.dropColumns("replicationStatus");
|
||||||
|
t.dropColumns("lastReplicated");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +16,10 @@ export const SecretImportsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
folderId: z.string().uuid(),
|
folderId: z.string().uuid(),
|
||||||
isReplication: z.boolean().default(false).nullable().optional()
|
isReplication: z.boolean().default(false).nullable().optional(),
|
||||||
|
isReplicationSuccess: z.boolean().nullable().optional(),
|
||||||
|
replicationStatus: z.string().nullable().optional(),
|
||||||
|
lastReplicated: z.date().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretImports = z.infer<typeof SecretImportsSchema>;
|
export type TSecretImports = z.infer<typeof SecretImportsSchema>;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type TSecretReplicationServiceFactoryDep = {
|
|||||||
"find" | "findByBlindIndexes" | "insertMany" | "bulkUpdate" | "delete" | "upsertSecretReferences"
|
"find" | "findByBlindIndexes" | "insertMany" | "bulkUpdate" | "delete" | "upsertSecretReferences"
|
||||||
>;
|
>;
|
||||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "insertMany" | "update" | "findLatestVersionMany">;
|
secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "insertMany" | "update" | "findLatestVersionMany">;
|
||||||
secretImportDAL: Pick<TSecretImportDALFactory, "find">;
|
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "updateById">;
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "findSecretPathByFolderIds" | "findBySecretPath">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findSecretPathByFolderIds" | "findBySecretPath">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "find" | "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "find" | "insertMany">;
|
||||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets">;
|
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets">;
|
||||||
@@ -115,131 +115,92 @@ export const secretReplicationServiceFactory = ({
|
|||||||
try {
|
try {
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
for (const secretImport of secretImports) {
|
for (const secretImport of secretImports) {
|
||||||
const hasJobCompleted = await keyStore.getItem(
|
try {
|
||||||
keystoreReplicationSuccessKey(job.id as string, secretImport.id),
|
const hasJobCompleted = await keyStore.getItem(
|
||||||
KeyStorePrefixes.SecretReplication
|
keystoreReplicationSuccessKey(job.id as string, secretImport.id),
|
||||||
);
|
KeyStorePrefixes.SecretReplication
|
||||||
if (hasJobCompleted) {
|
|
||||||
logger.info(
|
|
||||||
{ jobId: job.id, importId: secretImport.id },
|
|
||||||
"Skipping this job as this has been successfully replicated."
|
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line
|
if (hasJobCompleted) {
|
||||||
continue;
|
logger.info(
|
||||||
}
|
{ jobId: job.id, importId: secretImport.id },
|
||||||
|
"Skipping this job as this has been successfully replicated."
|
||||||
const [importedFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [secretImport.folderId]);
|
);
|
||||||
if (!importedFolder) throw new BadRequestError({ message: "Imported folder not found" });
|
// eslint-disable-next-line
|
||||||
const importFolderId = importedFolder.id;
|
continue;
|
||||||
|
|
||||||
const localSecrets = await secretDAL.find({
|
|
||||||
$in: { secretBlindIndex: replicatedSecrets.map(({ secretBlindIndex }) => secretBlindIndex) },
|
|
||||||
folderId: importFolderId
|
|
||||||
});
|
|
||||||
const localSecretsGroupedByBlindIndex = groupBy(localSecrets, (i) => i.secretBlindIndex as string);
|
|
||||||
|
|
||||||
const locallyCreatedSecrets = sanitizedSecrets.filter(({ operation, id }) => {
|
|
||||||
return (
|
|
||||||
(operation === SecretOperations.Create || operation === SecretOperations.Update) &&
|
|
||||||
!localSecretsGroupedByBlindIndex[replicatedSecretsGroupBySecretId[id][0].secretBlindIndex as string]?.[0]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const locallyUpdatedSecrets = sanitizedSecrets.filter(
|
|
||||||
({ operation, id }) =>
|
|
||||||
(operation === SecretOperations.Create || operation === SecretOperations.Update) &&
|
|
||||||
localSecretsGroupedByBlindIndex[replicatedSecretsGroupBySecretId[id][0].secretBlindIndex as string]?.[0]
|
|
||||||
);
|
|
||||||
|
|
||||||
const locallyDeletedSecrets = sanitizedSecrets.filter(
|
|
||||||
({ operation, id }) =>
|
|
||||||
operation === SecretOperations.Delete &&
|
|
||||||
Boolean(replicatedSecretsGroupBySecretId[id]?.[0]?.secretBlindIndex) &&
|
|
||||||
localSecretsGroupedByBlindIndex[replicatedSecretsGroupBySecretId[id][0].secretBlindIndex as string]?.[0]
|
|
||||||
);
|
|
||||||
|
|
||||||
const policy = await secretApprovalPolicyService.getSecretApprovalPolicy(
|
|
||||||
projectId,
|
|
||||||
importedFolder.environmentSlug,
|
|
||||||
importedFolder.path
|
|
||||||
);
|
|
||||||
// this means it should be a approval request rather than direct replication
|
|
||||||
if (policy && actor === ActorType.USER) {
|
|
||||||
const membership = await projectMembershipDAL.findOne({ projectId, userId: actorId });
|
|
||||||
if (!membership) {
|
|
||||||
logger.error("Project membership not found in %s for user %s", projectId, actorId);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const localSecretsLatestVersions = localSecrets.map(({ id }) => id);
|
const [importedFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [secretImport.folderId]);
|
||||||
const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(
|
if (!importedFolder) throw new BadRequestError({ message: "Imported folder not found" });
|
||||||
importFolderId,
|
const importFolderId = importedFolder.id;
|
||||||
localSecretsLatestVersions
|
|
||||||
);
|
|
||||||
await secretApprovalRequestDAL.transaction(async (tx) => {
|
|
||||||
const approvalRequestDoc = await secretApprovalRequestDAL.create(
|
|
||||||
{
|
|
||||||
folderId: importFolderId,
|
|
||||||
slug: alphaNumericNanoId(),
|
|
||||||
policyId: policy.id,
|
|
||||||
status: "open",
|
|
||||||
hasMerged: false,
|
|
||||||
committerId: membership.id,
|
|
||||||
isReplicated: true
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
const commits = locallyCreatedSecrets
|
|
||||||
.concat(locallyUpdatedSecrets)
|
|
||||||
.concat(locallyDeletedSecrets)
|
|
||||||
.map(({ id, operation }) => {
|
|
||||||
const doc = replicatedSecretsGroupBySecretId[id][0];
|
|
||||||
const localSecret = localSecretsGroupedByBlindIndex[doc.secretBlindIndex as string]?.[0];
|
|
||||||
return {
|
|
||||||
op: operation,
|
|
||||||
keyEncoding: doc.keyEncoding,
|
|
||||||
algorithm: doc.algorithm,
|
|
||||||
requestId: approvalRequestDoc.id,
|
|
||||||
metadata: doc.metadata,
|
|
||||||
secretKeyIV: doc.secretKeyIV,
|
|
||||||
secretKeyTag: doc.secretKeyTag,
|
|
||||||
secretKeyCiphertext: doc.secretKeyCiphertext,
|
|
||||||
secretValueIV: doc.secretValueIV,
|
|
||||||
secretValueTag: doc.secretValueTag,
|
|
||||||
secretValueCiphertext: doc.secretValueCiphertext,
|
|
||||||
secretBlindIndex: doc.secretBlindIndex,
|
|
||||||
secretCommentIV: doc.secretCommentIV,
|
|
||||||
secretCommentTag: doc.secretCommentTag,
|
|
||||||
secretCommentCiphertext: doc.secretCommentCiphertext,
|
|
||||||
isReplicated: true,
|
|
||||||
skipMultilineEncoding: doc.skipMultilineEncoding,
|
|
||||||
// except create operation other two needs the secret id and version id
|
|
||||||
...(operation !== SecretOperations.Create
|
|
||||||
? { secretId: localSecret.id, secretVersion: latestSecretVersions[localSecret.id].id }
|
|
||||||
: {})
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const approvalCommits = await secretApprovalRequestSecretDAL.insertMany(commits, tx);
|
|
||||||
|
|
||||||
return { ...approvalRequestDoc, commits: approvalCommits };
|
const localSecrets = await secretDAL.find({
|
||||||
|
$in: { secretBlindIndex: replicatedSecrets.map(({ secretBlindIndex }) => secretBlindIndex) },
|
||||||
|
folderId: importFolderId
|
||||||
});
|
});
|
||||||
} else {
|
const localSecretsGroupedByBlindIndex = groupBy(localSecrets, (i) => i.secretBlindIndex as string);
|
||||||
let nestedImportSecrets: TSyncSecretsDTO["secrets"] = [];
|
|
||||||
await secretReplicationDAL.transaction(async (tx) => {
|
const locallyCreatedSecrets = sanitizedSecrets.filter(({ operation, id }) => {
|
||||||
if (locallyCreatedSecrets.length) {
|
return (
|
||||||
const newSecrets = await fnSecretBulkInsert({
|
(operation === SecretOperations.Create || operation === SecretOperations.Update) &&
|
||||||
folderId: importFolderId,
|
!localSecretsGroupedByBlindIndex[replicatedSecretsGroupBySecretId[id][0].secretBlindIndex as string]?.[0]
|
||||||
secretVersionDAL,
|
);
|
||||||
secretDAL,
|
});
|
||||||
tx,
|
|
||||||
secretTagDAL,
|
const locallyUpdatedSecrets = sanitizedSecrets.filter(
|
||||||
secretVersionTagDAL,
|
({ operation, id }) =>
|
||||||
inputSecrets: locallyCreatedSecrets.map(({ id }) => {
|
(operation === SecretOperations.Create || operation === SecretOperations.Update) &&
|
||||||
|
localSecretsGroupedByBlindIndex[replicatedSecretsGroupBySecretId[id][0].secretBlindIndex as string]?.[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
const locallyDeletedSecrets = sanitizedSecrets.filter(
|
||||||
|
({ operation, id }) =>
|
||||||
|
operation === SecretOperations.Delete &&
|
||||||
|
Boolean(replicatedSecretsGroupBySecretId[id]?.[0]?.secretBlindIndex) &&
|
||||||
|
localSecretsGroupedByBlindIndex[replicatedSecretsGroupBySecretId[id][0].secretBlindIndex as string]?.[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
const policy = await secretApprovalPolicyService.getSecretApprovalPolicy(
|
||||||
|
projectId,
|
||||||
|
importedFolder.environmentSlug,
|
||||||
|
importedFolder.path
|
||||||
|
);
|
||||||
|
// this means it should be a approval request rather than direct replication
|
||||||
|
if (policy && actor === ActorType.USER) {
|
||||||
|
const membership = await projectMembershipDAL.findOne({ projectId, userId: actorId });
|
||||||
|
if (!membership) {
|
||||||
|
logger.error("Project membership not found in %s for user %s", projectId, actorId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localSecretsLatestVersions = localSecrets.map(({ id }) => id);
|
||||||
|
const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(
|
||||||
|
importFolderId,
|
||||||
|
localSecretsLatestVersions
|
||||||
|
);
|
||||||
|
await secretApprovalRequestDAL.transaction(async (tx) => {
|
||||||
|
const approvalRequestDoc = await secretApprovalRequestDAL.create(
|
||||||
|
{
|
||||||
|
folderId: importFolderId,
|
||||||
|
slug: alphaNumericNanoId(),
|
||||||
|
policyId: policy.id,
|
||||||
|
status: "open",
|
||||||
|
hasMerged: false,
|
||||||
|
committerId: membership.id,
|
||||||
|
isReplicated: true
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
const commits = locallyCreatedSecrets
|
||||||
|
.concat(locallyUpdatedSecrets)
|
||||||
|
.concat(locallyDeletedSecrets)
|
||||||
|
.map(({ id, operation }) => {
|
||||||
const doc = replicatedSecretsGroupBySecretId[id][0];
|
const doc = replicatedSecretsGroupBySecretId[id][0];
|
||||||
|
const localSecret = localSecretsGroupedByBlindIndex[doc.secretBlindIndex as string]?.[0];
|
||||||
return {
|
return {
|
||||||
|
op: operation,
|
||||||
keyEncoding: doc.keyEncoding,
|
keyEncoding: doc.keyEncoding,
|
||||||
algorithm: doc.algorithm,
|
algorithm: doc.algorithm,
|
||||||
type: doc.type,
|
requestId: approvalRequestDoc.id,
|
||||||
metadata: doc.metadata,
|
metadata: doc.metadata,
|
||||||
secretKeyIV: doc.secretKeyIV,
|
secretKeyIV: doc.secretKeyIV,
|
||||||
secretKeyTag: doc.secretKeyTag,
|
secretKeyTag: doc.secretKeyTag,
|
||||||
@@ -252,31 +213,31 @@ export const secretReplicationServiceFactory = ({
|
|||||||
secretCommentTag: doc.secretCommentTag,
|
secretCommentTag: doc.secretCommentTag,
|
||||||
secretCommentCiphertext: doc.secretCommentCiphertext,
|
secretCommentCiphertext: doc.secretCommentCiphertext,
|
||||||
isReplicated: true,
|
isReplicated: true,
|
||||||
skipMultilineEncoding: doc.skipMultilineEncoding
|
skipMultilineEncoding: doc.skipMultilineEncoding,
|
||||||
|
// except create operation other two needs the secret id and version id
|
||||||
|
...(operation !== SecretOperations.Create
|
||||||
|
? { secretId: localSecret.id, secretVersion: latestSecretVersions[localSecret.id].id }
|
||||||
|
: {})
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
});
|
const approvalCommits = await secretApprovalRequestSecretDAL.insertMany(commits, tx);
|
||||||
nestedImportSecrets = nestedImportSecrets.concat(
|
|
||||||
newSecrets.map(({ id, version }) => ({ operation: SecretOperations.Create, version, id }))
|
return { ...approvalRequestDoc, commits: approvalCommits };
|
||||||
);
|
});
|
||||||
}
|
} else {
|
||||||
if (locallyUpdatedSecrets.length) {
|
let nestedImportSecrets: TSyncSecretsDTO["secrets"] = [];
|
||||||
const newSecrets = await fnSecretBulkUpdate({
|
await secretReplicationDAL.transaction(async (tx) => {
|
||||||
projectId,
|
if (locallyCreatedSecrets.length) {
|
||||||
folderId: importFolderId,
|
const newSecrets = await fnSecretBulkInsert({
|
||||||
secretVersionDAL,
|
folderId: importFolderId,
|
||||||
secretDAL,
|
secretVersionDAL,
|
||||||
tx,
|
secretDAL,
|
||||||
secretTagDAL,
|
tx,
|
||||||
secretVersionTagDAL,
|
secretTagDAL,
|
||||||
inputSecrets: locallyUpdatedSecrets.map(({ id }) => {
|
secretVersionTagDAL,
|
||||||
const doc = replicatedSecretsGroupBySecretId[id][0];
|
inputSecrets: locallyCreatedSecrets.map(({ id }) => {
|
||||||
return {
|
const doc = replicatedSecretsGroupBySecretId[id][0];
|
||||||
filter: {
|
return {
|
||||||
folderId: importFolderId,
|
|
||||||
id: localSecretsGroupedByBlindIndex[doc.secretBlindIndex as string][0].id
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
keyEncoding: doc.keyEncoding,
|
keyEncoding: doc.keyEncoding,
|
||||||
algorithm: doc.algorithm,
|
algorithm: doc.algorithm,
|
||||||
type: doc.type,
|
type: doc.type,
|
||||||
@@ -293,60 +254,119 @@ export const secretReplicationServiceFactory = ({
|
|||||||
secretCommentCiphertext: doc.secretCommentCiphertext,
|
secretCommentCiphertext: doc.secretCommentCiphertext,
|
||||||
isReplicated: true,
|
isReplicated: true,
|
||||||
skipMultilineEncoding: doc.skipMultilineEncoding
|
skipMultilineEncoding: doc.skipMultilineEncoding
|
||||||
}
|
};
|
||||||
};
|
})
|
||||||
})
|
});
|
||||||
});
|
nestedImportSecrets = nestedImportSecrets.concat(
|
||||||
nestedImportSecrets = nestedImportSecrets.concat(
|
newSecrets.map(({ id, version }) => ({ operation: SecretOperations.Create, version, id }))
|
||||||
newSecrets.map(({ id, version }) => ({ operation: SecretOperations.Update, version, id }))
|
);
|
||||||
);
|
}
|
||||||
}
|
if (locallyUpdatedSecrets.length) {
|
||||||
if (locallyDeletedSecrets.length) {
|
const newSecrets = await fnSecretBulkUpdate({
|
||||||
const newSecrets = await secretDAL.delete(
|
projectId,
|
||||||
{
|
folderId: importFolderId,
|
||||||
$in: {
|
secretVersionDAL,
|
||||||
id: locallyDeletedSecrets.map(({ id }) => id)
|
secretDAL,
|
||||||
|
tx,
|
||||||
|
secretTagDAL,
|
||||||
|
secretVersionTagDAL,
|
||||||
|
inputSecrets: locallyUpdatedSecrets.map(({ id }) => {
|
||||||
|
const doc = replicatedSecretsGroupBySecretId[id][0];
|
||||||
|
return {
|
||||||
|
filter: {
|
||||||
|
folderId: importFolderId,
|
||||||
|
id: localSecretsGroupedByBlindIndex[doc.secretBlindIndex as string][0].id
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
keyEncoding: doc.keyEncoding,
|
||||||
|
algorithm: doc.algorithm,
|
||||||
|
type: doc.type,
|
||||||
|
metadata: doc.metadata,
|
||||||
|
secretKeyIV: doc.secretKeyIV,
|
||||||
|
secretKeyTag: doc.secretKeyTag,
|
||||||
|
secretKeyCiphertext: doc.secretKeyCiphertext,
|
||||||
|
secretValueIV: doc.secretValueIV,
|
||||||
|
secretValueTag: doc.secretValueTag,
|
||||||
|
secretValueCiphertext: doc.secretValueCiphertext,
|
||||||
|
secretBlindIndex: doc.secretBlindIndex,
|
||||||
|
secretCommentIV: doc.secretCommentIV,
|
||||||
|
secretCommentTag: doc.secretCommentTag,
|
||||||
|
secretCommentCiphertext: doc.secretCommentCiphertext,
|
||||||
|
isReplicated: true,
|
||||||
|
skipMultilineEncoding: doc.skipMultilineEncoding
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
});
|
||||||
|
nestedImportSecrets = nestedImportSecrets.concat(
|
||||||
|
newSecrets.map(({ id, version }) => ({ operation: SecretOperations.Update, version, id }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (locallyDeletedSecrets.length) {
|
||||||
|
const newSecrets = await secretDAL.delete(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
id: locallyDeletedSecrets.map(({ id }) => id)
|
||||||
|
},
|
||||||
|
isReplicated: true,
|
||||||
|
folderId: importFolderId
|
||||||
},
|
},
|
||||||
isReplicated: true,
|
tx
|
||||||
folderId: importFolderId
|
);
|
||||||
},
|
nestedImportSecrets = nestedImportSecrets.concat(
|
||||||
tx
|
newSecrets.map(({ id, version }) => ({ operation: SecretOperations.Delete, version, id }))
|
||||||
);
|
);
|
||||||
nestedImportSecrets = nestedImportSecrets.concat(
|
}
|
||||||
newSecrets.map(({ id, version }) => ({ operation: SecretOperations.Delete, version, id }))
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const folderLock = await keyStore
|
const folderLock = await keyStore
|
||||||
.acquireLock([`secret-replication-${importFolderId}`], 5000)
|
.acquireLock([`secret-replication-${importFolderId}`], 5000)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
if (folderLock) {
|
if (folderLock) {
|
||||||
await snapshotService.performSnapshot(importFolderId);
|
await snapshotService.performSnapshot(importFolderId);
|
||||||
await folderLock.release();
|
await folderLock.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
await secretQueueService.syncSecrets({
|
||||||
|
projectId,
|
||||||
|
secretPath: importedFolder.path,
|
||||||
|
_deDupeReplicationQueue: deDupeReplicationQueue,
|
||||||
|
_deDupeQueue: deDupeQueue,
|
||||||
|
environmentSlug: importedFolder.environmentSlug,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
secrets: nestedImportSecrets,
|
||||||
|
folderId: importedFolder.id,
|
||||||
|
environmentId: importedFolder.envId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await secretQueueService.syncSecrets({
|
// this is used to avoid multiple times generating secret approval by failed one
|
||||||
projectId,
|
await keyStore.setItemWithExpiry(
|
||||||
secretPath: importedFolder.path,
|
keystoreReplicationSuccessKey(job.id as string, secretImport.id),
|
||||||
_deDupeReplicationQueue: deDupeReplicationQueue,
|
SECRET_IMPORT_SUCCESS_LOCK,
|
||||||
_deDupeQueue: deDupeQueue,
|
1,
|
||||||
environmentSlug: importedFolder.environmentSlug,
|
KeyStorePrefixes.SecretReplication
|
||||||
actorId,
|
);
|
||||||
actor,
|
|
||||||
secrets: nestedImportSecrets,
|
await secretImportDAL.updateById(secretImport.id, {
|
||||||
folderId: importedFolder.id,
|
lastReplicated: new Date(),
|
||||||
environmentId: importedFolder.envId
|
replicationStatus: null,
|
||||||
|
isReplicationSuccess: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
err,
|
||||||
|
`Failed to replicate secret with import id=[${secretImport.id}] env=[${secretImport.importEnv.slug}] path=[${secretImport.importPath}]`
|
||||||
|
);
|
||||||
|
await secretImportDAL.updateById(secretImport.id, {
|
||||||
|
lastReplicated: new Date(),
|
||||||
|
replicationStatus: (err as Error)?.message.slice(0, 500),
|
||||||
|
isReplicationSuccess: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await keyStore.setItemWithExpiry(
|
|
||||||
keystoreReplicationSuccessKey(job.id as string, secretImport.id),
|
|
||||||
SECRET_IMPORT_SUCCESS_LOCK,
|
|
||||||
1,
|
|
||||||
KeyStorePrefixes.SecretReplication
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await secretVersionDAL.update({ $in: { id: replicatedSecrets.map(({ id }) => id) } }, { isReplicated: true });
|
await secretVersionDAL.update({ $in: { id: replicatedSecrets.map(({ id }) => id) } }, { isReplicated: true });
|
||||||
/* eslint-enable no-await-in-loop */
|
/* eslint-enable no-await-in-loop */
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ export type TSecretImport = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
isReplication?: boolean;
|
isReplication?: boolean;
|
||||||
|
isReplicationSuccess?: boolean;
|
||||||
|
replicationStatus?: string;
|
||||||
|
lastReplicated?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetImportedFoldersByEnvDTO = {
|
export type TGetImportedFoldersByEnvDTO = {
|
||||||
|
|||||||
+59
-4
@@ -2,19 +2,31 @@ import { useEffect } from "react";
|
|||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { useSortable } from "@dnd-kit/sortable";
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
import {
|
import {
|
||||||
|
faCalendarCheck,
|
||||||
faClose,
|
faClose,
|
||||||
faFileImport,
|
faFileImport,
|
||||||
faFolder,
|
faFolder,
|
||||||
|
faInfoCircle,
|
||||||
faKey,
|
faKey,
|
||||||
faRotate,
|
faRotate,
|
||||||
faUpDown
|
faUpDown,
|
||||||
|
faWarning,
|
||||||
|
faXmark
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { format } from "date-fns";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { EmptyState, IconButton, SecretInput, TableContainer, Tag } from "@app/components/v2";
|
import {
|
||||||
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
|
SecretInput,
|
||||||
|
TableContainer,
|
||||||
|
Tag,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import { useResyncSecretReplication } from "@app/hooks/api";
|
import { useResyncSecretReplication } from "@app/hooks/api";
|
||||||
@@ -24,6 +36,9 @@ type Props = {
|
|||||||
environment: string;
|
environment: string;
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
isReplication?: boolean;
|
isReplication?: boolean;
|
||||||
|
isReplicationSuccess?: boolean;
|
||||||
|
replicationStatus?: string;
|
||||||
|
lastReplicated?: string;
|
||||||
importEnvName: string;
|
importEnvName: string;
|
||||||
importEnvPath: string;
|
importEnvPath: string;
|
||||||
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
|
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
|
||||||
@@ -62,7 +77,10 @@ export const SecretImportItem = ({
|
|||||||
importedSecrets = [],
|
importedSecrets = [],
|
||||||
searchTerm = "",
|
searchTerm = "",
|
||||||
secretPath,
|
secretPath,
|
||||||
environment
|
environment,
|
||||||
|
isReplicationSuccess,
|
||||||
|
replicationStatus,
|
||||||
|
lastReplicated
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const [isExpanded, setIsExpanded] = useToggle();
|
const [isExpanded, setIsExpanded] = useToggle();
|
||||||
@@ -137,7 +155,44 @@ export const SecretImportItem = ({
|
|||||||
isReplication={isReplication}
|
isReplication={isReplication}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2 px-4 py-2">
|
<div className="flex items-center space-x-4 px-4 py-2">
|
||||||
|
{lastReplicated && (
|
||||||
|
<Tooltip
|
||||||
|
position="left"
|
||||||
|
className="max-w-md whitespace-normal break-words"
|
||||||
|
content={
|
||||||
|
<div className="flex max-h-[10rem] flex-col overflow-auto ">
|
||||||
|
<div className="flex self-start">
|
||||||
|
<FontAwesomeIcon icon={faCalendarCheck} className="pt-0.5 pr-2 text-sm" />
|
||||||
|
<div className="text-sm">Last Replication</div>
|
||||||
|
</div>
|
||||||
|
<div className="pl-5 text-left text-xs">
|
||||||
|
{lastReplicated
|
||||||
|
? format(new Date(lastReplicated), "yyyy-MM-dd, hh:mm aaa")
|
||||||
|
: "-"}
|
||||||
|
</div>
|
||||||
|
{!isReplicationSuccess && (
|
||||||
|
<>
|
||||||
|
<div className="mt-2 flex self-start">
|
||||||
|
<FontAwesomeIcon icon={faXmark} className="pt-1 pr-2 text-sm" />
|
||||||
|
<div className="text-sm">Fail reason</div>
|
||||||
|
</div>
|
||||||
|
<div className="pl-5 text-left text-xs">{replicationStatus}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"opacity-0 group-hover:opacity-100",
|
||||||
|
!isReplicationSuccess && "text-red-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isReplicationSuccess ? faInfoCircle : faWarning} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
|
|||||||
+12
-1
@@ -159,7 +159,15 @@ export const SecretImportListView = ({
|
|||||||
>
|
>
|
||||||
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||||
{items?.map((item) => {
|
{items?.map((item) => {
|
||||||
const { importPath, importEnv, id, isReplication } = item;
|
const {
|
||||||
|
importPath,
|
||||||
|
importEnv,
|
||||||
|
id,
|
||||||
|
isReplication,
|
||||||
|
replicationStatus,
|
||||||
|
lastReplicated,
|
||||||
|
isReplicationSuccess
|
||||||
|
} = item;
|
||||||
return (
|
return (
|
||||||
<SecretImportItem
|
<SecretImportItem
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
@@ -168,6 +176,9 @@ export const SecretImportListView = ({
|
|||||||
isReplication={isReplication}
|
isReplication={isReplication}
|
||||||
importEnvPath={importPath}
|
importEnvPath={importPath}
|
||||||
importEnvName={importEnv.name}
|
importEnvName={importEnv.name}
|
||||||
|
lastReplicated={lastReplicated}
|
||||||
|
replicationStatus={replicationStatus}
|
||||||
|
isReplicationSuccess={isReplicationSuccess}
|
||||||
importedSecrets={computeImportedSecretRows(
|
importedSecrets={computeImportedSecretRows(
|
||||||
importEnv.slug,
|
importEnv.slug,
|
||||||
importPath,
|
importPath,
|
||||||
|
|||||||
Reference in New Issue
Block a user