Skip to Content
Technical Articles
Author's profile photo Florian Pfeffer

Deploy UI5 apps to ABAP On-Premise systems via a SAP BTP Cloud Foundry proxy app from a Bitbucket Cloud Pipeline

Recently I searched for an option to deploy UI5 applications to an ABAP On-Premise system from a Bitbucket Pipline running in Cloud. As (at time of writing this) Bitbucket does not provide self-hosted agents which can be excuted in the corporate network, able to connect to the Bitbucket Cloud infrastrcuture and to the internal ABAP system, I had to search for another way to reach my goal for the moment. In the future Bitbucket will provide the option for self-hosted Pipeline Runners which will make the following described approach obsolete (for further information please have a look to Bitbucket Pipelines Runners Early Access Program EAP (atlassian.com)). And if someone says now, hey you can expose your ABAP On-Premise system to the external network and do an IP filtering on the IPs of the Bitbucket Pipeline Cloud infrastructure (there is a list of specific IPs), I say: No, that is from a security point of view a no-go for productive scenarios as everyone which is able to use that shared Bitbucket Pipeline Runner infrastructure in the Cloud could potentially reach the exposed ABAP On-Premise system.

So, what is my approach to reach my goal? I thought I have a secure connection of my ABAP On-Premise system to SAP Business Technology Platform using SAP Cloud Connector already. Let’s use this. But how can I make use of that connection from my Bitbucket environment? For that I need to expose an OAuth secured API which internally proxies my UI5 application deployment requests to the ABAP On-Premise system.

Overview

Following you can see a small overview of my approach and the included components and services:

  • In the Bitbucket Cloud Infrastructure I have the UI5 application repository. This repository uses the UI5 Tooling for building the application sources and the ui5-nwabap-deployer-cli tooling for doing the deployment stuff.
  • On SAP BTP a Cloud Foundry Node.js service is running which provides a /deploy endpoint. The implementation of the /deploy endpoint proxies the requests to the ABAP On-Premise system with the help of the SAP BTP Connectivity and the SAP BTP Destination Service. In my approach the credentials for the ABAP On-Premise system are stored in the destination pointing to that system. The consumer of the /deploy endpoint has to determine upfront an access token from the bound XSUAA service. The token is determined using client credentials provided by a service key created on the XSUAA service.

 

Proxy application running on SAP BTP Cloud Foundry

The proxy application is realized as an MTA application having one Node.js module and service definitions for the XSUAA, Connectivity and Destination service. All sources can be seen here: pfefferf / fpf-cf-sap-onpremise-proxy / packages / proxy — Bitbucket, therefore I will just describe the most interesting coding.

The MTA Deployment Descriptor:

_schema-version: '3.1'
ID: fpf-cf-sap-onpremise-proxy
description: On-Premise Proxy
version: 1.0.0
modules:
  - name: fpf-cf-sap-onpremise-proxy-api
    type: nodejs
    path: proxy-api
    parameters:
      memory: 128M
      disk-quota: 256M
    requires:
      - name: fpf-cf-sap-onpremise-proxy-uaa
      - name: fpf-cf-sap-onpremise-proxy-conn
      - name: fpf-cf-sap-onpremise-proxy-dest
        
resources:
  - name: fpf-cf-sap-onpremise-proxy-uaa
    type: org.cloudfoundry.managed-service
    parameters:
      service: xsuaa
      service-plan: application  
      path: ./xs-security.json
  - name: fpf-cf-sap-onpremise-proxy-conn
    type: org.cloudfoundry.managed-service
    parameters:
      service: connectivity
      service-plan: lite    
  - name: fpf-cf-sap-onpremise-proxy-dest
    type: org.cloudfoundry.managed-service
    parameters:
      service: destination
      service-plan: lite

The descriptor shows the Node.js module and the description of the used services bound to the application.

 

The server.js file:

const express = require("express");
const passport = require("passport");
const xssec = require("@sap/xssec"); 
const xsenv = require("@sap/xsenv");
const cfenv = require("cfenv");
const axios = require("axios");

const fetchToken = require("./lib/fetchToken");
const readDestination = require("./lib/readDestination");

const app = express();
const port = process.env.PORT || 3001;

passport.use("JWT", new xssec.JWTStrategy(xsenv.getServices({
	uaa: {
		tag: "xsuaa"
	}
}).uaa));

app.use(passport.initialize());
app.use(passport.authenticate("JWT", { session: false }));

app.use(express.raw({ limit: "20MB", type: "*/*" }));

app.use("/info", (req, res) => {
  res.json({
    "description": "fpf-cf-sap-onpremise-proxy",
    "version": "1.0.0"
  });
});

app.use("/deploy/*", async (req, res) => {
  try{
    const urlParts = req.originalUrl.split("/");
    urlParts.shift(); // remove empty element
    urlParts.shift(); // remove deploy
    const destinationName = urlParts.shift();
    const targetUrl = "/" + urlParts.join("/");

    const destCredentials = cfenv.getAppEnv().getService("fpf-cf-sap-onpremise-proxy-dest").credentials;
    const connCredentials = cfenv.getAppEnv().getService("fpf-cf-sap-onpremise-proxy-conn").credentials;    
    const destToken = await fetchToken(destCredentials.url, destCredentials.clientid, destCredentials.clientsecret);
    const destination = await readDestination(destinationName, destCredentials.uri, destToken);
    const connToken = await fetchToken(connCredentials.url, connCredentials.clientid, connCredentials.clientsecret);    
    
    const reqOptions = {
      method: req.method,
      url: destination.destinationConfiguration.URL + targetUrl,
      headers: {    
        "Proxy-Authorization": "Bearer " + connToken
      },
      proxy: {
        host: connCredentials.onpremise_proxy_host,
        port: connCredentials.onpremise_proxy_http_port
      },
      maxBodyLength: Infinity,
      maxContentLength: Infinity
    };

    reqOptions.headers["authorization"] = `${destination.authTokens[0].type} ${destination.authTokens[0].value}`; // pre-condition: destination with credentials

    ["accept", "x-csrf-token", "content-type", "cookie", "accept-language", "if-match"].forEach((headerIdentifier) => {
      if(req.headers[headerIdentifier]) {
        reqOptions.headers[headerIdentifier] = req.headers[headerIdentifier];
      }
    });

    if(req.method !== "GET" && req.method !== "DELETE" && req.body) {
      reqOptions.data = Buffer.from(req.body).toString("utf-8");
    }

    reqOptions.validateStatus = (status) => {
      return status < 999;
    };

    const resOnPrem = await axios(reqOptions); 
    res.status(resOnPrem.status).set(resOnPrem.headers).send(resOnPrem.data);
  } catch (error) {
    console.log(error);
    res.status(500).send(error);
  }
});

app.listen(port, () => {
  console.log(`Proxy listening on port ${port}`);
});

The server.js file contains a simple implementation of an express server which uses passport middleware for token validation. The express server provides an /info endpoint which can be used to get some metadata of the running application.

But the main part is the /deploy endpoint which forwards the requests to the ABAP On-Premise system. Which ABAP On-Premise system should be connected, is part of the URL in form of the destinaton name. In case the deployment should be done to a system called A4H which is represnted by destination deva4h, the URL to be called by the deployer is /deploy/deva4h.

Internally the helper functions fetchToken and readDestination (see following code snippets) are used to fetch tokens to access the Connectivity and Destination service and to read information for a destination. From the Destination service the information is required which URL needs to be called to reach the destination endpoint and which credentials are necessary. From the Connectivity service the proxy URL, port and credentials are required which have to be used to reach the URL defined by the destination. Endpoints and further relevant information (like credentials) are taken from the VCAP_SERVICES environment variable.

Function to fetch token from an OAuth Endpoint (+ a simple token cache to avoid getting new tokens everytime the /deploy endpoint is called):

const axios = require("axios");

const tokenCache = [];

module.exports = async (oauthUrl, oauthClient, oauthSecret) => {
  const tokenIdx = tokenCache.findIndex((cacheEntry) => {
    return cacheEntry.oauthUrl === oauthUrl && cacheEntry.oauthClient === oauthClient;
  })

  if(tokenIdx !== -1) {
    const cachedToken = tokenCache[tokenIdx];
    if(Math.abs((new Date().getTime() - cachedToken.created.getTime()) / 1000) > 180) {
      tokenCache.splice(tokenIdx, 1);
    } else {
      return cachedToken.token;
    }
  }

  const tokenUrl = oauthUrl + "/oauth/token?grant_type=client_credentials&response_type=token";
  const config = {
     headers: {
        Authorization: "Basic " + Buffer.from(oauthClient + ':' + oauthSecret).toString("base64")
     }
  };
  const res = await axios.get(tokenUrl, config);
  const access_token = res.data.access_token;
  
  tokenCache.push({
    token: access_token,
    oauthUrl: oauthUrl,
    oauthClient: oauthClient,
    created: new Date()
  });
  
  return access_token;
}  

Function to read destination information:

const axios = require("axios");

module.exports = async (destinationName, destUri, token) => {
  const destSrvUrl =
    destUri + "/destination-configuration/v1/destinations/" + destinationName;
  const config = {
    headers: {
      Authorization: "Bearer " + token,
    },
  };
  const res = await axios.get(destSrvUrl, config);
  return res.data;
};

 

The application can be built using the Cloud MTA Build Tool (sap.github.io) and deployed using the Cloud Foundry tooling. Relevant scripts are included in the package.json on the root of the MTA application (check the repository above).

UI5 Deployment Configuration

In the UI5 application respository a deployment configuration file (e.g. .ui5deployrc), required for the ui5-nwabap-deployer-cli tooling, is created like following. The most important part is the setting of the server information. It needs to be set to the URL of the proxy application running on SAP BTP Cloud Foundry + /deploy/<name of destination>.

{
  "cwd": "./dist",
  "files": "**/*.*",
  "server": "https://e58fa57dtrial-dev-fpf-cf-sap-onpremise-proxy-api.cfapps.eu10.hana.ondemand.com/deploy/deva4h",
  "client": "001",
  "language": "EN",
  "package": "ZZ_UI5_REPOSITORY",
  "bspContainer": "ZZ_UI5_TEST_BB",
  "bspContainerText": "Test UI5 Deployment from Bitbucket",
  "createTransport": true,
  "transportText": "Test Transport Deployment from Bitbucket",
  "transportUseLocked": true,
  "calculateApplicationIndex": true
}

In the package.json of the UI5 application repository the ui5-nwabap-deployer-cli is defined as development dependency + the following NPM script is defined; the script is only synthetic sugar to avoid writing the command in the pipeline itself:

"scripts": {
  "deploy": "ui5-deployer deploy --config .ui5deployrc"
}

Bitbucket Pipeline

The final piece is the Bitbucket Pipeline itself. The following example is restriced to show only the build and deployment step.

image: node:14

pipelines:
  branches:
    master:
  #   deployments steps
      - step:
          name: 'Deployment to ABAP development system'
          deployment: abap_dev
          script:
            - apt-get update
            - apt-get install -y jq          
            - cd packages/ui5-test-app
            - npm ci
            - cf_token=$(curl -X POST https://e58fa57dtrial.authentication.eu10.hana.ondemand.com/oauth/token -H 'application/x-www-form-urlencoded' -d "client_id=$cf_clientid" -d "client_secret=$cf_clientsecret" -d 'grant_type=client_credentials' | jq -r '.access_token')
            - npm run build
            - npm run deploy -- --bearerToken $cf_token

The pipeline uses as base a Node 14 image. For more efficiency a self-built image could be uses which comes out of the box with relevant command line toolings like e.g. jq which is installed manually by the pipeline each time it is executed.

What steps the pipeline do:

  • It installs the additional required tooling jq, to be able to easily extract the access token from the XSUAA OAuth token endpoint call (apt-get install -y jq).
  • It installs all npm dependencies (npm ci).
  • It determines the an access token from the XSUAA OAuth token endpoint using a CURL command. The access token is stored in a variable cf_token. The client id and client secret to access the OAuth endpoint are determined from a service key created for the XSUAA service bound the proxy application. The client id and secret are stored as secured variables in the Bitbucket pipeline.
  • The UI5 sources are build using the “npm run build” command which uses the UI5 Tooling.
  • The built UI5 sources are deployed using the command “npm run deploy”. The NPM script gets the access token determined before to be able to call the proxy application endpoint.

In that demo use case the pipeline is triggered everything something is commited to the master branch.

If that happens the pipeline starts and if everything works fine the UI5 application is deployed from the Bitbucket Cloud Pipeline infrastructure via SCP BTP CF to the via SAP Cloud Connector connected ABAP On-Premise system.

 

Hope this was interesting for you. And keep in mind that this approach is obsolete as soon as the Bitbucket Pipeline Runners are ready for execution in the corporate network.

The demo application can be installed and used using SAP BTP Trial + Bitbucket free infrastructure.

 

Assigned tags

      5 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Gregor Wolf
      Gregor Wolf

      Hi Florian,

      I haven't found a way to file an issue to your BitBucket Project. So I add my comment here. I would suggest to switch over to the getDestination method of the SAP Cloud SDK. It hides all the complexity to get the tokens for the on premise connectivity. Check out how we used it in ts/proxy/index.ts#L89.

      Best regards
      Gregor

      Author's profile photo Florian Pfeffer
      Florian Pfeffer
      Blog Post Author

      Good point. Thanks for making me aware of that.

      Author's profile photo Christoph Szymanski
      Christoph Szymanski

      Good blog Florian,

      Soon you can do that with the SAP Continuous Integration and Delivery service on BTP:

      Roadmap Explorer

       

      Christoph

      Author's profile photo Min Zeng
      Min Zeng

      Dear Florian

      Do you know how to open the existing SAP UI5 application(e.g. BPC4WEBCLIENT) on the bw4 system using VS code?  Many thanks.

       

      Regards

      Arthur Lappin

      Author's profile photo Florian Pfeffer
      Florian Pfeffer
      Blog Post Author

      Not sure what you mean exactly and how it is related to this blog post. Maybe you want to ask your question with a lot more details in the regular Q&A section of that community.