diff --git a/.github/workflows/build-images-on-commit.yml b/.github/workflows/build-images-on-commit.yml
index 143ac5c..cf8c606 100644
--- a/.github/workflows/build-images-on-commit.yml
+++ b/.github/workflows/build-images-on-commit.yml
@@ -6,6 +6,7 @@ on:
paths:
- "src/loaders/**"
- "src/services/**"
+ - "src/databases/**"
- ".github/workflows/reusable-build-container-images.yml"
- ".github/workflows/build-images-on-commit.yml"
workflow_dispatch:
diff --git a/.github/workflows/build-images-on-pr.yml b/.github/workflows/build-images-on-pr.yml
index a61373a..48078e2 100644
--- a/.github/workflows/build-images-on-pr.yml
+++ b/.github/workflows/build-images-on-pr.yml
@@ -6,6 +6,7 @@ on:
paths:
- "src/loaders/**"
- "src/services/**"
+ - "src/databases/**"
- ".github/workflows/reusable-build-container-images.yml"
- ".github/workflows/check.yml"
jobs:
diff --git a/.github/workflows/reusable-build-container-images.yml b/.github/workflows/reusable-build-container-images.yml
index b7a91ea..a3fc7b5 100644
--- a/.github/workflows/reusable-build-container-images.yml
+++ b/.github/workflows/reusable-build-container-images.yml
@@ -22,6 +22,10 @@ jobs:
name: loaders-curl
- context: ./src/services/java
name: services-java
+ - context: ./src/services/nodejs
+ name: services-nodejs
+ - context: ./src/databases/mysql
+ name: databases-mysql
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
diff --git a/.gitignore b/.gitignore
index 110f54d..3ab9b5d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,4 @@
-.vscode/*
-!.vscode/settings.json
-!.vscode/tasks.json
-!.vscode/launch.json
-!.vscode/extensions.json
-!.vscode/*.code-snippets
+.vscode/
# Local History for Visual Studio Code
.history/
diff --git a/scripts/build.sh b/scripts/build.sh
index 06ae215..d81b1ac 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -56,6 +56,15 @@ for DIR in "${REPO_DIR}/src/services"/*; do
fi
done
+for DIR in "${REPO_DIR}/src/databases"/*; do
+ if [ -d "$DIR" ]; then
+ IMAGE_TAG="${REPO_PREFIX}/${IMAGE_PREFIX}-databases-$(basename "$DIR"):$VERSION"
+ echo "Building $IMAGE_TAG..."
+ echo "Running 'docker buildx build --platform $PLATFORM -t $IMAGE_TAG $DIR $PUSH'"
+ docker buildx build --platform "$PLATFORM" -t "$IMAGE_TAG" $PUSH "$DIR" $PUSH
+ fi
+done
+
for DIR in "${REPO_DIR}/src/loaders"/*; do
if [ -d "$DIR" ]; then
IMAGE_TAG="${REPO_PREFIX}/${IMAGE_PREFIX}-loaders-$(basename "$DIR"):$VERSION"
@@ -64,3 +73,4 @@ for DIR in "${REPO_DIR}/src/loaders"/*; do
docker buildx build --platform "$PLATFORM" -t "$IMAGE_TAG" $PUSH "$DIR" $PUSH
fi
done
+
diff --git a/src/databases/mysql/Dockerfile b/src/databases/mysql/Dockerfile
new file mode 100644
index 0000000..5760d1e
--- /dev/null
+++ b/src/databases/mysql/Dockerfile
@@ -0,0 +1,9 @@
+FROM mysql:5.7
+
+LABEL org.opencontainers.image.source=https://github.com/cisco-open/app-simulator
+LABEL org.opencontainers.image.description="mysql database for app-simulator"
+LABEL org.opencontainers.image.licenses=BSD-3-Clause
+
+RUN yum install -y php-cli && yum clean all
+COPY setup.php /tmp/
+COPY setup.sh /docker-entrypoint-initdb.d/
diff --git a/src/databases/mysql/setup.php b/src/databases/mysql/setup.php
new file mode 100644
index 0000000..d53ccd7
--- /dev/null
+++ b/src/databases/mysql/setup.php
@@ -0,0 +1,33 @@
+databases as $database => $tables) {
+ $result .= "CREATE DATABASE ".$database.";".PHP_EOL;
+ $result .= "USE ".$database.";".PHP_EOL;
+ foreach($tables as $table => $columns) {
+ $result .="CREATE TABLE ".$table." (".PHP_EOL;
+ foreach($columns as $column) {
+ if($column === 'id') {
+ $result .= " ".$column." INT(6) UNSIGNED AUTO_INCREMENT PRIMARY KEY,".PHP_EOL;
+ } else {
+ $result .= " ".$column. " VARCHAR(255),".PHP_EOL;
+ }
+ }
+
+ $result = substr($result,0,-2).PHP_EOL;
+
+ $result .=') ENGINE=InnoDB;'.PHP_EOL;
+ }
+}
+
+echo '=====.PHP_EOL';
+
+echo $result;
+
+echo '=====.PHP_EOL';
+
+file_put_contents("/tmp/create.sql",$result);
diff --git a/src/databases/mysql/setup.sh b/src/databases/mysql/setup.sh
new file mode 100644
index 0000000..abd0cda
--- /dev/null
+++ b/src/databases/mysql/setup.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+APP_CONFIG="$( { var [k, v] = a.split(':'); c[k] = isNaN(parseInt(v)) ? v : parseInt(v); return c }, {})
+ data.value = chance[fn](attributes)
+ }
+
+ if (!data.value) {
+ reject('Data not processed: No value provided')
+ }
+
+ var value = data.value
+ var id = data.id
+
+ if (Array.isArray(value)) {
+ value = value[Math.floor(Math.random() * value.length)]
+ }
+ reject('No data added: Transaction not found.')
+}
+
+function logMessage(level, message) {
+ if (['trace', 'debug', 'info', 'warn', 'error', 'fatal'].includes(level)) {
+ logger[level](message)
+ } else {
+ logger.info(message)
+ }
+ return 'Logged (' + level + '): ' + message
+}
+
+function executeCustomScript(script, req, resolve, reject) {
+ var r = require(path.join(customCodeDir, script))({
+ logger: logger,
+ req: req,
+ cronmatch: cronmatch,
+ sleep: sleep.msleep,
+ chance: chance
+ })
+ if (r === false) {
+ reject(`Script ${script} was not executed successfully`)
+ } else if (typeof r === "object" && r.hasOwnProperty('code') && r.hasOwnProperty('code')) {
+ reject({ code: r.code, message: r.message })
+ } else if (typeof r === 'string') {
+ resolve(r)
+ } else {
+ resolve(`Script ${script} was executed successfully`)
+ }
+}
+
+function callRemoteService(call, useRp, catchExceptions, remoteTimeout, req, resolve, reject) {
+ if (useRp) {
+ rp.get({
+ uri: url.parse(call),
+ json: true,
+ timeout: remoteTimeout
+ }).then(function (body) {
+ resolve(body)
+ }).catch(function (err) {
+ if (catchExceptions) {
+ resolve(err)
+ } else {
+ reject(err)
+ }
+ })
+ } else {
+ var headers = {
+ 'Content-Type': 'application/json'
+ }
+ // removed logic to decide what happens w/ w/o agent
+ headers = req.headers
+ var opts = Object.assign(url.parse(call), {
+ headers: headers
+ })
+ const r = http.get(opts, function (res, req) {
+ const body = []
+ res.on('data', (chunk) => body.push(chunk))
+ res.on('end', () => resolve(body.join('')))
+ }).on('error', function (err) {
+ if (catchExceptions) {
+ resolve(err)
+ } else {
+ reject(err)
+ }
+ })
+ r.setTimeout(remoteTimeout, function () {
+ reject({ code: 500, message: 'Read timed out' })
+ })
+ }
+}
+
+function processCall(call, req) {
+ return new Promise(function (resolve, reject) {
+ console.log(call)
+ var remoteTimeout = 1073741824
+
+ var catchExceptions = true
+
+ // If call is an array, select one element as call
+ if (Array.isArray(call)) {
+ call = call[Math.floor(Math.random() * call.length)]
+ }
+ // If call is an object, check for probability
+ if (typeof call === 'object') {
+ if (call.hasOwnProperty('probability') && call.probability <= Math.random()) {
+ resolve(`${call.call} was not probable`)
+ return
+ }
+ if (call.hasOwnProperty('schedule') && !cronmatch.match(call.schedule, new Date())) {
+ resolve(`${call.call} was not scheduled`)
+ return
+ }
+ if (call.hasOwnProperty('remoteTimeout')) {
+ remoteTimeout = call.remoteTimeout
+ }
+ if (call.hasOwnProperty('catchExceptions')) {
+ catchExceptions = call.catchExceptions
+ }
+ if (call.hasOwnProperty('call') && call.call === 'data') {
+ return processData(resolve, reject, req, call)
+ }
+ call = call.call
+ }
+ if (call.startsWith('error')) {
+ const [_, code, message] = call.split(',')
+ reject({ code, message })
+ } else if (call.startsWith('sleep')) {
+ const [_, timeout] = call.split(',')
+ setTimeout(function () {
+ resolve(`Slept for ${timeout}`)
+ }, timeout)
+ } else if (call.startsWith('slow')) {
+ const [_, timeout] = call.split(',')
+ resolve(buildResponse(timeout))
+ } else if (call.startsWith('http://')) {
+ callRemoteService(call, useRp, catchExceptions, remoteTimeout, req, resolve, reject)
+ } else if (call.startsWith('image')) {
+ const [_, src] = call.split(',')
+ resolve(`
`)
+ } else if (call.startsWith('script')) {
+ const [_, src] = call.split(',')
+ resolve(``)
+ } else if (call.startsWith('ajax')) {
+ const [_, src] = call.split(',')
+ resolve(``)
+ } else if (call.startsWith('cache')) {
+ const [_, timeout] = call.split(',')
+ resolve(loadFromCache(timeout, txn))
+ } else if (call.startsWith('log')) {
+ const logging = call.split(',')
+ if (logging.length > 2) {
+ resolve(logMessage(logging[1], logging[2]))
+ } else {
+ resolve(logMessage('info', logging[1]))
+ }
+ } else if (call.startsWith('code')) {
+ const [_, script] = call.split(',')
+ executeCustomScript(script, req, resolve, reject)
+ } else {
+ // No other methods are currently implemented
+ resolve(`${call} is not supported`)
+ }
+ })
+}
+
+async function processRequest(req, res, params) {
+ const path = url.parse(req.url).pathname
+ logger.info("Request Headers:", req.headers)
+ if (endpoints.hasOwnProperty(path)) {
+ try {
+ const results = []
+ for (let i = 0; i < endpoints[path].length; i++) {
+ const call = endpoints[path][i]
+ results.push(await processCall(call, req))
+ }
+ var contype = req.headers['content-type']
+
+ if (req.query.output && req.query.output === 'javascript') {
+ res.send(results)
+ } else {
+ res.send(results)
+ }
+ } catch (reason) {
+ logger.error(reason.message)
+ res.status(typeof reason.code === 'number' ? reason.code : 500).send(reason.message)
+ }
+ } else {
+ res.status(404).send('404')
+ }
+}
+
+app.get('/**', function (req, res) {
+ processRequest(req, res, req.query)
+})
+
+app.post('/**', function (req, res) {
+ processRequest(req, res, req.body)
+})
+
+var server = app.listen(port, () => console.log(
+ `Running ${config.name} (type: ${config.type}) on port ${port}`))
+console.log(`Configuration:`)
+console.log(JSON.stringify(config))
+console.log(`Endpoints:`)
+console.log(JSON.stringify(endpoints))
+
+if (config.hasOwnProperty('options')) {
+ server.on('connection', (socket) => {
+ if (config.options.hasOwnProperty('connectionDelay')) {
+ sleep.msleep(config.options.connectionDelay)
+ }
+ if (config.options.hasOwnProperty('lossRate') && parseFloat(config.options.lossRate) >= Math.random()) {
+ socket.end()
+ throw new Error('An error occurred')
+ }
+ })
+}
\ No newline at end of file
diff --git a/src/services/nodejs/package.json b/src/services/nodejs/package.json
new file mode 100644
index 0000000..036bb1d
--- /dev/null
+++ b/src/services/nodejs/package.json
@@ -0,0 +1,21 @@
+{
+ "dependencies": {
+ "chance": "^1.1.3",
+ "cronmatch": "^0.1.1",
+ "express": "^4.17.1",
+ "log4js": "^3.0.6",
+ "morgan": "^1.9.1",
+ "request": "^2.88.2",
+ "request-promise": "^4.2.4",
+ "sleep": "^6.3.0"
+ },
+ "devDependencies": {
+ "eslint": "^6.2.1",
+ "eslint-config-standard": "^14.0.0",
+ "eslint-plugin-import": "^2.18.2",
+ "eslint-plugin-json": "^1.4.0",
+ "eslint-plugin-node": "^9.1.0",
+ "eslint-plugin-promise": "^4.2.1",
+ "eslint-plugin-standard": "^4.0.1"
+ }
+}
diff --git a/src/services/nodejs/run.sh b/src/services/nodejs/run.sh
new file mode 100755
index 0000000..f87865e
--- /dev/null
+++ b/src/services/nodejs/run.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+env CUSTOM_CODE_DIR="./scripts" APP_CONFIG="$(<../examples/frontend.json)" nodemon index.js 8080
diff --git a/src/services/nodejs/scripts/.gitkeep b/src/services/nodejs/scripts/.gitkeep
new file mode 100644
index 0000000..e69de29