ConfigParserService.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { ValidationError, ValidationService } from '../validation/ValidationService'
  2. import { Config } from '../../types'
  3. import fs from 'fs'
  4. import path from 'path'
  5. import YAML from 'yaml'
  6. import _ from 'lodash'
  7. import configSchema, { bytesizeUnits } from '../../schemas/configSchema'
  8. import { JSONSchema4, JSONSchema4TypeName } from 'json-schema'
  9. const MIN_CACHE_SIZE = 20 * Math.pow(1024, 3)
  10. export class ConfigParserService {
  11. validator: ValidationService
  12. constructor() {
  13. this.validator = new ValidationService()
  14. }
  15. public resolveConfigDirectoryPaths(paths: Config['directories'], configFilePath: string): Config['directories'] {
  16. return _.mapValues(paths, (v) =>
  17. typeof v === 'string' ? path.resolve(path.dirname(configFilePath), v) : v
  18. ) as Config['directories']
  19. }
  20. public resolveConfigKeysPaths(keys: Config['keys'], configFilePath: string): Config['keys'] {
  21. return keys.map((k) =>
  22. 'keyfile' in k ? { keyfile: path.resolve(path.dirname(configFilePath), k.keyfile) } : k
  23. ) as Config['keys']
  24. }
  25. private parseBytesize(bytesize: string) {
  26. const intValue = parseInt(bytesize)
  27. const unit = bytesize[bytesize.length - 1]
  28. return intValue * Math.pow(1024, bytesizeUnits.indexOf(unit))
  29. }
  30. private schemaTypeOf(schema: JSONSchema4, path: string[]): JSONSchema4['type'] {
  31. if (schema.properties && schema.properties[path[0]]) {
  32. const item = schema.properties[path[0]]
  33. if (path.length > 1) {
  34. return this.schemaTypeOf(item, path.slice(1))
  35. }
  36. if (item.oneOf) {
  37. const validTypesSet = new Set<JSONSchema4TypeName>()
  38. item.oneOf.forEach(
  39. (s) =>
  40. Array.isArray(s.type)
  41. ? s.type.forEach((t) => validTypesSet.add(t))
  42. : s.type
  43. ? validTypesSet.add(s.type)
  44. : undefined // do nothing
  45. )
  46. return Array.from(validTypesSet)
  47. }
  48. return item.type
  49. }
  50. }
  51. private setConfigEnvValue(
  52. config: Record<string, unknown>,
  53. path: string[],
  54. envKey: string,
  55. envValue: string | undefined
  56. ) {
  57. const schemaType = this.schemaTypeOf(configSchema, path)
  58. const possibleTypes = Array.isArray(schemaType) ? schemaType : [schemaType]
  59. for (const i in possibleTypes) {
  60. try {
  61. switch (possibleTypes[i]) {
  62. case undefined:
  63. // Invalid key - skip
  64. break
  65. case 'integer':
  66. _.set(config, path, parseInt(envValue || ''))
  67. break
  68. case 'number':
  69. _.set(config, path, parseFloat(envValue || ''))
  70. break
  71. case 'boolean':
  72. _.set(config, path, !!envValue)
  73. break
  74. case 'array':
  75. case 'object':
  76. try {
  77. const parsed = JSON.parse(envValue || 'undefined')
  78. _.set(config, path, parsed)
  79. } catch (e) {
  80. throw new ValidationError(`Invalid env value of ${envKey}: Not a valid JSON`, null)
  81. }
  82. break
  83. default:
  84. _.set(config, path, envValue)
  85. }
  86. const errors = this.validator.errorsByProperty('Config', path.join('.'), config)
  87. if (errors) {
  88. throw new ValidationError(`Invalid env value of ${envKey}`, errors)
  89. }
  90. return
  91. } catch (e) {
  92. // Only throw if there are no more possible types to test against
  93. if (parseInt(i) === possibleTypes.length - 1) {
  94. throw e
  95. }
  96. }
  97. }
  98. }
  99. private mergeEnvConfigWith(config: Record<string, unknown>) {
  100. Object.entries(process.env)
  101. .filter(([envKey]) => envKey.startsWith('JOYSTREAM_DISTRIBUTOR__'))
  102. .forEach(([envKey, envValue]) => {
  103. const configPath = envKey
  104. .replace('JOYSTREAM_DISTRIBUTOR__', '')
  105. .split('__')
  106. .map((key) => _.camelCase(key))
  107. this.setConfigEnvValue(config, configPath, envKey, envValue)
  108. })
  109. }
  110. public loadConfig(configPath: string): Config {
  111. let inputConfig: Record<string, unknown> = {}
  112. // Try to load config from file if exists
  113. if (fs.existsSync(configPath)) {
  114. const fileContent = fs.readFileSync(configPath).toString()
  115. if (path.extname(configPath) === '.json') {
  116. inputConfig = JSON.parse(fileContent)
  117. } else if (path.extname(configPath) === '.yml' || path.extname(configPath) === '.yaml') {
  118. inputConfig = YAML.parse(fileContent)
  119. } else {
  120. throw new Error('Unrecognized config format (use .yml or .json)')
  121. }
  122. }
  123. // Override config with env variables
  124. this.mergeEnvConfigWith(inputConfig)
  125. // Validate the config
  126. const configJson = this.validator.validate('Config', inputConfig)
  127. // Normalize values
  128. const directories = this.resolveConfigDirectoryPaths(configJson.directories, configPath)
  129. const keys = this.resolveConfigKeysPaths(configJson.keys, configPath)
  130. const storageLimit = this.parseBytesize(configJson.limits.storage)
  131. if (storageLimit < MIN_CACHE_SIZE) {
  132. throw new Error('Cache storage limit should be at least 20G!')
  133. }
  134. const parsedConfig: Config = {
  135. ...configJson,
  136. directories,
  137. keys,
  138. limits: {
  139. ...configJson.limits,
  140. storage: storageLimit,
  141. },
  142. }
  143. return parsedConfig
  144. }
  145. }