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