import { getServiceAccountToken } from '@axinom/mosaic-id-link-be'; import { isNullOrWhitespace } from '@axinom/mosaic-service-common'; import axios, { AxiosError, AxiosInstance } from 'axios'; import { green, red, yellow } from 'chalk'; import { readFileSync } from 'fs'; import * as yaml from 'js-yaml'; import { exitCode } from '../../../../common'; import { GetServiceDeployOptions } from './service-deploy-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 deployService = async ( args: Required, ): Promise => { const serviceAccountToken = await getServiceAccountToken( args.idServiceAuthBaseURL, args.mosaicHostingClientId, args.mosaicHostingClientSecret, ); const axiosInstance: AxiosInstance = getAxiosInstance( args.hostingServiceBaseURL, serviceAccountToken.accessToken, ); const deploymentDetails = await retrieveServiceDeploymentDetails( args, axiosInstance, ); if (deploymentDetails === undefined) { return; } const serviceDeployment = JSON.stringify({ query: `mutation initiateServiceDeployment($serviceDefinitionId: UUID!, $containerImageVersion: String!, $serviceDeploymentConfigurationId: UUID!, $servicePiletArtifactIds: [UUID!]!, $name: String) { initiateServiceDeployment( input: {serviceDefinitionId: $serviceDefinitionId, containerImageVersion: $containerImageVersion, serviceDeploymentConfigurationId: $serviceDeploymentConfigurationId, servicePiletArtifactIds: $servicePiletArtifactIds, name: $name} ) { serviceDeploymentId name status } }`, variables: { serviceDefinitionId: deploymentDetails.serviceDefinitionId, containerImageVersion: args.containerImageTag, serviceDeploymentConfigurationId: deploymentDetails.deploymentManifestId, servicePiletArtifactIds: deploymentDetails.piletArtifactIds ?? [], name: args.name, }, }); try { const response = await axiosInstance.post('graphql', serviceDeployment); if (response.data.errors !== undefined && response.data.errors.length > 0) { console.log(red(`Deployment for Service ID [${args.serviceId}] failed.`)); if ( response.data.errors[0].message === 'Attempt to create or update an element failed, as it would have resulted in a duplicate element.' ) { console.log( yellow( `Hint: The deployment likely failed because the deployment name [${args.name}] is already in use. Please try with a different name.`, ), ); } else { console.log(red(response.data.errors[0].message)); } console.log(); } else { console.log( green( `Deployment for Service ID [${args.serviceId}] initiated successfully. It may take some time to complete the deployment.`, ), ); console.log( yellow(JSON.stringify(response.data.data?.initiateServiceDeployment)), ); console.log(); } } catch (error) { console.log( red( `Error while performing deployment for Service ID [${args.serviceId}].`, ), ); console.log(red(JSON.stringify((error as AxiosError).message))); process.exit(exitCode); } }; /** * Validate arguments for Service Deployment * * @param args * @returns */ export const validateDeploymentArgs = ( args: GetServiceDeployOptions, ): [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 ?? getServiceIdFromManifest() ?? ''; const hostingServiceBaseURL = args.hostingServiceBaseURL ?? process.env.HOSTING_SERVICE_BASE_URL ?? ''; const manifestName = args.manifestName; const containerImageTag = args.containerImageTag; let pilets = args.pilets; 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 no serviceId is given through arguments, mosaic-hosting-deployment-manifest.yaml file in the current directory is checked to derive the serviceId.', ); } if (isNullOrWhitespace(hostingServiceBaseURL)) { errorMessages.push('[hostingServiceBaseURL] is required.'); } if (isNullOrWhitespace(manifestName)) { errorMessages.push( '[manifestName] is required and cannot be an empty string.', ); } if (isNullOrWhitespace(containerImageTag)) { errorMessages.push( '[containerImageTag] is required and cannot be an empty string.', ); } if (isNullOrWhitespace(pilets)) { pilets = []; } return [ { name: args.name, idServiceAuthBaseURL, mosaicHostingClientId, mosaicHostingClientSecret, serviceId, manifestName, pilets, containerImageTag: containerImageTag, hostingServiceBaseURL, }, errorMessages, ]; }; const getServiceIdFromManifest = (): string | undefined => { try { const doc = yaml.load( readFileSync('./mosaic-hosting-deployment-manifest.yaml', 'utf8'), ); if (!isNullOrWhitespace(doc)) { return (doc as any).serviceId; } } catch (error) { return undefined; } }; const retrieveServiceDeploymentDetails = async ( args: Required, axiosInstance: AxiosInstance, ): Promise< | { serviceDefinitionId: string; deploymentManifestId: string; piletArtifactIds: string[]; } | undefined > => { const piletNamingErrors: string[] = []; const piletFilter = args.pilets.map((pilet) => { const nameAndVersion = pilet.split('@'); if (nameAndVersion.length !== 2) { piletNamingErrors.push(`Pilet name [${pilet}] is invalid.`); return; } return { name: { equalTo: nameAndVersion[0] }, packageVersion: { equalTo: nameAndVersion[1] }, serviceId: { equalTo: args.serviceId }, }; }); if (piletNamingErrors.length > 0) { console.log(red(piletNamingErrors)); console.log( yellow( `Pilet names must match the pattern [pilet-name@package-version]. i.e.: media-workflows@2.0.1.`, ), ); console.log(); return; } const getDeploymentInfoQuery = `query GetDeploymentInfo($serviceId: String!, $manifestName: String!, $pilets: ServicePiletArtifactFilter ) { serviceDefinitions(condition: {serviceId: $serviceId}) { nodes { id } } serviceDeploymentManifests( filter: {name: {equalTo: $manifestName}, serviceId: {equalTo: $serviceId}} ) { nodes { id } } servicePiletArtifacts( filter: $pilets ) { nodes { id } } }`; const deploymentInfo = JSON.stringify({ query: getDeploymentInfoQuery, variables: { serviceId: args.serviceId, manifestName: args.manifestName, pilets: { and: [{ serviceId: { equalTo: args.serviceId } }, { or: piletFilter }], }, }, }); let serviceDefinitionId: string | undefined; let deploymentManifestId: string | undefined; let piletArtifactIds: string[] = []; try { const response = await axiosInstance.post('graphql', deploymentInfo); if (response.data.errors !== undefined && response.data.errors.length > 0) { console.log( red(`Error while retrieving deployment info for [${args.serviceId}].`), ); console.log(response.data.errors); console.log(); } else { serviceDefinitionId = response.data.data?.serviceDefinitions?.nodes[0]?.id; deploymentManifestId = response.data.data?.serviceDeploymentManifests?.nodes[0]?.id; piletArtifactIds = response.data.data?.servicePiletArtifacts?.nodes?.map( (pilet) => pilet.id, ); console.log( green( `Deployment info retrieved successfully for [${args.serviceId}].`, ), ); const joinedPiletArtifactIds = piletArtifactIds?.join(','); console.log(green(`Service Definition ID:\t${serviceDefinitionId}`)); console.log(green(`Deployment Manifest ID:\t${deploymentManifestId}`)); console.log( green( `Pilet Artifacts ID(s):\t${ joinedPiletArtifactIds === '' ? '(No Pilet Artifacts)' : joinedPiletArtifactIds }`, ), ); console.log(); } } catch (error) { console.log( red(`Error while retrieving deployment info for ${args.serviceId}.`), ); console.log(JSON.stringify((error as AxiosError).message)); console.log(); } if (isNullOrWhitespace(serviceDefinitionId)) { console.log( red( `No Service Definition found for Service ID [${args.serviceId}]. Cannot proceed with deployment.`, ), ); console.log(); return; } if (isNullOrWhitespace(deploymentManifestId)) { console.log( red( `No Deployment Manifest found for name [${args.manifestName}] and Service ID [${args.serviceId}]. Cannot proceed with deployment.`, ), ); console.log(); return; } if ( args.pilets.length > 0 && args.pilets.length !== piletArtifactIds.length ) { console.log( red( `Pilet artifacts were not found for the given pilet names. Cannot proceed with deployment.`, ), ); console.log(); return; } return { serviceDefinitionId, deploymentManifestId, piletArtifactIds, }; };