faasjs/faasjs

View on GitHub
packages/tencentcloud/src/cloud_function/deploy.ts

Summary

Maintainability
A
0 mins
Test Coverage
F
6%
import type { DeployData } from '@faasjs/func'
import { deepMerge } from '@faasjs/deep_merge'
import { Logger, Color } from '@faasjs/logger'
import { execSync } from 'node:child_process'
import { checkBucket, createBucket, upload, remove } from './cos'
import { scf } from './scf'
import type { Provider } from '..'
import { join } from 'node:path'

const defaults = {
  Handler: 'index.handler',
  MemorySize: 64,
  Timeout: 30,
  Runtime: 'Nodejs12.16',
}

// 腾讯云内置插件 https://cloud.tencent.com/document/product/583/12060
const INCLUDED_NPM = [
  'cos-nodejs-sdk-v5',
  'base64-js',
  'buffer',
  'crypto-browserify',
  'ieee754',
  'imagemagick',
  'isarray',
  'jmespath',
  'lodash',
  'microtime',
  'npm',
  'punycode',
  'puppeteer',
  'qcloudapi-sdk',
  'request',
  'sax',
  'scf-nodejs-serverlessdb-sdk',
  'tencentcloud-sdk-nodejs',
  'url',
  'uuid',
  'xml2js',
  'xmlbuilder',

  // 移除构建所需的依赖项
  '@faasjs/load',
]

export async function deployCloudFunction(
  tc: Provider,
  data: DeployData,
  origin: { [key: string]: any }
): Promise<void> {
  const logger = new Logger(`${data.env}#${data.name}`)

  const loggerPrefix = `[${data.env}#${data.name}]`
  logger.raw(
    `${logger.colorfy(Color.GRAY, `${loggerPrefix}[01/12]`)} 生成配置项...`
  )

  const config = deepMerge(origin)

  // 补全默认参数
  if (config.config.name) {
    config.config.FunctionName = config.config.name
    delete config.config.name
  } else config.config.FunctionName = data.name.replace(/[^a-zA-Z0-9-_]/g, '_')

  if (!config.config.Description)
    config.config.Description = `Source: ${data.name}\nPublished by ${process.env.LOGNAME}\nPublished at ${config.config.version}`

  if (config.config.memorySize) {
    config.config.MemorySize = config.config.memorySize
    delete config.config.memorySize
  }
  if (config.config.timeout) {
    config.config.Timeout = config.config.timeout
    delete config.config.timeout
  }

  config.config = deepMerge(defaults, config.config, {
    // 基本参数
    Region: tc.config.region,
    Namespace: data.env,
    Environment: {
      Variables: [
        {
          Key: 'FaasMode',
          Value: 'remote',
        },
        {
          Key: 'FaasEnv',
          Value: data.env,
        },
        {
          Key: 'FaasLog',
          Value: 'debug',
        },
        {
          Key: 'NODE_ENV',
          Value: data.env,
        },
      ],
    },
    FunctionVersion: '1',

    // 构建参数
    filename: data.filename,
    name: data.name,
    version: data.version,
    env: data.env,
    dependencies: data.dependencies,
    tmp: data.tmp,

    // cos 参数
    Bucket: `scf-${tc.config.appId}`,
    FilePath: `${data.tmp}deploy.zip`,
    CosObjectName: `${data.env}/${config.config.FunctionName}/${data.version}.zip`,
  })

  logger.debug('[01/12] 完成配置项 %j', config)

  logger.raw(
    `${logger.colorfy(Color.GRAY, `${loggerPrefix}[02/12]`)} 生成代码包...`
  )

  logger.debug('[2.1/12] 生成 index.js...')

  const ts = await require('@faasjs/load').loadTs(config.config.filename, {
    output: {
      file: `${config.config.tmp}/index.js`,
      format: 'cjs',
      name: 'index',
      banner: `/**
 * @name ${config.config.name}
 * @author ${config.config.author}
 * @build ${config.config.version}
 * @staging ${config.config.env}
 * @dependencies ${Object.keys(config.config.dependencies).join(',')}
 */`,
      footer: `
const main = module.exports;
main.config = ${JSON.stringify(data.config)};
if(typeof cloud_function !== 'undefined' && !main.plugins.find(p => p.type === 'cloud_function'))
  main.plugins.unshift(new cloud_function.CloudFunction(main.config.plugins['cloud_function'] || {}))
module.exports = main.export();`,
    },
    modules: {
      excludes: INCLUDED_NPM,
      additions: Object.keys(config.config.dependencies).concat([
        '@faasjs/tencentcloud',
      ]),
    },
  })

  logger.debug('%j', ts.modules)

  logger.debug('[2.2/12] 生成 node_modules...')
  for (const key in ts.modules) {
    const target = join(config.config.tmp, 'node_modules', key)
    execSync(`mkdir -p ${target}`)
    execSync(
      `rsync -avhpr --exclude '*.cache' --exclude '*.bin' --exclude 'LICENSE' --exclude 'license' --exclude 'ChangeLog' --exclude 'CHANGELOG' --exclude '*.ts' --exclude '*.flow' --exclude '*.map' --exclude '*.md' --exclude 'node_modules/*/node_modules' --exclude '__tests__' ${join(
        ts.modules[key],
        '*'
      )} ${target}`
    )
  }

  logger.raw(
    `${logger.colorfy(Color.GRAY, `${loggerPrefix}[03/12]`)} 打包代码包...`
  )
  execSync(`cd ${config.config.tmp} && zip -r deploy.zip *`)

  logger.raw(
    `${logger.colorfy(Color.GRAY, `${loggerPrefix}[04/12]`)} 检查 COS...`
  )

  try {
    logger.debug('[4.1/12] 检查 Cos Bucket 状态')
    await checkBucket(tc, {
      Bucket: config.config.Bucket,
      Region: tc.config.region,
    })
    logger.debug('[4.2/12] Cos Bucket 已存在,跳过')
  } catch (_) {
    logger.debug('[4.2/12] 创建 Cos Bucket...')
    await createBucket(tc, {
      Bucket: config.config.Bucket,
      Region: tc.config.region,
    })
  }

  logger.raw(
    `${logger.colorfy(Color.GRAY, `${loggerPrefix}[05/12]`)} 上传代码包...`
  )
  await upload(tc, {
    Bucket: config.config.Bucket,
    FilePath: config.config.FilePath,
    Key: config.config.CosObjectName,
    Region: tc.config.region,
  })

  logger.raw(
    `${logger.colorfy(Color.GRAY, `${loggerPrefix}[06/12]`)} 检查命名空间...`
  )
  logger.debug('[6.1/12] 检查命名空间状态')
  const namespaceList = await scf('ListNamespaces', tc.config, {})
  if (
    !namespaceList.Namespaces.find(
      (n: any) => n.Name === config.config.Namespace
    )
  ) {
    logger.debug('[6.2/12] 创建命名空间...')
    await scf('CreateNamespace', tc.config, {
      Namespace: config.config.Namespace,
    })
  } else logger.debug('[6.2/12] 命名空间已存在,跳过')

  logger.raw(
    `${logger.colorfy(Color.GRAY, `${loggerPrefix}[07/12]`)} 上传云函数...`
  )

  try {
    logger.debug('[7.1/12] 检查云函数是否已存在...')
    await scf('GetFunction', tc.config, {
      FunctionName: config.config.FunctionName,
      Namespace: config.config.Namespace,
    })

    logger.debug('[7.2/12] 更新云函数代码...')
    await scf('UpdateFunctionCode', tc.config, {
      CosBucketName: 'scf',
      CosBucketRegion: config.config.Region,
      CosObjectName: config.config.CosObjectName,
      FunctionName: config.config.FunctionName,
      Handler: config.config.Handler,
      Namespace: config.config.Namespace,
    })

    let status = null
    while (status !== 'Active') {
      logger.debug('[7.3/12] 等待云函数代码更新完成...')

      status = await scf('GetFunction', tc.config, {
        FunctionName: config.config.FunctionName,
        Namespace: config.config.Namespace,
      }).then(res => res.Status)
    }

    logger.debug('[7.2/12] 更新云函数配置...')
    await scf('UpdateFunctionConfiguration', tc.config, {
      Environment: config.config.Environment,
      FunctionName: config.config.FunctionName,
      MemorySize: config.config.MemorySize,
      Timeout: config.config.Timeout,
      VpcConfig: config.config.VpcConfig,
      Namespace: config.config.Namespace,
      Role: config.config.Role,
      ClsLogsetId: config.config.ClsLogsetId,
      ClsTopicId: config.config.ClsTopicId,
      Layers: config.config.Layers,
      DeadLetterConfig: config.config.DeadLetterConfig,
      PublicNetConfig: config.config.PublicNetConfig,
      CfsConfig: config.config.CfsConfig,
      InitTimeout: config.config.InitTimeout,
    })

    status = null
    while (status !== 'Active') {
      logger.debug('[7.3/12] 等待云函数配置更新完成...')

      status = await scf('GetFunction', tc.config, {
        FunctionName: config.config.FunctionName,
        Namespace: config.config.Namespace,
      }).then(res => res.Status)
    }
  } catch (error: any) {
    if (error.message.startsWith('ResourceNotFound')) {
      logger.debug('[7.2/12] 创建云函数...')
      await scf('CreateFunction', tc.config, {
        ClsLogsetId: config.config.ClsLogsetId,
        ClsTopicId: config.config.ClsTopicId,
        Code: {
          CosBucketName: 'scf',
          CosBucketRegion: config.config.Region,
          CosObjectName: config.config.CosObjectName,
        },
        CodeSource: config.config.CodeSource,
        Environment: config.config.Environment,
        FunctionName: config.config.FunctionName,
        Handler: config.config.Handler,
        Description: config.config.Description,
        Namespace: config.config.Namespace,
        MemorySize: config.config.MemorySize,
        Runtime: config.config.Runtime,
        Role: config.config.Role,
        Timeout: config.config.Timeout,
        VpcConfig: config.config.VpcConfig,
        Layers: config.config.Layers,
        DeadLetterConfig: config.config.DeadLetterConfig,
        PublicNetConfig: config.config.PublicNetConfig,
        CfsConfig: config.config.CfsConfig,
        InitTimeout: config.config.InitTimeout,
      })

      while (true) {
        logger.debug('[7.3/12] 等待云函数代码更新完成...')
        if (
          (
            await scf('GetFunction', tc.config, {
              FunctionName: config.config.FunctionName,
              Namespace: config.config.Namespace,
            })
          ).Status === 'Active'
        )
          break
      }
    } else throw error
  }

  logger.raw(
    `${logger.colorfy(Color.GRAY, `${loggerPrefix}[08/12]`)} 发布版本...`
  )

  const version = await scf('PublishVersion', tc.config, {
    Description: `Published by ${process.env.LOGNAME}`,
    FunctionName: config.config.FunctionName,
    Namespace: config.config.Namespace,
  })
  config.config.FunctionVersion = version.FunctionVersion

  while (true) {
    logger.debug('[8.1/12] 等待版本发布完成...')
    if (
      (
        await scf('GetFunction', tc.config, {
          FunctionName: config.config.FunctionName,
          Namespace: config.config.Namespace,
          Qualifier: config.config.FunctionVersion,
        })
      ).Status === 'Active'
    )
      break
  }

  logger.raw(
    `${logger.colorfy(Color.GRAY, `${loggerPrefix}[09/12]`)} 更新别名...`
  )

  try {
    logger.debug('[9.1/12] 检查别名状态...')
    await scf('GetAlias', tc.config, {
      Name: config.config.Namespace,
      FunctionName: config.config.FunctionName,
      Namespace: config.config.Namespace,
    })

    logger.debug('[9.2/12] 更新别名...')
    await scf('UpdateAlias', tc.config, {
      Name: config.config.Namespace,
      FunctionName: config.config.FunctionName,
      Namespace: config.config.Namespace,
      FunctionVersion: config.config.FunctionVersion,
    })
  } catch (error: any) {
    if (error.message.startsWith('ResourceNotFound.Alias')) {
      logger.debug('[9.2/12] 创建别名...')
      await scf('CreateAlias', tc.config, {
        Name: config.config.Namespace,
        FunctionName: config.config.FunctionName,
        FunctionVersion: config.config.FunctionVersion,
        Namespace: config.config.Namespace,
      })
    } else throw error
  }

  logger.raw(
    `${logger.colorfy(Color.GRAY, `${loggerPrefix}[10/12]`)} 更新触发器...`
  )
  const triggers = await scf('ListTriggers', tc.config, {
    FunctionName: config.config.FunctionName,
    Namespace: config.config.Namespace,
  })
  for (const trigger of triggers.Triggers) {
    logger.debug('[10.1/12] 删除旧触发器: %s...', trigger.TriggerName)
    await scf('DeleteTrigger', tc.config, {
      FunctionName: config.config.FunctionName,
      Namespace: config.config.Namespace,
      TriggerName: trigger.TriggerName,
      Type: trigger.Type,
      Qualifier: trigger.Qualifier,
    })
  }

  if (config.config.triggers)
    for (const trigger of config.config.triggers) {
      logger.debug('[10.2/12] 创建触发器 %s...', trigger.name)
      await scf('CreateTrigger', tc.config, {
        FunctionName: config.config.FunctionName,
        TriggerName: trigger.name || trigger.type,
        Type: trigger.type,
        TriggerDesc: trigger.value,
        Qualifier: config.config.FunctionVersion,
        Namespace: config.config.Namespace,
        Enable: 'OPEN',
      })
    }

  logger.raw(
    `${logger.colorfy(Color.GRAY, `${loggerPrefix}[11/12]`)} 更新预制并发...`
  )

  logger.debug('[11.1/12] 查询预制并发...')
  const current = await scf('GetProvisionedConcurrencyConfig', tc.config, {
    FunctionName: config.config.FunctionName,
    Namespace: config.config.Namespace,
  })
  if (current.Allocated.length)
    for (const allocated of current.Allocated) {
      logger.debug('[11.2/12] 删除旧预制并发 %j...', allocated)
      await scf('DeleteProvisionedConcurrencyConfig', tc.config, {
        FunctionName: config.config.FunctionName,
        Namespace: config.config.Namespace,
        Qualifier: allocated.Qualifier,
      })
    }

  if (config.config.provisionedConcurrent?.executions) {
    logger.debug(
      '[11/12] 添加预制并发 %s...',
      config.config.provisionedConcurrent.executions
    )
    await scf('PutProvisionedConcurrencyConfig', tc.config, {
      FunctionName: config.config.FunctionName,
      Namespace: config.config.Namespace,
      Qualifier: config.config.FunctionVersion,
      VersionProvisionedConcurrencyNum:
        config.config.provisionedConcurrent.executions,
    })
  }

  logger.raw(
    `${logger.colorfy(Color.GRAY, `${loggerPrefix}[12/12]`)} 清理文件...`
  )

  logger.debug('[12.1/12] 清理 Cos Bucket...')
  await remove(tc, {
    Bucket: config.config.Bucket,
    Key: config.config.CosObjectName,
    Region: config.config.Region,
  })

  if (process.env.FaasLog !== 'debug') {
    logger.debug('[12.2/12] 清理本地文件...')
    execSync(`rm -rf ${config.config.tmp}`)
  }

  logger.info(
    `云函数发布完成 ${data.env}#${data.name}#${config.config.FunctionVersion}`
  )
}