APS Endpoint Runtime implementation in Node.js
This repository uses git flow with the following config:
[gitflow "branch"]
master = master
develop = dev
[gitflow "prefix"]
feature = feature/
release = release/
hotfix = hotfix/
versiontag =
support = support/
You will need NodeJS for all activities. To quickly install it, you can use these commands:
wget https://nodejs.org/dist/v5.4.0/node-v5.4.0-linux-x64.tar.gz && tar xvf node-v5.4.0-linux-x64.tar.gz && cd node-v5.4.0-linux-x64 && cp -r */ /usr && cd ..
This will install node
along with npm
with /usr
prefix. Use node -v
and npm -v
to check.
Currently, about 80% of insfrastructure and routing is completed. However, only about 5% of user code API and APS functionality is done. If you want to go ahead and test what's working, follow the steps below, and remember, contributions are welcome in all forms (issues, pull requests, etc.).
You need a working OA 6.0 installation with APS. Download the latest RPM from releases section or build it yourself.
Use your RPM to install the runtime on the endpoint host (it needs to have NodeJS of version 5 and above already installed and available in PATH
):
yum install aps-node-runtime-version.rpm
Pay attention to what the terminal says during installation. The output will contain instructions on how to generate TLS credentials for your runtime. What you just installed consists of these parts:
/usr/sbin/aps-node - control script
/usr/share/aps/node/ - home directory
/etc/aps/node - main and endpoints configuration directory, also holds main TLS credentials
/var/aps-node - default endpoint home directory (note that endpoints may be installed anywhere), holds user code and instance and endpoint logs
/var/log/aps-node.log - main daemon log
You can control the runtime daemon with these commands:
aps-node start
aps-node stop
aps-node restart
If you try to start the daemon (control script needs to be run as root), it will display something like this (assuming you have generated the TLS credentials correctly, see above):
[root@endpoint ~]# aps-node start
Starting APS Node.js daemon... [ FAIL ]
Reason: Endpoints directory is empty. Nothing to do!
More information may be available in '/var/log/aps-node.log'.
It will generally tell you what's wrong if it doesn't start. You can check the mentioned log for more info. In this case, endpoint configuration directory is empty (we have not yet created any endpoints, you will learn how to do that below).
You can also check and adjust main daemon configuration file: /etc/aps/node/config.json
.
When you fix all issues that prevent the daemon from starting, you will see something similar to this:
[root@endpoint node]# aps-node start
Starting APS Node.js daemon... [ OK ] PID: 851
0.0.0.0
└─┬ 443
└─┬ (*)
└── /test - 'c72'
More information may be available in '/var/log/aps-node.log'.
That output includes a PID of a newly started daemon as well as location of a main log file (also featured before) and a routing table for all active endpoints (it's a tree that has these levels ip->port->virtualHost->name
and also includes the ID of each endpoint in single quotes). You can check that it's running:
[root@endpoint node]# ps auxf | grep 851
aps-node 851 1.6 0.6 1048000 55656 pts/0 Sl 11:24 0:01 /usr/bin/node daemon.js
Here it is, dropped its privileges and is now running as aps-node
user.
That's it! You can now use this host the same way you did with PHP runtime. You can use VPS Cloud Basic for testing the functionality (it has service named clouds
out of the box).
In PHP runtime, that task was performed by endpoint.sh
script. For this runtime, the task is not yet automated (althoug it will probably be performed by the same aps-node
script in the future).
An endpoint is defined by two assets:
- Configuration file in JSON format
- Home directory that contains user code
Minimal configuration file looks like this:
test.json
{
"services": [
"clouds"
]
}
Placing this file inside the endpoints configuration directory at /etc/aps/node/endpoints
will create an endpoint with name test
(deduced from file name), home inside default directory (full path: /var/aps/node/test
) and all other default options. Upon starting, runtime will look for clouds.js
inside the endpoint home directory to handle the incoming requests for service with ID clouds
.
If something goes wrong, you can always check the main log or the endpoint log (aps/endpoint.log
inside the endpoint gome directory).
The runtime drops privileges after starting and all credentials are switched to aps-node
(user and group). Make sure that all objects inside endpoint home directory (and the directory itself) are readable, writable and executable for that user and group.
More verbose configuration example:
{
"host": "127.0.0.1",
"port": 443,
"virtualHost": "test.com",
"name": "aac",
"home": "folder+1",
"services": {
"aaa": true,
"cloud": "globals.js",
"aac": true,
"clouv": ""
},
"useBabel": true,
"logLevel": "trace",
"dummy": false
}
Full docs about this configuration are coming later™. 😀
You will need to create and place service code files in the endpoint home directory. You will learn how to do that below.
When starting, endpoint will create aps
directory (which contains a log and a type cache that is useless for now). When endpoint receives requests for instance creation, aps
directory will be filling with folders named as IDs (GUIDs) of the newly created instances. These directoriess will contain all the instance assets (instance's own TLS credentials as well as controller's certificate, instance configuration JSON file and instance log).
Service code files are located inside (sometimes not immediately) the endpoint home directory. If everything else goes well, this file is ultimately the one deciding what to do with the request. Service handlers are ES6 (ES2015) compatible, by default, babel
will be used to transpile the file, unless useBabel
is set to false
for that endpoint. A typical file looks like this:
import crypto from 'crypto';
import util from 'util';
// this scope runs during a dry run and on each request
export default class globals {
constructor() {
// this scope runs on each request when resource is being constructed ('this' refers to the resource)
}
provision() {
//these scopes run each time a corresponding operation is performed on the resource
aps.logger.info(util.inspect(this));
this.apphost = new Date();
return new Promise(resolve => {
setTimeout(resolve, 1000);
});
}
configure(newResource) {
this.cloudpass = 'testtest';
}
unprovision() {
aps.logger.info('HELP! They are deleting me!!!1');
return new Promise(resolve => {
setTimeout(resolve, 1000);
});
}
}
You are encouraged to experiment with the service code files to see what they are capable of. Notable features:
- Global object is shared between all service code scopes of the same instance and is not dropped while the runtime is running (use it to share DB connections or other persistent resources).
- Global scope contains
aps
object that will hold the user code API (note: when it's undefined, that means that runtime is executing a so-called 'dry run', it does that once for each service after starting). - Returning
Promise
(or any.then
able) from functions will use its value to decide the request's fate. aps.logger
allows you to write to the correspondinginstance.log
.this
is set to the resource in question (note: no validation is performed onthis
beforeJSON.stringify
ing it and sending back to the controller).require
works properly (uses correct paths), butprocess.cwd
does not. This means that running something likefs.readFile('./test.txt')
will actually attempt to read/usr/share/aps/node/test.txt
instead of a file inside the endpoint home.
Keep in mind that you need to restart the main daemon after changing code because this runtime is persistent and reads all its files only once when starting. The only handles it keeps when running are logs and sockets.
This runtime has a very verbose logging. Adjust log levels in corresponding configs if you have to.
2016-01-11 10:41:28.310 [WARNING][E:c8c] Dummy mode set to: false (default, key: 'dummy' not found)
2016-01-11 10:41:28.310 [INFO][E:c8c] Initialized successfully! Key: '(*)0.0.0.0:443/test'.
2016-01-11 10:41:28.311 [DEBUG][Router] Endpoint host identifier is already an IP address: '0.0.0.0'. No NS lookup needed!
Log messages typically contain date and time, message level, a prefix or several (sometimes with an ID) and the message itself. Most dynamic values will be enclosed in '
if they are strings.
Prefixes help you identify what sent that log message. Prefixes can contain IDs (like endpoint or instance IDs) as well. Those that do are shown here:
E: endpoint
I: instance
R: request
Each log message has a level. The message only gets printed if the destination logger's log level is lower or equal to that of a message:
TRACE - typically used to log dynamic data (file contents, etc.)
DEBUG - emitted when small part of bigger action completes
INFO - more general messages, configuration value changes
WARNING - minor unexpected condition occurred
ERROR - component failure occurred, daemon continues its operation
CRITICAL - emitted only by daemon itself when fatal error occurred, daemon stops
As a rule of thumb, whenever you see a stack trace in a log or somewhere else, a code change will be required to fix it. It may be a change in runtime code or a service handlers. If no stack trace is found, most likely the problem lies within configuration or other input data.
Good luck! 😀
You will need:
- Centos >= 6
- NodeJS >= 5
- NPM that comes with it
- Babel 5.8.34
rpmbuild
command
If you are missing something from this list (except the first item 😀), build/build.sh
script will tell you about it.
Steps:
- Download the latest source ZIP.
unzip dev.zip
cd aps-node-runtime-dev/build
chmod +x build.sh
./build.sh
- If you want to prevent cleanup after build process (e.g. to examine individual stages), run
./build.sh debug
This will download all the necessary packages and make an RPM inisde the build
directory. Use that RPM to install the runtime on your OA endpoint.
Why is your RPM so huge (3.25 MB)?
It contains all npm
dependencies for the runtime so you don't have to download them after installation (since their version is supposed to be frozen anyway). It includes some fat packages like babel
.
Why doesn't feature X work? It works fine in PHP runtime!
This is a prototype. There is only so much you can do with limited development time that is interrupted by ticket processing. Fix/create something yourself and submit that pull request, buddy. 😀 Or at least head over to issues section and tell us what doesn't work (don't forget to include complete information on how to reproduce the issue).
An error has occured in one of my service scripts, but it's stack trace does not match the code I wrote. What's the deal here?
I've installed your runtime and then went to /usr/share/aps/node
to check the JS sources out. The code I saw there is not readable at all. How did you write this? Are you insane?
I'm not insane... no, really. 😆 However, the people behind Babel transpiler ARE insane (and by that I mean insanely smart). This runtime uses Babel extensively to transform code from ES6 to ES5 both inside itself and inside user scripts. If you want to check out code, do it on this repository and not on the already-installed runtime. If you see a discrepancy in stack traces, that means that source map support failed somewhere and you can submit a bug.