Kyma runtimeでCloud Foundry CLIを実行するAPIを作成

## はじめに
先日、[SAP Build Process AutomationでBTPのユーザ登録を自動化する](https://qiita.com/tami/items/5d1fa776d7370727424c)という記事を書きました。記事の中で紹介したのは、Process Automationのワークフローをベースとして、申請されたユーザをBTPのサブアカウントに登録したり、ロールコレクションを付与したりするフローでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c620c43f-a7de-2916-33eb-ed1b9bc74f76.png)

[改善ポイント](https://qiita.com/tami/items/5d1fa776d7370727424c#1-cloud-foundry%E3%81%AE%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9%E3%81%B8%E3%81%AE%E3%83%A6%E3%83%BC%E3%82%B6%E7%99%BB%E9%8C%B2)として残っていたのが、Cloud Foundryのスペースへのユーザ登録でした。スペースへのユーザ登録ができるHTTPベースのAPIが見つからなかったため、上記の記事ではシステム管理者がマニュアルで行うこととしていました。
ワークフローからユーザ登録をするためには[Cloud Foundry CLI](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html#overview)をラップするAPIが必要になります。そこで今回は以下のような構成でAPIを作成することにしました。
- CFコマンドを実行するためのスクリプトを作成
- スクリプトを実行するNode.jsアプリを作成し、APIを公開

Cloud Foundry CLIを実行環境にインストールする必要があるため、Kyma runtimeを利用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/ecc3bc05-9466-7c88-4eb5-35d7deb43682.png)

## ステップ
1. スクリプトの作成
1. Node.jsアプリの作成
1. Dockerイメージの作成
1. シークレットの作成
1. Deploymentの作成

プロジェクトの構成は以下のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/eddf132e-02ae-c95c-f59a-64af5dc99eb8.png)

### 1. スクリプトの作成
以下のスクリプトを作成しました。

```bash:src/script.sh
#!/bin/bash

if [ -z "$1" ] || [ -z "$2" ]; then
    echo "Usage: $0 USER ROLE"
    exit 1
fi

# Assign positional arguments to variables
USER=$1
ROLE=$2

# login
cf login -a $CF_API_URL -u $CF_USERNAME -p $CF_PASSWORD -o $CF_ORG -s $CF_SPACE

# add a user to space
result=$(cf set-space-role $USER $CF_ORG $CF_SPACE $ROLE | awk '{print $NF}')

echo $result
if [[ "$result" == *OK ]]; then
    echo "OK"
else
    echo "Error"
    exit 1
fi
```

このスクリプトは引数にユーザと割り当てたいスペースのロールを受け取ります。
```:実行方法（例）
./script.sh user1@gmail.com SpaceDeveloper
```
スクリプトで行っているのは、Cloud Foundryにログインしてユーザにロールを割り当てることです。スペースにユーザが未登録の場合は、`cf set-space-role`コマンドによりユーザが追加されます。`$CF_API_URL`、`$CF_USERNAME`等のパラメータは環境変数として定義します。

`cf set-space-role`コマンドは、以下のような結果を返します。

```:ロールの割り当てに成功した場合（正常）
Assigning role SpaceAuditor to user xxx@gmail.com in org f83f5860trial / space dev as xxx@gmail.com...
OK
```
```:すでにロールが割り当て済みの場合（正常）
Assigning role SpaceAuditor to user mio.fujita.01@gmail.com in org f83f5860trial / space dev as xxx@gmail.com...
User 'xxx@gmail.com' already has role 'SpaceAuditor' in org 'f83f5860trial' / space 'dev'.
OK
```
```:サブアカウントに存在しないユーザを指定した場合（異常）
Assigning role SpaceAuditor to user mio.fujita.02@gmail.com in org f83f5860trial / space dev as xxx@gmail.com...
No user exists with the username 'xxx@gmail.com'.
FAILED
```

よって、結果の末尾が"OK"か否かによって、スクリプトを正常終了するか異常終了するかを判断します。

### 2. Node.jsアプリの作成
Node.jsアプリは`/spaceuser`というエンドポイントを公開します。ボディ(JSON)に設定された`user`, `role`という2つの項目を先ほどのスクリプトに渡します。
```javascript:src/server.js
const express = require('express')
const { execSync } = require('child_process')

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

app.use(express.json())

app.post('/spaceuser', (req, res) => {
  console.log('Received data:', req.body)
  const user = sanitize(req.body.user)
  const role= sanitize(req.body.role)

  try {
    execSync(`./script.sh ${user} ${role}`)
  } catch (error) {
    return res.status(400).send(error.message);
  }

  res.send('Space user was addedded successfully.')
})

function sanitize (input) {
    return input.split("|")[0]
}

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})
```
`sanitize`というファンクションは、例えば`"role": "SpaceDeveloper" | ls"`のように任意のコマンドを渡されても後ろのコマンドが実行されないように、 パイプの前の項目のみを取得します。

### 3. Dockerイメージの作成
以下のDockerfileを作成します。

```dockerfile
# syntax=docker/dockerfile:1

FROM node:latest

# install cf cli
RUN wget -q -O - https://packages.cloudfoundry.org/debian/cli.cloudfoundry.org.key | apt-key add -
RUN echo "deb https://packages.cloudfoundry.org/debian stable main" | tee /etc/apt/sources.list.d/cloudfoundry-cli.list
RUN apt-get update
RUN apt-get install cf8-cli

WORKDIR /app
COPY ["src/package.json", "src/package-lock.json*", "./"]
RUN npm install
COPY src/ .
# remove windows style carriage returns
RUN sed -i -e 's/\r$//' script.sh
CMD [ "node", "server.js" ]
```
nodeをベースイメージとして、そこにCLoud Foundry CLIをインストールします。インストール方法は以下のページを参考にしました。
https://docs.cloudfoundry.org/cf-cli/install-go-cli.html#pkg-linux

以下の部分は、私がWindowsでDockerイメージを作成していることに起因する問題への対応です。
```dockerfile
# remove windows style carriage returns
RUN sed -i -e 's/\r$//' script.sh
```
私はスクリプトやNode.jsアプリの開発はBusiness Application Studio（Linux環境）で行い、Dockerイメージを作成するためにWindowsの端末にGit経由で引っ張ってくるという開発を行っていました。するとWindowsに持ってきたときにスクリプトファイルにWindowsスタイルの改行コードが入り込み、Kyma runtimeでスクリプトを実行しようとすると`./script.sh: not found`というエラーになりました。対応として、イメージ作成時にWindowsスタイルの改行コードを除去することにしました。

**APIを実行したときのエラー内容（Windowsスタイルの改行コードが含まれている場合）**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5231bf92-0c64-c16e-103d-61f5f3a27aa4.png)

以下のコマンドでイメージを作成し、Docker Hubにプッシュします。
```
docker build -t <ユーザ名>/<イメージ>:<タグ> .
docker image push <ユーザ名>/<イメージ>:<タグ>
```


### 4. シークレットの作成
スクリプトの中で環境変数として使いたいパラメータをシークレットとして登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/51d2af99-d62b-a26d-85ea-f8a4d3645784.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/a1b863c6-58c5-2788-f215-772ac24d3be3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2a6c05c4-ff21-0319-49b4-0146ee35fdc9.png)

今回はUIから作成しましたが、kubectlのコマンドを使って以下のように登録することもできます。（改行はPower Shell向けになっているので、Linux, Macの場合は\に変更）

```
kubectl create secret generic env-cfcommand `
  --namespace default `
  --from-literal=CF_API_URL=apiUrl `
  --from-literal=CF_USERNAME=userName `
  --from-literal=CF_PASSWORD=password `
  --from-literal=CF_ORG=org `
  --from-literal=CF_SPACE=space 
```


### 5. Deploymentの作成
deployment.yamlを以下のように設定します。

```yaml:deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cfcommand
  labels:
    app: cfcommand
spec:
  replicas: 1
  selector:
    matchLabels:
      app: cfcommand
  template:
    metadata:
      labels:
        app: cfcommand
    spec:
      containers:
      - name: cfcommand
        image: docker.io/miyasuta/cfcommand:0.0.2
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
        resources:
            limits:
              ephemeral-storage: 256M
              memory: 256M
              cpu: 100m
            requests:
              cpu: 100m
              ephemeral-storage: 256M
              memory: 256M
        env:
          - name: CF_USERNAME
            valueFrom:
              secretKeyRef:
                name: env-cfcommand
                key: CF_USERNAME
          - name: CF_PASSWORD
            valueFrom:
              secretKeyRef:
                name: env-cfcommand
                key: CF_PASSWORD
          - name: CF_API_URL
            valueFrom:
              secretKeyRef:
                name: env-cfcommand
                key: CF_API_URL
          - name: CF_ORG
            valueFrom:
              secretKeyRef:
                name: env-cfcommand
                key: CF_ORG 
          - name: CF_SPACE
            valueFrom:
              secretKeyRef:
                name: env-cfcommand
                key: CF_SPACE      
status: {}
      
---
apiVersion: v1
kind: Service
metadata:
  name: cfcommand
spec:
  selector:
    app: cfcommand
  ports:
  - port: 8080
    targetPort: 8080
    protocol: TCP

---

apiVersion: gateway.kyma-project.io/v1alpha1
kind: APIRule
metadata:
  name: cfcommand
spec:
  gateway: kyma-gateway.kyma-system.svc.cluster.local
  service:
    name: cfcommand
    port: 8080
    host: cfcommand
  rules:
    - path: /.*
      methods: ["GET", "POST"]
      accessStrategies:
        - handler: oauth2_introspection
          config: {}
```
ポイントは以下の2点です。

**①シークレットを環境変数にバインドする**
以下のセクションで`env-cfcommand`というシークレットの各項目を、アプリケーションで使う環境変数にバインドしています。

```yaml
    spec:
      containers:
        #...
        env:
          - name: CF_USERNAME
            valueFrom:
              secretKeyRef:
                name: env-cfcommand
                key: CF_USERNAME
```

参考：[Define container environment variables using Secret data](https://kubernetes.io/docs/tasks/inject-data-application/distribute-credentials-secure/#define-container-environment-variables-using-secret-data)

**②認証の設定**
APIRuleの設定で、accessStrategies.handlerに`oauth2_introspection`を設定します。これにより、APIにアクセスする際にKyma runtimeで作成したOAuthクライアントを使った認証が必要になります。

```yaml
apiVersion: gateway.kyma-project.io/v1alpha1
kind: APIRule
metadata:
  name: cfcommand
spec:
    #...
  rules:
    #...
      accessStrategies:
        - handler: oauth2_introspection
          config: {}
```
OAuthクライアントの作成方法については、以下の記事をご参照ください。

https://qiita.com/tami/items/700022722493bc42a220#kyma-runtime%E3%81%AEoauth2-client%E3%82%92%E4%BD%BF%E3%81%86

※2023/3/15追記
OAuthクライアントを使用した認証はBTPのDestinationで使用することができなかった（認証エラーになる）ため、[XSUAAを使用した認証](https://qiita.com/tami/items/700022722493bc42a220#openid-connect%E3%81%AB%E6%BA%96%E6%8B%A0%E3%81%97%E3%81%9Fidentity-provider%E3%82%92%E4%BD%BF%E3%81%86)に変更しました。

```yaml
      accessStrategies:
        # - handler: oauth2_introspection
        #   config: {}
        - handler: jwt
          config: 
            jwks_urls:
            - https://f83f5860trial.authentication.us10.hana.ondemand.com/token_keys    
``` 

## 動作確認
[Rest Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client)を使ってAPIのテストを行います。一つ目のリクエストで認証用のトークンを取得し、二つ目のリクエストでCloud Foundry CLIを実行するAPIを呼び、ユーザにCloud Foundryのスペースロールを割り当てます。

```
@server=https://cfcommand.<クラスタードメイン>.kyma.ondemand.com
@oauthserver=https://oauth2.<クラスタードメイン>.kyma.ondemand.com
@user=OAuth2Clientのclient_id
@password=OAuth2Clientのclient_secret

### 1. Get Token
# @name client_credentials
POST {{oauthserver}}/oauth2/token
Authorization: Basic {{user}}:{{password}}
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials

### 2. Add user to space
POST {{server}}/spaceuser
Content-Type: application/json
Authorization: Bearer {{client_credentials.response.body.access_token}}

{
    "user": "user",
    "role": "SpaceAuditor"
}
```
以下のような結果が返ってくれば成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/3dc8514a-fa4c-968a-c0c6-d9aa9dfcfc98.png)

## おわりに
これでCloud Foundryスペースへのユーザ登録がAPIでできるようになりました。次回はこのAPIをSAP Build Process Automationのフローに組み込んでいきます。