feat: init repo

This commit is contained in:
wss-git 2021-06-09 11:14:08 +08:00
parent fbc7d762df
commit 9b74bd56c2
11 changed files with 559 additions and 73 deletions

View File

@ -1 +1,44 @@
## 组件说明
fc-remote-invoke 组件调用 FC 函数。
## 带有 YAML 文件用法
### yaml 配置
````
edition: 1.0.0 # 命令行YAML规范版本遵循语义化版本Semantic Versioning规范
name: compoent-test # 项目名称
services:
component-test:
component: devsapp/fc-remote-invoke # 这里引入的是相对路径正式配置替换成你自己的component名称即可
props:
region: ${region}
serviceName: ${serviceName}
functionName: ${functionName}
````
### 函数调用
````
$ s exec -- invoke --invocation-type sync --event ${payload}
$ s exec -- invoke --invocation-type async --event-file ${path}
$ s exec -- invoke --event-stdin
````
## CLI 用法
````
$ s cli fc-remote-invoke invoke --region * --service-name * --function-name * --invocation-type sync --event ${payload}
$ s cli fc-remote-invoke invoke --region * --service-name * --function-name * --invocation-type async --event-file ${path}
$ s cli fc-remote-invoke invoke --region * --service-name * --function-name * --event-stdin
````
## 特别说明
当函数是 http 函数时event最终获取值目前仅支持 json 字符串,[示例参考](https://github.com/devsapp/fc-remote-invoke/blob/master/example/http.json)
invocation-type 选填,默认 sync
event 选填event 函数默认为空字符串http 函数默认 GET 请求,其他参数为空

View File

@ -1,6 +1,6 @@
{
"name": "fc-remote-invoke",
"version": "0.0.1",
"version": "0.0.6",
"description": "This is a component demo for Serverless Devs Tool ",
"keywords": [
"Serverless",
@ -32,8 +32,10 @@
"dependencies": {
"@alicloud/fc2": "^2.2.2",
"@serverless-devs/core": "^0.0.*",
"fs-extra": "^10.0.0",
"i18n": "^0.13.2",
"lodash.get": "^4.4.2"
"lodash.get": "^4.4.2",
"readline": "^1.3.0"
},
"devDependencies": {
"@types/lodash": "^4.14.168",

View File

@ -2,7 +2,7 @@ Type: Component
Name: fc-remote-invoke
Provider:
- 其它
Version: 0.0.1
Version: 0.0.6
Description: 初始化component模板
HomePage: https://www.serverless-devs.com
Tags: #标签详情

77
src/common/help.ts Normal file
View File

@ -0,0 +1,77 @@
export default [
{
header: 'Description',
content: 'Remote Invoke',
},
{
header: 'Usage',
content: '$ fc-remote-invoke invoke <options>',
},
{
header: 'Options',
optionList: [
{
name: 'invocation-type',
description: 'Invocation type: optional value "async"|"sync", default value "sync" (default: "sync")',
alias: '-t',
type: String,
},
{
name: 'event',
description: 'Event data (strings) passed to the function during invocation (default: "")',
alias: '-e',
type: String,
},
{
name: 'event-file',
description: 'A file containing event data passed to the function during invoke.',
alias: '-f',
type: String,
},
{
name: 'event-stdin',
description: 'Read from standard input, to support script pipeline.',
alias: '-s',
type: Boolean,
},
{
name: 'region',
description: 'Pass in region in cli mode',
type: String,
},
{
name: 'service-name',
description: 'Pass in service name in cli mode',
type: String,
},
{
name: 'function-name',
description: 'Pass in function name in cli mode',
type: String,
},
],
},
{
header: 'Global Options',
optionList: [
{
name: 'help',
description: 'fc-remote-invoke help for command',
alias: 'h',
type: Boolean,
},
],
},
{
header: 'Examples with Yaml',
content: [
'$ s exec -- invoke <options>',
],
},
{
header: 'Examples with Cli',
content: [
'$ s cli fc-remote-invoke invoke --region * --service-name * --function-name * <options>',
],
},
]

View File

@ -3,34 +3,32 @@ import i18n from './i18n';
import { Logger } from '@serverless-devs/core';
export default class ComponentLogger {
static CONTENT = 'FC-REMOTE-INVOKE';
static setContent(content) {
ComponentLogger.CONTENT = content;
}
static log(m, color?: 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'whiteBright' | 'gray') {
Logger.log(i18n.__(m) || m, color);
}
static info(m) {
Logger.info(ComponentLogger.CONTENT, i18n.__(m) || m);
}
static CONTENT = 'FC-REMOTE-INVOKE';
static setContent(content) {
ComponentLogger.CONTENT = content;
}
static log(m, color?: 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'whiteBright' | 'gray') {
Logger.log(i18n.__(m) || m, color);
}
static info(m) {
Logger.info(ComponentLogger.CONTENT, i18n.__(m) || m);
}
static debug(m) {
Logger.debug(ComponentLogger.CONTENT, i18n.__(m) || m);
}
static debug(m) {
Logger.debug(ComponentLogger.CONTENT, i18n.__(m) || m);
}
static error(m) {
Logger.error(ComponentLogger.CONTENT, i18n.__(m) || m);
}
static error(m) {
Logger.error(ComponentLogger.CONTENT, i18n.__(m) || m);
}
static warning(m) {
Logger.warn(ComponentLogger.CONTENT, i18n.__(m) || m);
}
static success(m) {
Logger.log(i18n.__(m) || m, 'green');
}
static warning(m) {
Logger.warn(ComponentLogger.CONTENT, i18n.__(m) || m);
}
static success(m) {
Logger.log(i18n.__(m) || m, 'green');
}
}

View File

@ -1,45 +1,93 @@
import logger from './common/logger';
import _ from 'lodash';
import * as core from '@serverless-devs/core';
import logger from './common/logger';
import help from './common/help';
import { InputProps, ICredentials, isProperties, IProperties } from './interface/entity';
import RemoteInvoke from './lib/remote-invoke';
export default class FcRemoteInvoke {
async report(componentName: string, command: string, accountID?: string, access?: string): Promise<void> {
let uid: string = accountID;
if (_.isEmpty(accountID)) {
const credentials = await core.getCredential(access);
uid = credentials.AccountID;
}
async report(componentName: string, command: string, accountID: string): Promise<void> {
core.reportComponent(componentName, {
command,
uid,
uid: accountID,
});
}
async handlerInputs(inputs): Promise<any> {
const project = inputs?.project;
const properties = inputs?.props;
const access: string = project?.access;
const appName: string = inputs?.appName;
const credentials = await core.getCredential(access);
// 去除 args 的行首以及行尾的空格
const args: string = inputs?.args.replace(/(^\s*)|(\s*$)/g, '');
const curPath: any = inputs?.path;
const projectName: string = project?.projectName;
const { region } = properties;
async handlerInputs(inputs: InputProps): Promise<any> {
const credentials: ICredentials = await core.getCredential(inputs?.project?.access);
if (args?.includes('help')) {
// 去除 args 的行首以及行尾的空格
const args: string = (inputs?.args || '').replace(/(^\s*)|(\s*$)/g, '');
logger.debug(`input args: ${args}`);
const parsedArgs: {[key: string]: any} = core.commandParse({ args }, {
boolean: ['help', 'event-stdin'],
string: ['invocation-type', 'event', 'event-file', 'region', 'service-name', 'function-name', 'qualifier'],
alias: {
'help': 'h',
'event': 'e',
'invocation-type': 't',
'event-file': 'f',
'event-stdin': 's',
}
});
const argsData: any = parsedArgs?.data || {};
logger.debug(`command parse: ${JSON.stringify(argsData)}`);
if (argsData.help) {
return {
region,
credentials,
curPath,
args,
access,
isHelp: true,
};
}
return {};
const {
e: event,
f: eventFile,
s: eventStdin,
t: invocationType = 'sync',
} = argsData;
const eventPayload = { event, eventFile, eventStdin };
// @ts-ignore: 判断三个值有几个真
const eventFlag = !!event + !!eventFile + !!eventStdin;
if (eventFlag > 1) {
throw new Error('event | event-file | event-stdin must choose one.');
} else if (eventFlag === 0) {
eventPayload.event = '';
}
if (!['sync', 'async'].includes(invocationType)) {
throw new Error('invocation-type enum value sync, async.');
}
logger.debug(`input props: ${JSON.stringify(inputs.props)}`);
let props: IProperties = {
region: argsData.region,
serviceName: argsData['service-name'],
functionName: argsData['function-name'],
};
logger.debug(`input args props: ${JSON.stringify(props)}`);
if (!isProperties(props)) {
props = inputs.props;
}
logger.debug(`props: ${JSON.stringify(props)}`);
if (!isProperties(props)) {
throw new Error('region/serviceName(service-name)/functionName(function-name) can not be empty.');
}
props.qualifier = argsData.qualifier || inputs.props?.qualifier;
return {
props,
credentials,
eventPayload,
isHelp: false,
invocationType: _.upperFirst(invocationType),
};
}
/**
@ -47,29 +95,22 @@ export default class FcRemoteInvoke {
* @param inputs
* @returns
*/
public async invoke(inputs): Promise<any> {
public async invoke(inputs: InputProps): Promise<any> {
const {
args,
credentials
props,
eventPayload,
credentials,
isHelp,
invocationType,
} = await this.handlerInputs(inputs);
await this.report('fc-remote-invoke', 'invoke', credentials?.AccountID);
const parsedArgs: {[key: string]: any} = core.commandParse({ args }, {
boolean: ['debug'],
alias: { 'help': 'h',
'config': 'c',
'mode': 'm',
'event': 'e',
'event-file': 'f',
'event-stdin': 's',
'debug-port': 'd'
}
});
const argsData: any = parsedArgs?.data || {};
if (argsData.help) {
// TODO: help info
// await this.report('fc-remote-invoke', 'invoke', credentials?.AccountID);
if (isHelp) {
core.help(help)
return;
}
return {};
const remoteInvoke = new RemoteInvoke(props.region, credentials);
await remoteInvoke.invoke(props, eventPayload, { invocationType });
}
}

38
src/interface/entity.ts Normal file
View File

@ -0,0 +1,38 @@
export interface ICredentials {
AccountID?: string,
AccessKeyID?: string,
AccessKeySecret?: string,
SecurityToken?: string,
}
export interface InputProps {
props?: IProperties, // 用户自定义输入
credentials: ICredentials, // 用户秘钥
appName: string, //
project: {
component: string, // 组件名(支持本地绝对路径)
access: string, // 访问秘钥名
projectName: string, // 项目名
},
command: string, // 执行指令
args: string, // 命令行 扩展参数
path: {
configPath: string // 配置路径
}
}
export interface IProperties {
region: string;
serviceName: string;
functionName: string;
qualifier?: string;
}
export function isProperties(args: any): args is IProperties {
return args && args.region && args.serviceName && args.functionName;
}
export interface IEventPayload {
event?: string,
eventFile?: string,
eventStdin?: boolean,
}

14
src/lib/client.ts Normal file
View File

@ -0,0 +1,14 @@
import FC from '@alicloud/fc2';
import { ICredentials } from '../interface/entity';
export default class Client {
static buildFcClient(region: string, credentials: ICredentials) {
return new FC(credentials.AccountID, {
accessKeyID: credentials.AccessKeyID,
accessKeySecret: credentials.AccessKeySecret,
securityToken: credentials.SecurityToken,
region,
timeout: 6000000,
})
}
}

54
src/lib/event.ts Normal file
View File

@ -0,0 +1,54 @@
import path from 'path';
import fs from 'fs-extra';
import { isString } from 'lodash';
import readline from 'readline';
import logger from '../common/logger';
import { getStdin } from './stdin';
export default class File {
static async getEvent(eventFile) {
let event = await getStdin(); // read from pipes
if (!eventFile) return event;
return await new Promise((resolve, reject) => {
let input;
if (eventFile === '-') { // read from stdin
logger.log('Reading event data from stdin, which can be ended with Enter then Ctrl+D')
input = process.stdin;
} else {
input = fs.createReadStream(eventFile, {
encoding: 'utf-8'
})
}
const rl = readline.createInterface({
input,
output: process.stdout
})
event = '';
rl.on('line', (line) => {
event += line
})
rl.on('close', () => resolve(event))
rl.on('SIGINT', () => reject(new Error('^C')))
})
}
static async eventPriority(eventPriority) {
let eventFile: string;
if (isString(eventPriority.event)) {
return eventPriority.event;
} else if (eventPriority.eventStdin) {
eventFile = '-';
} else if (eventPriority.eventFile) {
eventFile = path.resolve(process.cwd(), eventPriority.eventFile);
}
return await this.getEvent(eventFile)
}
}

186
src/lib/remote-invoke.ts Normal file
View File

@ -0,0 +1,186 @@
import _ from 'lodash';
import Client from './client';
import { IProperties, IEventPayload } from '../interface/entity';
import Event from './event';
import logger from '../common/logger';
export default class RemoteInvoke {
fcClient: any;
accountId: string;
constructor(region: string, credentials) {
this.accountId = credentials.AccountID;
this.fcClient = Client.buildFcClient(region, credentials);
}
async invoke (props: IProperties, eventPayload: IEventPayload, { invocationType }) {
const event = await Event.eventPriority(eventPayload);
logger.debug(`event: ${event}`);
const {
region,
serviceName,
functionName,
qualifier,
} = props;
const httpTriggers = await this.getHttpTrigger(serviceName, functionName)
const payload: any = { event, serviceName, functionName, qualifier };
if (_.isEmpty(httpTriggers)) {
payload.invocationType = invocationType;
payload.event = event;
await this.eventInvoke(payload);
} else {
payload.region = region;
try {
payload.event = event ? JSON.parse(event) : {};
} catch (ex) {
logger.debug(ex);
throw new Error('handler event error. Example: https://github.com/devsapp/fc-remote-invoke/blob/master/example/http.json');
}
await this.httpInvoke(payload);
}
}
async getHttpTrigger(serviceName, functionName) {
const { data } = await this.fcClient.listTriggers(serviceName, functionName);
logger.debug(`get listTriggers: ${JSON.stringify(data)}`);
if (_.isEmpty(data.triggers)) { return [] }
const httpTrigger = data.triggers.filter(t => t.triggerType === 'http' || t.triggerType === 'https')
if (_.isEmpty(httpTrigger)) { return [] }
return httpTrigger;
}
async eventInvoke({
serviceName,
functionName,
event,
qualifier = 'LATEST',
invocationType
}) {
if (invocationType === 'Sync') {
const rs = await this.fcClient.invokeFunction(serviceName, functionName, event, {
'X-Fc-Log-Type': 'Tail',
'X-Fc-Invocation-Type': invocationType
}, qualifier);
const log = rs.headers['x-fc-log-result'];
if (log) {
logger.log('========= FC invoke Logs begin =========', 'yellow');
const decodedLog = Buffer.from(log, 'base64');
logger.log(decodedLog.toString());
logger.log('========= FC invoke Logs end =========', 'yellow');
logger.log('\nFC Invoke Result:', 'green');
console.log(rs.data);
console.log('\n');
}
} else {
await this.fcClient.invokeFunction(serviceName, functionName, event, {
'X-Fc-Invocation-Type': invocationType
}, qualifier);
logger.log('`${serviceName}/${functionName} async invoke success.\n`', 'green');
}
}
async httpInvoke({ region, serviceName, functionName, event, qualifier }) {
const q = qualifier ? `.${qualifier}` : '';
event.path = `/proxy/${serviceName}${q}/${functionName}/${event.path || ''}`;
logger.log(`https://${this.accountId}.${region}.fc.aliyuncs.com/2016-08-15/proxy/${serviceName}${q}/${functionName}/`);
await this.request(event)
}
/**
* @param event: { body, headers, method, queries, path }
* path /proxy/serviceName/functionName/path ,
*/
async request (event) {
const { headers, queries, method, path: p, body } = this.handlerHttpParmase(event);
let resp;
try {
const mt = method.toLocaleUpperCase();
logger.debug(`method is ${mt}.`);
logger.debug(`start invoke.`);
if (mt === 'GET') {
resp = await this.fcClient.get(p, queries, headers);
} else if (mt === 'POST') {
resp = await this.fcClient.post(p, body, headers, queries);
} else if (mt === 'PUT') {
resp = await this.fcClient.put(p, body, headers);
} else if (mt === 'DELETE') {
resp = await this.fcClient.request('DELETE', p, queries, null, headers);
/* else if (method.toLocaleUpperCase() === 'PATCH') {
resp = await this.fcClient.request('PATCH', p, queries, body, headers);
} else if (method.toLocaleUpperCase() === 'HEAD') {
resp = await this.fcClient.request('HEAD', p, queries, body, headers);
} */
} else {
logger.log(`Does not support ${method} requests temporarily.`);
}
} catch (e) {
logger.debug(e);
if (e.message === 'Unexpected token r in JSON at position 0' && e.stack.includes('/fc2/lib/client.js') && e.stack.includes('at Client.request')) {
throw new Error('The body in http responss is not in json format, but the content-type in response header is application/json. We recommend that you make the format of the response body be consistent with the content-type in response header.');
}
throw e;
}
logger.debug(`end invoke.`);
if (resp) {
const log = resp.headers['x-fc-log-result'];
if (log) {
logger.log('\n========= FC invoke Logs begin =========', 'yellow');
const decodedLog = Buffer.from(log, 'base64')
logger.log(decodedLog.toString())
logger.log('========= FC invoke Logs end =========', 'yellow');
}
logger.log('\nFC Invoke Result:', 'green');
console.log(resp.data);
console.log('\n');
}
}
handlerHttpParmase (event) {
const { body = '', headers = {}, method = 'GET', queries = '', path: p = '' } = event;
let postBody;
if (body) {
let buff = null;
if (Buffer.isBuffer(body)) {
buff = body;
headers['content-type'] = 'application/octet-stream';
} else if (typeof body === 'string') {
buff = Buffer.from(body, 'utf8');
headers['content-type'] = 'application/octet-stream';
} else if (typeof body.pipe === 'function') {
buff = body;
headers['content-type'] = 'application/octet-stream';
} else {
buff = Buffer.from(JSON.stringify(body), 'utf8');
headers['content-type'] = 'application/json';
}
postBody = buff;
}
if (!headers['X-Fc-Log-Type']) {
headers['X-Fc-Log-Type'] = 'Tail';
}
return {
headers,
queries,
method,
path: p,
body: postBody
}
}
}

33
src/lib/stdin.ts Normal file
View File

@ -0,0 +1,33 @@
const {stdin} = process;
export async function getStdin() {
let result = '';
if (stdin.isTTY) {
return result;
}
stdin.setEncoding('utf8');
for await (const chunk of stdin) {
result += chunk;
}
return result;
}
getStdin.buffer = async () => {
const result = [];
let length = 0;
if (stdin.isTTY) {
return Buffer.concat([]);
}
for await (const chunk of stdin) {
result.push(chunk);
length += chunk.length;
}
return Buffer.concat(result, length);
};