Repository: aliyun/fun
Pull request #78 (2018-09-17 07:42:52)

add custom domain support
add custom domain support

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/aliyun/fun?pullRequest=78) <br/>Thank you for your submission, we really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/aliyun/fun?pullRequest=78) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/aliyun/fun?pullRequest=78) it.</sub>

Patch:
diff --git a/examples/custom_domain/template.yml b/examples/custom_domain/template.yml
new file mode 100644
index 0000000..9f1e1a6
--- /dev/null
+++ b/examples/custom_domain/template.yml
@@ -0,0 +1,16 @@
+ROSTemplateFormatVersion: '2015-09-01'
+Transform: 'Aliyun::Serverless-2018-04-03'
+Resources:
+  fun.cn-shanghai.1221968287646227.cname-test.fc.aliyun-inc.com:
+    Type: 'Aliyun::Serverless::CustomDomain'
+    Properties:
+      Protocol: HTTP
+      RouteConfig:
+        routes: 
+          '/a':
+            serviceName: serviceA
+            functionName: functionA
+          '/b':
+            serviceName: serviceB
+            functionName: functionB
+  
\ No newline at end of file
diff --git a/lib/deploy/deploy-by-tpl.js b/lib/deploy/deploy-by-tpl.js
index 46d692b..56d7c1d 100644
--- a/lib/deploy/deploy-by-tpl.js
+++ b/lib/deploy/deploy-by-tpl.js
@@ -16,9 +16,9 @@ const readFile = util.promisify(fs.readFile);
 
 let {
   makeApi, makeApiTrigger, makeFunction,
-  makeGroup, makeOtsTable, makeOtsInstance, 
+  makeGroup, makeOtsTable, makeOtsInstance,
   makeOtsTrigger, makeService, makeTrigger,
-  makeSlsProject, makeLogstore, makeLogstoreIndex
+  makeSlsProject, makeLogstore, makeLogstoreIndex, makeCustomDomain
 } = require('./deploy-support');
 
 let {
@@ -89,7 +89,7 @@ async function deployFunctions(serviceName, serviceDefinition) {
     if ((v || {}).Type === 'Aliyun::Serverless::Function') {
       console.log(`\tWaiting for function ${k} to be deployed...`);
       await deployFunction(serviceName, k, v);
-      console.log(green(`\tfunction ${k} deploy success` ));
+      console.log(green(`\tfunction ${k} deploy success`));
     }
   }
 }
@@ -98,16 +98,16 @@ async function deployPolicy(serviceName, roleName, policy, curCount) {
   if (typeof policy === 'string') {
     await attachPolicyToRole(policy, roleName);
     return curCount;
-  } 
+  }
 
   const profile = await getProfile();
 
   const policyName = nomalizeRoleOrPoliceName(`AliyunFcGeneratedServicePolicy-${profile.defaultRegion}-${serviceName}${curCount}`);
   
   await makeAndAttachPolicy(policyName, policy, roleName);
-    
+
   return curCount + 1;
-  
+
 }
 
 async function deployPolicies(serviceName, roleName, policies) {
@@ -141,6 +141,7 @@ async function deployFcService(serviceName, serviceDefinition) {
     } catch(ex) {
       throw new Error('The role you provided is not correct. You must provide the correct role arn.');
     }
+
     createRoleIfNotExist = false;
   } else {
     roleName = `aliyunfcgeneratedrole-${profile.defaultRegion}-${serviceName}`;
@@ -150,11 +151,11 @@ async function deployFcService(serviceName, serviceDefinition) {
 
   const role = await makeRole(roleName, createRoleIfNotExist);
 
-  if ( ! roleArn && policies ) { // if roleArn exist, then ignore polices
+  if (!roleArn && policies) { // if roleArn exist, then ignore polices
     await deployPolicies(serviceName, roleName, policies);
   }
 
-  if ( ! roleArn && vpcConfig) {
+  if (!roleArn && vpcConfig) {
     await attachPolicyToRole('AliyunECSNetworkInterfaceManagementAccess', roleName);
   }
 
@@ -177,7 +178,7 @@ async function deployFcService(serviceName, serviceDefinition) {
     }
   } else if (logConfig.LogStore || logConfig.Project) {
     throw new Error('LogStore and Project must both exist');
-  } 
+  }
 
   await makeService({
     serviceName,
@@ -187,7 +188,7 @@ async function deployFcService(serviceName, serviceDefinition) {
     logConfig,
     vpcConfig
   });
-  
+
   await deployFunctions(serviceName, serviceDefinition);
 }
 
@@ -213,11 +214,30 @@ async function deployLogstore(projectName, logstoreDefinition) {
 
       await deployLogstoreDefaultIndex(projectName, logstoreName);
 
-      console.log(green(`\tlog serivce logstore ${logstoreName} deploy success` ));
+      console.log(green(`\tlog serivce logstore ${logstoreName} deploy success`));
     }
   }
 }
 
+async function deployCustomDomain(domainName, domainDefinition) {
+  const properties = (domainDefinition.Properties || {});
+  const protocol = properties.Protocol;
+  const tplRouteConfig = properties.RouteConfig.routes;
+  var routes = [];
+  var transRoute = Object.entries(tplRouteConfig);
+  for (var i of transRoute) {
+    i[1].path = i[0];
+    routes.push(i[1]);
+  }
+  var routeConfig = {};
+  routeConfig.routes = routes;
+
+  await makeCustomDomain({
+    domainName,
+    protocol,
+    routeConfig,
+  });
+}
 
 
 async function deployLogs(resourcesDefinition) {
@@ -228,20 +248,20 @@ async function deployLogs(resourcesDefinition) {
 
       const description = properties.Description || '';
       await makeSlsProject(projectName, description);
-      
+
       await deployLogstore(projectName, v);
-      console.log(green(`log serivce project ${projectName} deploy success\n` ));
+      console.log(green(`log serivce project ${projectName} deploy success\n`));
     }
   }
 }
 
-async function deployTablestore(instanceName, resourceDefinition) {  
+async function deployTablestore(instanceName, resourceDefinition) {
   const properties = (resourceDefinition || {}).Properties;
   if (properties) {
     const clusterType = (properties || {}).ClusterType;
     const description = (properties || {}).Description;
     await makeOtsInstance(instanceName, clusterType, description);
-  } 
+  }
 
   for (const [k, v] of Object.entries(resourceDefinition)) {
     if ((v || {}).Type === 'Aliyun::Serverless::TableStore::Table') {
@@ -308,7 +328,7 @@ async function deployApigateway(name, { apiDefinition, template, tplPath }) {
   if (swaggerContent) {
     for (const [k, v] of Object.entries(swaggerContent)) {
       if (!['openapi', 'info', 'components'].includes(k)) {
-        
+
         const apiGroup = await makeGroup({
           name,
           description: `api group for function compute`
@@ -317,12 +337,12 @@ async function deployApigateway(name, { apiDefinition, template, tplPath }) {
           const fcDefinition = methodDefinition['x-aliyun-apigateway-fc'];
 
           let roleName;
-          if ( fcDefinition.role ) {
+          if (fcDefinition.role) {
             roleName = extractFcRole(fcDefinition.role);
           } else {
             roleName = `AliyunFcGeneratedApiGatewayRole`;
           }
-          
+
           const role = await makeRole(roleName, true, 'API Gateway access FunctionCompute', {
             'Statement': [
               {
@@ -346,7 +366,7 @@ async function deployApigateway(name, { apiDefinition, template, tplPath }) {
           const apiName = methodDefinition['x-aliyun-apigateway-api-name'] || `${k.replace(/^\//, '').replace(/(\[|\])/g, '').replace(/\//g, '_')}_${method}`;
 
           const { serviceName, functionName } = extractFcArn(fcDefinition.arn);
-          
+
           const serviceTimeout = fcDefinition.timeout || 3000;
 
           const resultConfig = {
@@ -381,7 +401,7 @@ async function deployApigateway(name, { apiDefinition, template, tplPath }) {
             visibility: methodDefinition['x-aliyun-apigateway-visibility'],
             requestConfig,
             resultConfig,
-            constantParameters, 
+            constantParameters,
             description: methodDefinition['x-aliyun-apigateway-description']
           });
         }
@@ -408,7 +428,7 @@ async function deploy(tplPath) {
     if (resource.Type === 'Aliyun::Serverless::Service') {
       console.log(`Waiting for service ${name} to be deployed...`);
       await deployFcService(name, resource);
-      console.log(green(`service ${name} deploy success\n` ));
+      console.log(green(`service ${name} deploy success\n`));
     } else if (resource.Type === 'Aliyun::Serverless::Api') {
       console.log(`Waiting for api gateway ${name} to be deployed...`);
       await deployApigateway(name, {
@@ -416,13 +436,17 @@ async function deploy(tplPath) {
         template: tpl,
         tplPath
       });
-      console.log(green(`api gateway ${name} deploy success\n` ));
+      console.log(green(`api gateway ${name} deploy success\n`));
     } else if (resource.Type === 'Aliyun::Serverless::TableStore') {
       console.log(`Waiting for table store ${name} to be deployed...`);
       await deployTablestore(name, resource);
-      console.log(green(`table store ${name} deploy success\n` ));
+      console.log(green(`table store ${name} deploy success\n`));
     } else if (resource.Type === 'Aliyun::Serverless::Log') {
       // ignore, done by deployLogs
+    } else if (resource.Type === 'Aliyun::Serverless::CustomDomain') {
+      console.log(`Waiting for custom domain ${name} to be deployed...`);
+      await deployCustomDomain(name, resource);
+      console.log(green(`custom domain ${name} deploy success\n`));
     } else {
       console.log('unknown resource %s', name);
     }
diff --git a/lib/deploy/deploy-support.js b/lib/deploy/deploy-support.js
index ccb3805..bb68c3b 100644
--- a/lib/deploy/deploy-support.js
+++ b/lib/deploy/deploy-support.js
@@ -522,6 +522,50 @@ async function makeTrigger({
   return trigger;
 }
 
+
+async function makeCustomDomain({domainName, 
+  protocol, 
+  routeConfig
+}) {
+  const fc = await getFcClient();
+  var customDomain;
+  await promiseRetry(async (retry, times) => {
+    try {
+      customDomain = await fc.getCustomDomain(domainName);
+    } catch (ex) {
+      if (ex.code !== 'DomainNameNotFound') {
+        console.log(red(`\tretry ${times} times`));
+        retry(ex);
+      }
+    }
+  }, retryOptions);
+
+  const options = {
+    protocol, 
+  };
+
+  if ( routeConfig ) {
+    Object.assign(options, {
+      routeConfig
+    });
+  }
+
+  await promiseRetry(async (retry, times) => {
+    try {
+      if (!customDomain) {
+        customDomain = await fc.createCustomDomain(domainName, options);
+      } else {
+        customDomain = await fc.updateCustomDomain(domainName, options);
+      }
+    } catch (ex) {
+      console.log(red(`\tretry ${times} times`));
+      retry(ex);
+    }
+  }, retryOptions);
+
+  return customDomain;
+}
+
 async function makeGroup(group) {
   const ag = await getCloudApiClient();
 
@@ -849,5 +893,5 @@ module.exports = {
   makeApi, makeApiTrigger, makeFunction,
   makeGroup, makeOtsTable, makeOtsInstance, 
   makeOtsTrigger, makeService, makeTrigger, makeSlsProject, 
-  makeLogstore, makeLogstoreIndex
+  makeLogstore, makeLogstoreIndex, makeCustomDomain
 };
\ No newline at end of file
diff --git a/lib/validate/schema/custom-domain-resource.js b/lib/validate/schema/custom-domain-resource.js
new file mode 100644
index 0000000..584a424
--- /dev/null
+++ b/lib/validate/schema/custom-domain-resource.js
@@ -0,0 +1,39 @@
+'use strict';
+
+const customDomainResourceSchema = {
+  '$id': '/Resources/CustomDomain',
+  'type': 'object',
+  'description': 'CustomDomain',
+  'properties': {
+    'Type': {
+      'type': 'string',
+      'const': 'Aliyun::Serverless::CustomDomain'
+    },
+    'Properties': {
+      'type': 'object',
+      'properties': {
+        'Protocol': {
+          'type': 'string'
+        },
+        'RouteConfig': {
+          'type': 'object',
+          'properties': {
+            'Routes': {
+              'type': 'object',
+              'patternProperties': {
+                '^/': {
+                  'anyOf': [
+                    { '$ref': '/Resources/CustomDomain/PathConfig' }
+                  ]
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  'required': ['Type']
+};
+
+module.exports = customDomainResourceSchema;
\ No newline at end of file
diff --git a/lib/validate/schema/path-config.js b/lib/validate/schema/path-config.js
new file mode 100644
index 0000000..3f6c4f2
--- /dev/null
+++ b/lib/validate/schema/path-config.js
@@ -0,0 +1,24 @@
+'use strict';
+
+const pathConfigResourceSchema = {
+  '$id': '/Resources/CustomDomain/PathConfig',
+  'type': 'object',
+  'properties': {
+    'Type': {
+      'type': 'string'
+    },
+    'Properties': {
+      'type': 'object',
+      'properties': {
+        'ServiceName': {
+          'type': 'string'
+        },
+        'FunctionName': {
+          'type': 'string'
+        }
+      }
+    }
+  }
+};
+
+module.exports = pathConfigResourceSchema;
\ No newline at end of file
diff --git a/lib/validate/schema/ros.js b/lib/validate/schema/ros.js
index afdbef2..34896bb 100644
--- a/lib/validate/schema/ros.js
+++ b/lib/validate/schema/ros.js
@@ -16,12 +16,13 @@ const rosSchema = {
     'Resources': {
       'type': 'object',
       'patternProperties': {
-        '^[a-zA-Z_][a-zA-Z0-9_-]{0,127}$': {
+        '^[a-zA-Z_][a-zA-Z.0-9_-]{0,127}$': {
           anyOf: [
             { '$ref': '/Resources/Service' },
             { '$ref': '/Resources/Api' },
             { '$ref': '/Resources/TableStore' },
-            { '$ref': '/Resources/Log' }
+            { '$ref': '/Resources/Log' },
+            { '$ref': '/Resources/CustomDomain'}
           ]
         }
       }
diff --git a/package-lock.json b/package-lock.json
index 4bf1115..574b034 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "@alicloud/fun",
-  "version": "2.2.1",
+  "version": "2.4.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -24,9 +24,9 @@
       }
     },
     "@alicloud/fc2": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/@alicloud/fc2/-/fc2-2.0.4.tgz",
-      "integrity": "sha512-ESQnZKQewCilIGxBeFbuIngliBjXViTHvQpiMXqVWU9WeEcYlAMTo+SJzosBQAzzQ1BXeRGZc2To6K+hru87tA==",
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@alicloud/fc2/-/fc2-2.0.5.tgz",
+      "integrity": "sha512-ivQJAYAFh6VDG7bavRqFJA+mEUQ3qNxwMnXL8Ln2NFbPR3CJmP5qMxjL54AHRQIUTy05KZWqDPqFnsXHd6NNLg==",
       "requires": {
         "babel-runtime": "6.25.0",
         "debug": "2.6.8",
diff --git a/package.json b/package.json
index 1cffd36..fc073c7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@alicloud/fun",
-  "version": "2.3.0",
+  "version": "2.4.0",
   "description": "(have)Fun with Serverless",
   "engines": {
     "node": ">=8.0.0"
@@ -25,7 +25,7 @@
   "dependencies": {
     "@alicloud/cloudapi": "^1.0.0",
     "@alicloud/fc": "^1.2.3",
-    "@alicloud/fc2": "^2.0.4",
+    "@alicloud/fc2": "^2.0.5",
     "@alicloud/log": "^1.0.0",
     "@alicloud/ots2": "^2.0.0-beta1",
     "@alicloud/pop-core": "^1.6.0",


Test patch:
diff --git a/test/deploy/deploy-by-tpl.test.js b/test/deploy/deploy-by-tpl.test.js
index e389f8a..456fd52 100644
--- a/test/deploy/deploy-by-tpl.test.js
+++ b/test/deploy/deploy-by-tpl.test.js
@@ -456,4 +456,28 @@ describe('deploy', () => {
       resultConfig: { failResultSample: undefined, resultSample: undefined, resultType: undefined },      serviceTimeout: 3000
     });
   });
+
+  it('deploy custom_domain', async () => {
+    await deploy('custom_domain');
+
+    assert.calledWith(deploySupport.makeCustomDomain, {
+      domainName: 'fun.cn-shanghai.1221968287646227.cname-test.fc.aliyun-inc.com',
+      protocol: 'HTTP',
+      routeConfig: {
+        routes: [
+          {
+            path: '/a',
+            serviceName: 'serviceA',
+            functionName: 'functionA',
+          },
+          {
+            path: '/b',
+            serviceName: 'serviceB',
+            functionName: 'functionB',
+          },
+        ]
+      }
+    });
+  });
+
 });