From 5932451b0dac38f0823c570666df9aa267c83855 Mon Sep 17 00:00:00 2001 From: Uwe Klinger <48084033+uwe-klinger@users.noreply.github.com> Date: Fri, 15 Jul 2022 19:32:35 +0200 Subject: [PATCH] Helm Chart (#227) * Helm Chart * Add "bash" for Windows users * Improve UI doc * Ignore Helm chart --- .gitignore | 8 + README-Kyma-Runtime.md | 172 ++++++++++++++++++ README.md | 6 +- db/package.json | 9 + deployment/kyma/scripts/build-ui-image.sh | 67 +++++++ .../create-container-registry-secret.sh | 28 +++ deployment/kyma/scripts/create-db-secret.sh | 52 ++++++ deployment/kyma/scripts/format-kyma-secret.js | 9 + deployment/kyma/scripts/prepareUiFiles.js | 166 +++++++++++++++++ deployment/kyma/scripts/value.js | 38 ++++ deployment/kyma/values.yaml | 66 +++++++ srv/pom.xml | 7 +- 12 files changed, 625 insertions(+), 3 deletions(-) create mode 100644 README-Kyma-Runtime.md create mode 100644 db/package.json create mode 100755 deployment/kyma/scripts/build-ui-image.sh create mode 100755 deployment/kyma/scripts/create-container-registry-secret.sh create mode 100755 deployment/kyma/scripts/create-db-secret.sh create mode 100644 deployment/kyma/scripts/format-kyma-secret.js create mode 100644 deployment/kyma/scripts/prepareUiFiles.js create mode 100755 deployment/kyma/scripts/value.js create mode 100644 deployment/kyma/values.yaml diff --git a/.gitignore b/.gitignore index 8f4a45d7..6283bc25 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,11 @@ dist/ .classpath # .vscode/* # IMPORTANT: Do not exclude .vscode please! + +# Local dev +.env +.values.yaml +.cdsrc-private.json + +# Exclude Helm chart for this example +/chart \ No newline at end of file diff --git a/README-Kyma-Runtime.md b/README-Kyma-Runtime.md new file mode 100644 index 00000000..4bc5a447 --- /dev/null +++ b/README-Kyma-Runtime.md @@ -0,0 +1,172 @@ +# Deployment to SAP Business Technology Platform - Kyma Runtime + +- [Deployment to SAP Business Technology Platform - Kyma Runtime](#deployment-to-sap-business-technology-platform---kyma-runtime) + - [Preconditions](#preconditions) + - [Add Deployment Files](#add-deployment-files) + - [Configuration](#configuration) + - [Prepare Kubernetes Namespace](#prepare-kubernetes-namespace) + - [Create container registry secret](#create-container-registry-secret) + - [Create a secret for your HDI container](#create-a-secret-for-your-hdi-container) + - [Build - Node.js](#build---nodejs) + - [Build - Java](#build---java) + - [Build HTML5 application deployer image](#build-html5-application-deployer-image) + - [Push docker images](#push-docker-images) + - [Deployment](#deployment) + - [Access the UI](#access-the-ui) + +**TIP:** You can find more information in the [Deploy Your CAP Application on SAP BTP Kyma Runtime](https://developers.sap.com/mission.btp-deploy-cap-kyma.html) tutorial and in the [Deploy to Kyma/K8s](https://cap.cloud.sap/docs/guides/deployment/deploy-to-kyma) guide of the CAP documentation. + +## Preconditions + +- BTP Subaccount with Kyma Runtime +- BTP Subaccount with Cloud Foundry Space +- HANA Cloud instance available for your Cloud Foundry space +- BTP Entitlements for: *HANA HDI Services & Container* plan *hdi-shared*, *Launchpad Service* plan *standard* +- Container Registry +- Command Line Tools: `kubectl`, `kubectl-oidc_login`, `pack`, `docker`, `helm`, `cf` +- Logged into Kyma Runtime (with `kubectl` CLI), Cloud Foundry space (with `cf` CLI) and Container Registry (with `docker login`) +- `@sap/cds-dk` >= 6.0.1 + +## Add Deployment Files + +CAP tooling provides your a Helm chart for deployment to Kyma. + +Add the CAP Helm chart with the required features to this project: + +```bash +cds add helm +cds add helm:html5_apps_deployer +``` + +## Configuration + +This project contains a pre-configured configuration file `deployment/kyma/values.yaml`, you just need to do the following changes in this file: + +- `` - full-qualified hostname of your container registry +- `domain`- full-qualified domain name used to access applications in your Kyma cluster + +## Prepare Kubernetes Namespace + +1. Export the kubeconfig.yaml + + ``` + set KUBECONFIG=~/.kube/cap-kyma-app-config + ``` + +2. Setting the namespace + + ``` + kubectl config set-context --current --namespace=<> + ``` + +### Create container registry secret + +Create a secret `container-registry` with credentials to access the container registry: + +``` +bash deployment/kyma/scripts/create-container-registry-secret.sh +``` + +The *Docker Server* is the full-qualified hostname of your container registry. + +### Create a secret for your HDI container + +``` +bash deployment/kyma/scripts/create-db-secret.sh sflight-db +``` + +## Build - Node.js + +Do the following steps if you want to deploy the **Node.js** application. + +The `CDS_ENV=node` env variable needs to be provided to build for Node.js. The application will be built for Java by default. + +``` +CDS_ENV=node cds build --production +``` +**Build data base deployer image:** + +``` +pack build $YOUR_CONTAINER_REGISTRY/sflight-hana-deployer \ + --path gen/db \ + --buildpack gcr.io/paketo-buildpacks/nodejs:0.16.1 \ + --builder paketobuildpacks/builder:base +``` +(Replace `$YOUR_CONTAINER_REGISTRY` with the full-qualified hostname of your container registry) + +**Build image for CAP service:** + +``` +pack build \ + $YOUR_CONTAINER_REGISTRY/sflight-srv \ + --path "gen/srv" \ + --buildpack gcr.io/paketo-buildpacks/nodejs \ + --builder paketobuildpacks/builder:base +``` + +## Build - Java + +Do the following steps if you want to deploy the **Java** application. + +**Build data base deployer image:** + +``` +cds build --production +``` + +``` +pack build $YOUR_CONTAINER_REGISTRY/sflight-hana-deployer \ + --path db \ + --buildpack gcr.io/paketo-buildpacks/nodejs:0.16.1 \ + --builder paketobuildpacks/builder:base +``` + +(Replace `$YOUR_CONTAINER_REGISTRY` the full-qualified hostname of your container registry) + +**Build image for CAP service:** + +``` +mvn package +``` + +``` +pack build $YOUR_CONTAINER_REGISTRY/sflight-srv \ + --path srv/target/*-exec.jar \ + --buildpack gcr.io/paketo-buildpacks/sap-machine \ + --buildpack gcr.io/paketo-buildpacks/java \ + --builder paketobuildpacks/builder:base \ + --env SPRING_PROFILES_ACTIVE=cloud +``` + +## Build HTML5 application deployer image + +``` +bash deployment/kyma/scripts/build-ui-image.sh +``` + +## Push docker images + +You can push all the docker images to your docker registry, using: + +``` +docker push $YOUR_CONTAINER_REGISTRY/sflight-hana-deployer +docker push $YOUR_CONTAINER_REGISTRY/sflight-srv +docker push $YOUR_CONTAINER_REGISTRY/sflight-html5-deployer +``` + +## Deployment + +``` +helm upgrade sflight ./chart --install -f deployment/kyma/values.yaml +``` + +## Access the UI + +1. Create Launchpad Service subscription in the BTP Cockpit +2. Create a role collection `sflight` +3. Add role `admin` of `sflight.tXYZ` application to role collection +4. Add your user to the role collection +5. Goto **HTML5 Applications** +6. Start HTML5 application `sapfecaptravel` + +Additionally, you can add the UI to a Launchpad Service site like it is described in in the last two steps of [this tutorial](https://developers.sap.com/tutorials/btp-app-kyma-launchpad-service.html#9aab2dd0-18ea-4ccd-bc44-24e87c845740). diff --git a/README.md b/README.md index 19dfd8a1..529e9f2c 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ The configuration file `mta.yaml` is for the Node.js backend of the app. If you - Create a [trial account on SAP BTP](https://www.sap.com/products/business-technology-platform/trial.html). See this [tutorial](https://developers.sap.com/tutorials/hcp-create-trial-account.html) for more information. Alternatively, you can use a sub-account in a productive environment. - Subscribe to the [SAP Launchpad Service](https://developers.sap.com/tutorials/cp-portal-cloud-foundry-getting-started.html). - Create an [SAP HANA Cloud Service instance](https://developers.sap.com/tutorials/btp-app-hana-cloud-setup.html#08480ec0-ac70-4d47-a759-dc5cb0eb1d58) or use an existing one. - + #### Local Machine - Install the Cloud Foundry command line interface (CLI). See this [tutorial](https://developers.sap.com/tutorials/cp-cf-download-cli.html) for more details. @@ -135,6 +135,10 @@ You need to have access to a HANA Cloud instance and SAP BTP. The running application is now connected to its own HDI container/schema. Please keep in mind that the credentials for that HDI container are stored locally on your filesystem (default-env.json). +## Deployment to SAP Business Technology Platform - Kyma Runtime + +The deployment to Kyma Runtime is explained in file [README-Kyma-Runtime.md](./README-Kyma-Runtime.md). + ## Creating an SAP Fiori App from Scratch If you want to implement an SAP Fiori app, follow these tutorials: diff --git a/db/package.json b/db/package.json new file mode 100644 index 00000000..20ce56ce --- /dev/null +++ b/db/package.json @@ -0,0 +1,9 @@ +{ + "name": "deploy", + "dependencies": { + "@sap/hdi-deploy": "^4" + }, + "scripts": { + "start": "node node_modules/@sap/hdi-deploy/deploy.js" + } +} \ No newline at end of file diff --git a/deployment/kyma/scripts/build-ui-image.sh b/deployment/kyma/scripts/build-ui-image.sh new file mode 100755 index 00000000..e2f67a1e --- /dev/null +++ b/deployment/kyma/scripts/build-ui-image.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +set -e +cd "$(dirname "$(npm root)")" +DIR="$(pwd)" + +npm install --no-save yaml + +function value() { + node "$DIR/deployment/kyma/scripts/value.js" "$1" +} + +function image() { + local REPOSITORY="$(value "$1.image.repository")" + local TAG="$(value "$1.image.tag")" + if [ "$TAG" != "" ]; then + echo "$REPOSITORY:$TAG" + else + echo "$REPOSITORY" + fi +} + +rm -rf gen/ui +mkdir -p gen/ui/resources + +CLOUD_SERVICE="$(value html5_apps_deployer.cloudService)" +DESTINATIONS="$(value html5_apps_deployer.backendDestinations)" + +IMAGE="$(image html5_apps_deployer)" + +for APP in app/*; do + if [ -f "$APP/webapp/manifest.json" ]; then + echo "Build $APP..." + echo + + rm -rf "gen/$APP" + mkdir -p "gen/app" + cp -r "$APP" gen/app + pushd >/dev/null "gen/$APP" + + node "$DIR/deployment/kyma/scripts/prepareUiFiles.js" $CLOUD_SERVICE $DESTINATIONS + npm install + npx ui5 build preload --clean-dest --config ui5-deploy.yaml --include-task=generateManifestBundle generateCachebusterInfo + cd dist + rm manifest-bundle.zip + mv *.zip "$DIR/gen/ui/resources" + + popd >/dev/null + fi +done + +cd gen/ui + +echo +echo "HTML5 Apps:" +ls -l resources +echo + +cat >package.json <" ]; then + echo >&2 "[ERROR] Please either specify the name for the DB secret or maintain it in the Helm chart" + exit 1 + fi +fi + +SECRET_HEADER="$(cat </dev/null >/dev/null service $NAME || cf create-service hana hdi-shared $NAME +while true; do + STATUS="$(cf 2>/dev/null service $NAME | grep status: | head -n 1)" + echo $STATUS + if [[ "$STATUS" = *succeeded* ]]; then + break + fi + sleep 1 +done + +cf create-service-key $NAME $NAME-key + +node "$(dirname "$0")/format-kyma-secret.js" -- "$(echo "$SECRET_HEADER")" "$(cf service-key $NAME $NAME-key)" | kubectl apply -f - +echo +echo "HANA DB Kubernetes secret '$NAME' created." +echo +echo "You can view it using:" +echo +echo "kubectl get secret $NAME -o yaml" +exit 0 \ No newline at end of file diff --git a/deployment/kyma/scripts/format-kyma-secret.js b/deployment/kyma/scripts/format-kyma-secret.js new file mode 100644 index 00000000..eccc78a6 --- /dev/null +++ b/deployment/kyma/scripts/format-kyma-secret.js @@ -0,0 +1,9 @@ +const key=JSON.parse(process.argv[4].replace(/^.*/, "")); +const credentials=key.credentials /* new cfcli? */ || key; +console.log(process.argv[3]); +console.log(Object.keys(credentials).map(k => { + if (credentials[k].match(/\n/s)) + return (` ${k}: |\n${credentials[k]}`).replace(/\n/gs,"\n ") + else + return ` ${k}: "${credentials[k]}"` +}).join("\n")) \ No newline at end of file diff --git a/deployment/kyma/scripts/prepareUiFiles.js b/deployment/kyma/scripts/prepareUiFiles.js new file mode 100644 index 00000000..2fd750e1 --- /dev/null +++ b/deployment/kyma/scripts/prepareUiFiles.js @@ -0,0 +1,166 @@ + +const Path = require('path'); +const posixJoin = Path.posix.join; + +function prepareUiFiles(path, options) { + const { readFileSync, writeFileSync, existsSync } = require('fs'); + const Path = require('path'); + + const srvDestination = getSrvDestination(options.destinations); + + const packageJsonInclude = getPackageJsonInclude(); + const ui5DeployTemplate = getUI5DeployTemplateYaml(); + const xsAppJsonTemplate = getXsAppTemplateJson(); + + const packageJsonPath = Path.join(path, 'package.json'); + const manifestJsonPath = Path.join(path, 'webapp/manifest.json'); + const packageJson = JSON.parse(readFileSync(packageJsonPath)); + const manifestJson = JSON.parse(readFileSync(manifestJsonPath)); + + // Adjust package.json + packageJson.devDependencies = packageJsonInclude.devDependencies; + packageJson.ui5 = packageJsonInclude.ui5; + writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4)); + + // Adjust manifest.json + console.log('Adjust webapp/manifest.json'); + manifestJson["sap.cloud"] = { + "public": true, + "service": options.cloudService || throwError(`Missing cloudService name`) + }; + + if (!existsSync(Path.join(path, 'xs-app.json'))) { + const dataSources = manifestJson['sap.app'].dataSources || {}; + for (const dataSource of Object.values(dataSources)) { + dataSource.uri = posixJoin('service', dataSource.uri); + } + } + + writeFileSync(manifestJsonPath, JSON.stringify(manifestJson, null, 4)); + + // Add xs-app.json + if (!existsSync(Path.join(path, 'xs-app.json'))) { + console.log('Add xs-app.json'); + const xsAppJson = JSON.parse(JSON.stringify(xsAppJsonTemplate)); + xsAppJson.routes[0].destination = srvDestination || throwError('Expect srv destination'); + writeFileSync(Path.join(path, 'xs-app.json'), JSON.stringify(xsAppJson, null, 4)); + } + + // Add ui5-deploy.yaml + if (!existsSync(Path.join(path, 'ui5-deploy.yaml'))) { + console.log('Add ui5-deploy.yaml'); + const appId = manifestJson["sap.app"].id; + const replacements = { + "ID": appId, + "ARCHIVENAME": appId.replace(/\./g, "") + }; + + const ui5Deploy = ui5DeployTemplate.replace(/\$([A-Z]+|)/g, (_, v) => replacements[v]); + writeFileSync(Path.join(path, "ui5-deploy.yaml"), ui5Deploy); + } +} + +function throwError(msg) { + throw new Error(msg); +} + +function getPackageJsonInclude() { + return { + "name": "ui5-builde-root", + "devDependencies": { + "@ui5/cli": "^2.11.1", + "@ui5/fs": "^2.0.6", + "@ui5/logger": "^2.0.1", + "@sap/ux-ui5-tooling": "1", + "rimraf": "3.0.2", + "@sap/ui5-builder-webide-extension": "1.0.x", + "ui5-task-zipper": "^0.3.1", + "mbt": "^1.0.15" + }, + "ui5": { + "dependencies": [ + "@sap/ui5-builder-webide-extension", + "ui5-task-zipper", + "mbt" + ] + } + } +} + +function getUI5DeployTemplateYaml() { + return `specVersion: '2.4' +metadata: + name: $ID +type: application +resources: + configuration: + propertiesFileSourceEncoding: UTF-8 +builder: + resources: + excludes: + - "/test/**" + - "/localService/**" + customTasks: + - name: webide-extension-task-updateManifestJson + beforeTask: generateManifestBundle + configuration: + appFolder: webapp + destDir: dist + - name: ui5-task-zipper + afterTask: generateCachebusterInfo + configuration: + archiveName: $ARCHIVENAME + additionalFiles: + - xs-app.json` +} + +function getXsAppTemplateJson() { + return { + "welcomeFile": "/index.html", + "authenticationMethod": "route", + "routes": [ + { + "source": "^/service/(.*)$", + "target": "$1", + "destination": "overwrite-me", + "authenticationType": "xsuaa", + "csrfProtection": false + }, + { + "source": "^/resources/(.*)$", + "target": "/resources/$1", + "authenticationType": "none", + "destination": "ui5" + }, + { + "source": "^/test-resources/(.*)$", + "target": "/test-resources/$1", + "authenticationType": "none", + "destination": "ui5" + }, + { + "source": "^(.*)$", + "target": "$1", + "service": "html5-apps-repo-rt", + "authenticationType": "xsuaa" + } + ] + } + +} + +function getSrvDestination(destinations) { + let destinationsJSON = JSON.parse(destinations); + for (let key in destinationsJSON) { + if(destinationsJSON[key].service === "srv") { + return key; + } + } +} + +if (process.argv[1].endsWith('prepareUiFiles.js')) { + // Run in standalone mode + prepareUiFiles('.', { cloudService: process.argv[2], destinations: process.argv[3] }); +} else { + module.exports = prepareUiFiles; +} \ No newline at end of file diff --git a/deployment/kyma/scripts/value.js b/deployment/kyma/scripts/value.js new file mode 100755 index 00000000..1ebb0edc --- /dev/null +++ b/deployment/kyma/scripts/value.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node + +const yaml = require('yaml'); +const fs = require('fs'); + +const path = process.argv[2]; +if (!path) { + console.error('missing path'); + process.exit(1); +} + +const segments = path.split('.'); + +function printProperty(file) { + let valuesYaml; + try { + valuesYaml = fs.readFileSync(file, 'utf-8'); + } catch(error) { + return false; + } + const values = yaml.parse(valuesYaml); + + let o = values; + for (let segment of segments) { + o = o[segment]; + if (typeof o === "undefined" || o === null) return false; + } + + if (typeof o === "object") { + console.log(JSON.stringify(o)); + } else { + console.log(o); + } + + return true; +} + +printProperty('.values.yaml') || printProperty('deployment/kyma/values.yaml') || printProperty('chart/values.yaml') || process.exit(1); \ No newline at end of file diff --git a/deployment/kyma/values.yaml b/deployment/kyma/values.yaml new file mode 100644 index 00000000..b5296278 --- /dev/null +++ b/deployment/kyma/values.yaml @@ -0,0 +1,66 @@ +global: + # >>> Execute the following command to receive the domain: + # $ kubectl get gateway -n kyma-system kyma-gateway -o jsonpath='{.spec.servers[0].hosts[0]}' + # Remove the leading "*." + domain: null + imagePullSecret: + name: container-registry +hana_deployer: + image: + # >>> Replace with your container registry + repository: /sflight-hana-deployer + tag: latest + bindings: + hana: + fromSecret: sflight-db +destinations: + serviceOfferingName: destination + servicePlanName: lite + parameters: + version: 1.0.0 + HTML5Runtime_enabled: true +html5_apps_deployer: + cloudService: sap.fe.cap.sflight + backendDestinations: + sflight-srv: + service: srv + image: + # >>> Replace with your container registry + repository: /sflight-html5-deployer + tag: latest + bindings: + xsuaa: + serviceInstanceName: xsuaa + destination: + serviceInstanceName: destinations + html5_apps_repo: + serviceInstanceName: html5-apps-repo-host +html5_apps_repo_host: + serviceOfferingName: html5-apps-repo + servicePlanName: app-host +srv: + bindings: + uaa: + serviceInstanceName: xsuaa + db: + fromSecret: sflight-db + image: + # >>> Replace with your container registry + repository: /sflight-srv + tag: latest + resources: + limits: + cpu: 2000m + ephemeral-storage: 1G + memory: 2G + requests: + cpu: 2000m + ephemeral-storage: 1G + memory: 2G + health_check: + liveness: + path: / + readiness: + path: / + env: + SPRING_PROFILES_ACTIVE: cloud diff --git a/srv/pom.xml b/srv/pom.xml index c785d9e9..7891748b 100644 --- a/srv/pom.xml +++ b/srv/pom.xml @@ -60,7 +60,11 @@ spring-boot-starter-security - + + com.sap.cds + cds-feature-k8s + runtime + @@ -87,7 +91,6 @@ - org.codehaus.mojo build-helper-maven-plugin