in controlplane/src/core/composition/composer.ts [236:395]
async uploadRouterConfig({
routerConfig,
blobStorage,
organizationId,
federatedGraphId,
federatedSchemaVersionId,
admissionConfig,
admissionWebhookURL,
admissionWebhookSecret,
actorId,
routerCompatibilityVersion,
}: {
routerConfig: RouterConfig;
blobStorage: BlobStorage;
organizationId: string;
federatedGraphId: string;
federatedSchemaVersionId: string;
admissionConfig: {
jwtSecret: string;
cdnBaseUrl: string;
};
admissionWebhookURL?: string;
admissionWebhookSecret?: string;
actorId: string;
routerCompatibilityVersion: string;
}): Promise<{
errors: ComposeDeploymentError[];
}> {
const routerConfigJsonStringBytes = Buffer.from(routerConfig.toJsonString(), 'utf8');
const errors: ComposeDeploymentError[] = [];
let versionPath = '';
if (routerCompatibilityVersion !== ROUTER_COMPATIBILITY_VERSION_ONE) {
if (ROUTER_COMPATIBILITY_VERSIONS.has(routerCompatibilityVersion as SupportedRouterCompatibilityVersion)) {
versionPath = `${routerCompatibilityVersion}/`;
} else {
errors.push(
new RouterConfigUploadError(`Invalid router compatibility version "${routerCompatibilityVersion}".`),
);
return {
errors,
};
}
}
// CDN path and bucket path are the same in this case
const s3PathDraft = `${organizationId}/${federatedGraphId}/routerconfigs/draft.json`;
const s3PathReady = `${organizationId}/${federatedGraphId}/routerconfigs/${versionPath}latest.json`;
// The signature will be added by the admission webhook
let signatureSha256: undefined | string;
// It is important to use undefined here, we do not null check in the database queries
let deploymentError: RouterConfigUploadError | undefined;
let admissionError: AdmissionError | undefined;
if (admissionWebhookURL) {
try {
// 1. Upload the draft config to the blob storage
// so that the admission webhook can download it.
await blobStorage.putObject<S3RouterConfigMetadata>({
key: s3PathDraft,
body: routerConfigJsonStringBytes,
contentType: 'application/json; charset=utf-8',
metadata: {
version: federatedSchemaVersionId,
'signature-sha256': '', // The signature will be added by the admission webhook
},
});
try {
// 2. Create a private URL with a token that the admission webhook can use to fetch the draft config.
// The token is valid for 5 minutes and signed with the organization ID and the federated graph ID.
const token = await signJwtHS256<AdmissionWebhookJwtPayload>({
secret: admissionConfig.jwtSecret,
token: {
exp: nowInSeconds() + 5 * 60, // 5 minutes
aud: audiences.cosmoCDNAdmission, // to distinguish from other tokens
organization_id: organizationId,
federated_graph_id: federatedGraphId,
},
});
const admissionWebhookController = new AdmissionWebhookController(
this.db,
this.logger,
admissionWebhookURL,
admissionWebhookSecret,
);
const resp = await admissionWebhookController.validateConfig(
{
privateConfigUrl: `${admissionConfig.cdnBaseUrl}/${s3PathDraft}?token=${token}`,
organizationId,
federatedGraphId,
},
actorId,
);
signatureSha256 = resp.signatureSha256;
} finally {
// Always clean up the draft config after the draft has been validated.
await blobStorage.deleteObject({
key: s3PathDraft,
});
}
} catch (err: any) {
this.logger.debug(
{
error: err,
federatedGraphId,
},
`Admission webhook failed to validate the router config for the federated graph.`,
);
if (err instanceof AdmissionError) {
admissionError = err;
} else {
admissionError = new AdmissionError('Admission webhook failed to validate the router config', err);
}
}
}
// Deploy the final router config to the blob storage if the admission webhook did not fail
if (!admissionError) {
try {
await blobStorage.putObject<S3RouterConfigMetadata>({
key: s3PathReady,
body: routerConfigJsonStringBytes,
contentType: 'application/json; charset=utf-8',
metadata: {
version: federatedSchemaVersionId,
'signature-sha256': signatureSha256 || '',
},
});
} catch (err: any) {
this.logger.error(err, `Failed to upload the final router config for ${federatedGraphId} to the blob storage`);
deploymentError = new RouterConfigUploadError('Failed to upload the final router config to the CDN', err);
}
}
if (deploymentError || admissionError) {
await this.graphCompositionRepository.updateComposition({
fedGraphSchemaVersionId: federatedSchemaVersionId,
deploymentErrorString: deploymentError?.message,
admissionErrorString: admissionError?.message,
});
} else if (signatureSha256) {
await this.graphCompositionRepository.updateComposition({
fedGraphSchemaVersionId: federatedSchemaVersionId,
routerConfigSignature: signatureSha256,
});
}
if (deploymentError) {
errors.push(deploymentError);
}
if (admissionError) {
errors.push(admissionError);
}
return {
errors,
};
}