Skip to Content
Technical Articles

Jenkins Build Process with SAPUI5 through HANA XSA

In this blog, we will build the NodesJS and SAPUI5 app in SAP HANA XSA that controls the Jenkins build process.

What’s Jenkins ?

Jenkins (jenkins.io) offers a simple way to set up a continuous integration or continuous delivery environment for almost any combination of languages and source code repositories using pipelines, as well as automating other routine development tasks.

Prerequisites

High Level Steps

  • Configure the Jenkins job to execute the Windows batch command to run the Python program. For simplicity, Windows batch file and Python app reside in the same Jenkins server (this is may not practical in the Production mode).
  • Install and configure NodeJS Client for Jenkins in SAP HANA XSA and build the NodeJS app to call the REST API to build, get and stop the Jenkins job.
  • Lastly, we will build the SAPUI5 Front End in SAP HANA XSA to trigger job, check job status and cancel job.

 

Configure Jenkins

  • Go to Jenkins main dashboard and click on New Item.
  • Enter item name trigger and select Freestyle project. Click OK to continue.
  • Under Build, add build step Execute Windows batch command.
  • Specify the command. Here, I would like to execute run.bat in folder C:\MyPython\.
  • Click Save to finish.
  • Go back to main dashboard and you will see the item “trigger” the that we just created.

SAP HANA XSA

Create Project in SAP Web IDE

  • Logon to SAP Web IDE for SAP HANA XSA. Create a new project in SAP Web IDE for HANA XSA and select SAP Cloud Platform Business Application. Click Next to continue.
  • Give the project name, for example zjenkinauto. Click Next to continue.
  • On this page, just click Next to continue.
  • We need the NodeJS module but we will add it later on and we don’t need the database module. Select Not Included for both Service and Database. Click Next to continue.
  • Click Finish to complete the setup.

Adding NodeJS Module

  • Now on the project that we just created, do a right-click and select New > Node.js Module.
  • Give a module name srv. Click Next to continue.
  • On this page, just click Next to continue.
  • Click Finish to complete.
  • We need to update the code in server.js in srv folder.

    Update with the following code:

    /*eslint no-console: 0, no-unused-vars: 0, no-undef:0, no-process-exit:0*/
    /*eslint-env node, es6 */
    "use strict";
    const port = process.env.PORT || 3000;
    const server = require("http").createServer();
    
    const cds = require("@sap/cds");
    //Initialize Express App for XSA UAA and HDBEXT Middleware
    const xsenv = require("@sap/xsenv");
    const passport = require("passport");
    const xssec = require("@sap/xssec");
    const xsHDBConn = require("@sap/hdbext");
    const express = require("express");
    global.__base = __dirname + "/";
    
    //logging
    var logging = require("@sap/logging");
    var appContext = logging.createAppContext();
    
    //Initialize Express App for XS UAA and HDBEXT Middleware
    var app = express();
    
    //Compression
    app.use(require("compression")({
      threshold: "1b"
    }));
    
    //Helmet for Security Policy Headers
    const helmet = require("helmet");
    // ...
    app.use(helmet());
    app.use(helmet.contentSecurityPolicy({
      directives: {
        defaultSrc: ["'self'"],
        styleSrc: ["'self'", "sapui5.hana.ondemand.com"],
        scriptSrc: ["'self'", "sapui5.hana.ondemand.com"]
      }
    }));
    // Sets "Referrer-Policy: no-referrer".
    app.use(helmet.referrerPolicy({ policy: "no-referrer" }));
    
    passport.use("JWT", new xssec.JWTStrategy(xsenv.getServices({
    	uaa: {
    		tag: "xsuaa"
    	}
    }).uaa));
    app.use(logging.middleware({
    	appContext: appContext,
    	logNetwork: true
    }));
    app.use(passport.initialize());
    app.use(
    	passport.authenticate("JWT", {
    		session: false
    	})
    );
    
    // Redirect any to service root
    app.get("/", (req, res) => {
    	res.send("Jenkins with UI5");
    });
    
    //Setup Additonal Node.js Routes
    require("./router")(app, server);
    
    //Start the Server 
    server.on("request", app);
    server.listen(port, function () {
    	console.info(`HTTP Server: ${server.address().port}`);
    });​
  • Also open package.json in srv folder.

    And update with the following code:

    {
        "name": "serve",
        "description": "Generated from ../package.json, do not change!",
        "version": "1.0.0",
        "dependencies": {
            "@sap/cds": "^3.10.0",
            "express": "^4.17.1",
            "@sap/xssec": "^2.1.17",
    		"@sap/xsenv": "^2.0.0",
            "hdb": "^0.17.0",
            "@sap/hdbext": "^6.0.0",
    		"@sap/hana-client": "^2.4.139",
    		"@sap/textbundle": "latest",
    		"@sap/logging": "^5.0.1",
    		"@sap/audit-logging": "^3.0.0",
    		"nodemailer": "^6.2.1",
    		"passport": "~0.4.0",
    		"async": "^3.0.1",
    		"ws": "^7.0.0",
    		"accept-language-parser": "latest",
    		"node-xlsx": "^0.15.0",
    		"node-zip": "~1.1.1",
    		"xmldoc": "~1.1.2",
    		"winston": "^3.2.1",
    		"body-parser": "^1.19.0",
    		"elementtree": "latest",
    		"then-request": "latest",
    		"compression": "~1.7",
    		"helmet": "^3.18.0",
    		"jenkins": "^0.27.0"
        },
        "engines": {
            "node": "^8.9",
            "npm": "^6"
        },
        "devDependencies": {},
        "scripts": {
        	"postinstall": "cds build/all --project .. --clean",
    		"start": "node server.js"
        },
        "i18n": {
    		"folders": [
    			"_i18n"
    		]
    	},
    	"cds": {
    		"data": {
    			"driver": "hana"
    		}
    	}
    }​

    We added the NodeJS client for Jenkins from https://www.npmjs.com/package/jenkins in dependencies section: “jenkins”: “^0.27.0”.

  • Create a folder in srv folder called router.
  • Create index.js in folder router.

    Insert the following code:

    /*eslint-env node, es6 */
    "use strict";
    
    module.exports = (app, server) => {
    	app.use("/node", require("./routes/myNode")());
    };​
  • Create another folder routes inside router folder.
  • We will create the NodeJS app to trigger build, get job information and stop build.
  • Create myNode.js inside routes folder.

    Insert the following code:

    /*eslint no-console: 0, no-unused-vars: 0, no-shadow: 0, newcap:0*/
    /*eslint-env node, es6 */
    "use strict";
    var express = require("express");
    var async = require("async");
    var jenkinsurl = "**UPDATE WITH YOUR JENKINS URL";
    
    module.exports = function () {
    	var app = express.Router();
    
    	app.get("/triggerJob", (req, res) => {
    		var jenkins = require("jenkins")({
    			baseUrl: jenkinsurl,
    			crumbIssuer: true
    		});
    
    		jenkins.job.build("trigger", function (err, data) {
    			if (err) {
    				res.type("application/json").status(200).send("Error");
    			} else {
    
    				console.log("queue item number:" + data);
    				res.type("application/json").status(200).send('{"queue":' + data + '}');
    			}
    		});
    	});
    
    	app.get("/statusJob", (req, res) => {
    		var jenkins = require("jenkins")({
    			baseUrl: jenkinsurl,
    			crumbIssuer: true
    		});
    
    		jenkins.job.get("trigger", function (err, data) {
    			if (err) {
    				res.type("application/json").status(200).send("Error");
    			} else {
    
    				console.log("build:" + data);
    				res.type("application/json").status(200).send(data);
    			}
    		});
    	});
    	
    	app.get("/cancelJob", (req, res) => {
    		var jobq = req.query.q;
    		console.log(jobq);
    
    		var jenkins = require("jenkins")({
    			baseUrl: jenkinsurl,
    			crumbIssuer: true
    		});
    
    		jenkins.build.stop('trigger', jobq, function(err) {
    			if (err) {
    				res.type("application/json").status(200).send('{"status":error}');
    			} else {
    				res.type("application/json").status(200).send('{"status":ok}');
    			}
    		});
    	});
    
    	return app;
    };​
  • Update the variable jenkinsurl in the above code with your Jenkins server. For example:
    var jenkinsurl = "http://userid:password@10.11.18.134:8080";​

Create Web User (SAPUI5) Interface

  • Do a right click on the project root folder zjenkinauto and select New > Basic HTML5 Module.
  • Give a module name web. Click Next to continue.
  • Click Finish to complete.
  • You will see the web folder. Open xs-app.json in web folder.

    And update with the following code:

    {
    	"welcomeFile": "index.html",
    	"authenticationMethod": "route",
    	"routes": [{
    		"source": "/node(.*)",
    		"destination": "srv_api",
    		"csrfProtection": true,
    		"authenticationType": "xsuaa"
    	}]
    }​
  • Populate the SAP UI5 files in resources folder. Copy from my Git: https://github.com/ferrygun/zjenkinauto
  • In Page.controller.js, we just call the NodeJS service using Ajax query to start, get status and cancel the job.
    /*eslint no-console: 0, no-unused-vars: 0, no-use-before-define: 0, no-redeclare: 0, no-shadow:0*/
    /*eslint-env es6 */
    sap.ui.define(["sap/m/MessageToast", "sap/m/MessageBox", "sap/ui/core/mvc/Controller"],
    	function (MessageToast, MessageBox, Controller) {
    		"use strict";
    
    		function ongetJobStatus(myJSON) {
    			try {
    				var result = JSON.parse(myJSON);
    				console.log(result);
    
    				var lastBuild = result.lastBuild;
    				var lastCompletedBuild = result.lastCompletedBuild;
    				console.log(lastBuild + ":" + lastCompletedBuild);
    				
    				//true = job is not running; false = job is running
    				if(lastBuild === null && lastCompletedBuild === null) {
    					return true;
    				} else if(lastBuild !== null && lastCompletedBuild === null) {
    					return false;
    				} else if(lastBuild !== null && lastCompletedBuild !== null) {
    					lastBuild = result.lastBuild.number;
    					lastCompletedBuild = result.lastCompletedBuild.number;
    				
    
    					if (lastBuild === lastCompletedBuild) {
    						return true;
    					} else {
    						return false;
    					}
    				} else {
    					return true;
    				}
    
    			} catch (e) {
    				return "";
    			}
    		}
    
    		function getJobStatus() {
    			var aUrl = "/node/statusJob";
    
    			return ongetJobStatus(
    				jQuery.ajax({
    					url: aUrl,
    					method: "GET",
    					dataType: "json",
    					async: false
    				}).responseText);
    		}
    
    		function ontriggerJob(myJSON) {
    			try {
    				var result = JSON.parse(myJSON);
    				console.log(result);
    				return (result.queue);
    			} catch (e) {
    				return "";
    			}
    		}
    
    		function triggerJob() {
    			var aUrl = "/node/triggerJob";
    
    			return ontriggerJob(
    				jQuery.ajax({
    					url: aUrl,
    					method: "GET",
    					dataType: "json",
    					async: false
    				}).responseText);
    		}
    
    		function ongetcurrentJobqueue(myJSON) {
    			try {
    				var result = JSON.parse(myJSON);
    				console.log(result);
    
    				var lastBuild = result.lastBuild.number;
    				return lastBuild;
    
    			} catch (e) {
    				return "";
    			}
    		}
    
    		function getcurrentJobqueue() {
    			var aUrl = "/node/statusJob";
    
    			return ongetcurrentJobqueue(
    				jQuery.ajax({
    					url: aUrl,
    					method: "GET",
    					dataType: "json",
    					async: false
    				}).responseText);
    		}
    
    		function oncancelJob(myJSON) {
    			try {
    				var result = JSON.parse(myJSON);
    				console.log(result);
    
    				return (result.status);
    
    			} catch (e) {
    				return "";
    			}
    		}
    
    		function cancelJob(jobqueue) {
    			var aUrl = "/node/cancelJob?q=" + jobqueue;
    
    			return oncancelJob(
    				jQuery.ajax({
    					url: aUrl,
    					method: "GET",
    					dataType: "json",
    					async: false
    				}).responseText);
    		}
    
    		var PageController = Controller.extend("sap.m.sample.Button.Page", {
    
    			onPress_triggerjob: function (evt) {
    
    				let jobstatus = getJobStatus();
    				console.log(jobstatus);
    				if (jobstatus) {
    
    					let jobqueuenumber = triggerJob();
    					console.log(jobqueuenumber);
    					MessageBox.information("Job number:" + jobqueuenumber, {
    						onClose: function (oAction) {
    							if (oAction === sap.m.MessageBox.Action.OK) {
    								console.log("OK");
    							}
    						}
    					});
    				} else {
    					MessageBox.information("Please wait. Job is still running", {
    						onClose: function (oAction) {
    							if (oAction === sap.m.MessageBox.Action.OK) {
    								console.log("OK");
    							}
    						}
    					});
    				}
    			},
    
    			onPress_checkjob: function (evt) {
    
    				let jobstatus = getJobStatus();
    				console.log(jobstatus);
    				if (jobstatus) {
    					MessageBox.information("Job is not running", {
    						onClose: function (oAction) {
    							if (oAction === sap.m.MessageBox.Action.OK) {
    								console.log("OK");
    							}
    						}
    					});
    				} else {
    					MessageBox.information("Job is still running", {
    						onClose: function (oAction) {
    							if (oAction === sap.m.MessageBox.Action.OK) {
    								console.log("OK");
    							}
    						}
    					});
    				}
    			},
    
    			onPress_canceljob: function (evt) {
    				let jobqueue = getcurrentJobqueue();
    				let status = cancelJob(jobqueue);
    				console.log(status);
    
    				MessageBox.information("Job has been cancelled", {
    					onClose: function (oAction) {
    						if (oAction === sap.m.MessageBox.Action.OK) {
    							console.log("OK");
    						}
    					}
    				});
    			}
    		});
    
    		return PageController;
    
    	});​

Adding xs-security.json and update mta.yaml

  • The last step we need to do is to add the xs-security.json file in the root folder.
    {
    	"xsappname": "zjenkinauto",
    	"scopes": [{
    		"name": "$XSAPPNAME.Display",
    		"description": "display"
    	}, {
    		"name": "$XSAPPNAME.Create",
    		"description": "create"
    	}, {
    		"name": "$XSAPPNAME.Edit",
    		"description": "edit"
    	}, {
    		"name": "$XSAPPNAME.Delete",
    		"description": "delete"
    	}, {
    		"name": "$XSAPPNAME.DataGenerator",
    		"description": "data generator"
    	}, {
    		"name": "xs_authorization.read",
    		"description": "Read authorization information from UAA"
    	}, {
    		"name": "xs_authorization.write",
    		"description": "Write authorization information to UAA"
    	}, {
    		"name": "$XSAPPNAME.ODATASERVICEUSER",
    		"description": "Enter"
    	}, {
    		"name": "$XSAPPNAME.ODATASERVICEADMIN",
    		"description": "Enter"
    	}],
    	"attributes": [{
    		"name": "client",
    		"description": "Session Client",
    		"valueType": "int"
    	}, {
    		"name": "country",
    		"description": "country",
    		"valueType": "s"
    	}],
    	"role-templates": [{
    		"name": "Viewer",
    		"description": "View all records",
    		"scope-references": [
    			"$XSAPPNAME.Display"
    		],
    		"attribute-references": [
    			"client", "country"
    		]
    	}, {
    		"name": "Editor",
    		"description": "Edit and Delete records",
    		"scope-references": [
    			"$XSAPPNAME.Create",
    			"$XSAPPNAME.Edit",
    			"$XSAPPNAME.Delete",
    			"$XSAPPNAME.Display",
    			"$XSAPPNAME.DataGenerator",
    			"$XSAPPNAME.ODATASERVICEUSER",
    			"$XSAPPNAME.ODATASERVICEADMIN"
    		],
    		"attribute-references": [
    			"client"
    		]
    	}]
    }​
  • And also to modify the mta.yaml with this code:
    ID: zjenkinauto
    _schema-version: '2.1'
    version: 0.0.1
    
    modules:
     - name: srv
       type: nodejs
       path: srv
       parameters:
         memory: 512M
         disk-quota: 256M
       provides:
         - name: srv_api
           properties:
             url: '${default-url}'
       requires:
         - name: zjenkinauto-uaa
    
     - name: web
       type: html5
       path: web
       requires:
         - name: zearnpfe-uaa
         - name: srv_api
           group: destinations
           properties:
             name: srv_api
             url: '~{url}'
             forwardAuthToken: true
             
    resources:
      - name: zjenkinauto-uaa
        type: com.sap.xs.uaa-space
        parameters:
          config-path: ./xs-security.json
    
    ​

Execution

We have completed the necessary setup and now it’s time for the testing and execution.

  • Click on Trigger Job to submit job.
  • If there is no error, you will see the job queue number.
  • Now go to Jenkins console and you will see the job is being executed.
  • Click Check Job to get the job status.
  • If the job is complete or not running, you will see this message.
  • Click Cancel Job to cancel the job that has been submitted.
  • Check the job status in Jenkins console.

 

1 Comment
You must be Logged on to comment or reply to a post.
  • Pretty good one, thank you for that.

    Would it be also possible to connect NodeJS service to some MongoDB instance running on SCP and keep it secure?