async uploadRouterConfig()

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,
    };
  }