import { getServiceAccountToken } from '@axinom/mosaic-id-link-be'; import { isNullOrWhitespace } from '@axinom/mosaic-service-common'; import axios, { AxiosError, AxiosInstance } from 'axios'; import { existsSync, lstatSync, readFileSync } from 'fs'; import * as yaml from 'js-yaml'; import * as path from 'path'; import { exitCode } from '../../../common'; import { GetUploadManifestOptions } from './upload-manifest-options'; const getAxiosInstance = ( hostingServiceBaseUrl: string, serviceAccountToken: string, ): AxiosInstance => { return axios.create({ baseURL: new URL(hostingServiceBaseUrl).toString(), headers: { Authorization: `Bearer ${serviceAccountToken}`, 'content-type': 'application/json', }, }); }; export const uploadDeploymentManifest = async ( args: Required, ): Promise => { const serviceAccountToken = await getServiceAccountToken( args.idServiceAuthBaseURL, args.mosaicHostingClientId, args.mosaicHostingClientSecret, ); let manifestToUpload; try { manifestToUpload = findManifestToUpload(args.deploymentManifestPath); } catch (error) { console.log( `Error occurred while trying to read the Deployment Manifest at ${args.deploymentManifestPath}.`, ); return; } manifestToUpload = replaceDynamicVariables(manifestToUpload); if (manifestToUpload.serviceId !== args.serviceId) { console.log( `Service ID in the given Deployment Manifest does not match with the argument value. Please make sure the value of the argument given for Service ID matches the value in the Deployment Manifest.`, ); return; } const axiosInstance: AxiosInstance = getAxiosInstance( args.hostingServiceBaseURL, serviceAccountToken.accessToken, ); const data = JSON.stringify({ query: `mutation CreateServiceDeploymentManifest($serviceId: String!, $name: String, $configMap: DeploymentConfigMap!) { createServiceDeploymentManifest( input: {configMap: $configMap, name: $name, serviceId: $serviceId} ) { serviceDeploymentManifest { id name } } }`, variables: { serviceId: args.serviceId, name: args.name === '' ? undefined : args.name, configMap: manifestToUpload, }, }); try { const response = await axiosInstance.post('graphql', data); if (response.data.errors !== undefined && response.data.errors.length > 0) { console.log('Upload of Deployment Manifest failed.'); console.log(response.data.errors[0].message); console.log(); if (response.data.errors[0].details?.validateErrors?.length > 0) { console.log('Schema Validation Errors:'); for (const detail of response.data.errors[0].details.validateErrors) { console.log(detail); } console.log(); } if (response.data.errors[0].details?.validationErrors?.length > 0) { console.log('Deployment Manifest Syntax Validation Errors:'); const validationErrors: string[] = []; for (const detail of response.data.errors[0].details.validationErrors) { validationErrors.push(detail); } console.log(validationErrors); console.log(); } console.log( `The Deployment Manifest Schema can be found at ${args.hostingServiceBaseURL}/.well-known.`, ); } else { console.log('Upload of Deployment Manifest successful.'); console.log( response.data.data?.createServiceDeploymentManifest .serviceDeploymentManifest, ); } } catch (error) { console.log('Error while uploading the Deployment Manifest.'); console.log(JSON.stringify((error as AxiosError).message)); process.exit(exitCode); } }; /** * Method that validates the arguments. * We're using this to allow both input arguments and environment variables. * * @param args * @returns */ export const validateArgs = ( args: GetUploadManifestOptions, ): [Required, string[]] => { const errorMessages: string[] = []; const idServiceAuthBaseURL = args.idServiceAuthBaseURL ?? process.env.ID_SERVICE_AUTH_BASE_URL ?? ''; const mosaicHostingClientId = args.mosaicHostingClientId ?? process.env.MOSAIC_HOSTING_CLIENT_ID ?? ''; const mosaicHostingClientSecret = args.mosaicHostingClientSecret ?? process.env.MOSAIC_HOSTING_CLIENT_SECRET ?? ''; const serviceId = args.serviceId ?? process.env.SERVICE_ID ?? ''; const hostingServiceBaseURL = args.hostingServiceBaseURL ?? process.env.HOSTING_SERVICE_BASE_URL ?? ''; if (isNullOrWhitespace(idServiceAuthBaseURL)) { errorMessages.push('[idServiceAuthBaseURL] is required.'); } if (isNullOrWhitespace(mosaicHostingClientId)) { errorMessages.push('[clientId] is required.'); } if (isNullOrWhitespace(mosaicHostingClientSecret)) { errorMessages.push('[clientSecret] is required.'); } if (isNullOrWhitespace(serviceId)) { errorMessages.push('[serviceId] is required.'); } if (isNullOrWhitespace(hostingServiceBaseURL)) { errorMessages.push('[hostingServiceBaseURL] is required.'); } return [ { name: args.name, idServiceAuthBaseURL, mosaicHostingClientId, mosaicHostingClientSecret, serviceId, deploymentManifestPath: args.deploymentManifestPath ?? './mosaic-hosting-deployment-manifest.yaml', hostingServiceBaseURL, }, errorMessages, ]; }; const findManifestToUpload = (deploymentManifestPath: string): unknown => { if (!existsSync(deploymentManifestPath)) { throw new Error(`No directory found at ${deploymentManifestPath}`); } const stat = lstatSync(deploymentManifestPath); let manifestFile: string; if (stat.isDirectory()) { manifestFile = path.join( deploymentManifestPath, 'mosaic-hosting-deployment-manifest.yaml', ); } else { manifestFile = deploymentManifestPath; } const doc = yaml.load(readFileSync(manifestFile, 'utf8')); console.log(`Deployment Manifest found at ${manifestFile}.`); return doc; }; const replaceDynamicVariables = (manifestToUpload: object): string => { const matcher = /\${__ax_hosted__\.dynamic\.([\w\d_]+)}/g; const stringifiedManifestToUpload = JSON.stringify(manifestToUpload).replace( matcher, (match, variableName) => { const envVarName = variableName.trim(); const envVarValue = process.env[envVarName]; if (envVarValue !== undefined) { return envVarValue; } else { console.error( `\n Could not find the environment variable [${envVarName}] used in the placeholder [${match}].\n`, ); process.exit(exitCode); } }, ); return JSON.parse(stringifiedManifestToUpload); };