CloudFoundry ランタイム表示画面を Node-RED で作る

この記事は [IBM Cloud Advent Calendar 2020](https://qiita.com/advent-calendar/2020/ibmcloud) 11日目の記事です。

**Cloud Foundry API** を利用して、IBM Cloud Foundry のランタイム情報を表示する画面の作成手順をご紹介したいと思います。すでに非推奨な、古い手順になってしまいますがご了承ください…

- [V2 ドキュメント](https://apidocs.cloudfoundry.org/15.3.0/)
- [V3 ドキュメント](http://v3-apidocs.cloudfoundry.org/version/3.93.0/index.html)

ページ最下部に [フローの JSON](#今回のフロー) を掲載しております。このページでは処理の流れやソースコードを解説しますので興味がある方はフローをインポートして実行してみてください。

**「こうやればもっとスマートだよ」** といったノウハウがあれば**コメント**いただけると嬉しいですっ！

## IBM Cloud Foundry のランタイム画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311258/d5194bd5-a02c-40f1-7cd9-dc32264246e9.png)
複数の Cloud Foudry アプリを実行しているとき、各アプリの状況を切り替えて表示するのが少し手間だと感じたのでお手製のランタイム画面を作成してみました。

## お手製 ランタイム画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311258/761bcad9-de60-a1ac-870a-19821e3ca524.png)
上部ドロップダウンで Cloud Foundry アプリを選択すると、現在の状況が表示されます。

## フローの全容
### Cloud Foundry 認可フロー
IBM Cloud Foundry アプリの一覧や詳細情報を取得するために、認可トークンを取得するフローをサブフローで用意しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311258/b723544d-27ea-eb09-2b13-acf1953f9034.png)
#### 認可用のエンドポイントURL取得
以下のAPIから認可用APIのエンドポイントを取得します。

```
https://api.ng.bluemix.net/v2/info
```

#### 認可API呼び出し
認可APIを呼び出し、トークンを取得します。(IBM Cloud のユーザ／パスワードを埋め込んでます…)

```javascript:トークン取得
msg.url = msg.payload.authorization_endpoint ;
msg.url += "/oauth/token";
msg.method = "POST";
//(補足) Y2Y6は、username:cf (passwordなし）をBase64でエンコードしたものになります。
msg.headers = {
    'Authorization': "Basic Y2Y6",
    "Content-Type": "application/x-www-form-urlencoded"
};
msg.payload = {
    'grant_type': "password",
    'username'  : [IBM Cloud ユーザ名],
    'password': [IBM Cloud パスワード],
    'response_type' : 'token'
};
return msg;
```
### メインのフロー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311258/4cf26cc2-6e64-2d4e-355b-9ec1b30bd6d9.png)
Node-RED 起動時に、IBM Cloud Foundry のアプリ一覧を取得し、ドロップダウンにGUID をセットします。

ドロップダウンの値が変化すると、選択されたアプリの詳細情報を取得して、UI の Gauge に値をセットします。

#### Cloud Foundry アプリ一覧取得
認可トークンをヘッダにセットして、アプリ一覧取得のAPIを呼び出します。

```javascript:AppList取得
msg.url = "https://api.ng.bluemix.net/";
msg.url += "/v2/apps";
msg.method = "GET";
msg.headers = {
    'Authorization': msg.cfAuth.token_type + " " + msg.cfAuth.access_token,
    "Content-Type": "application/x-www-form-urlencoded"
};
msg.payload = {
};
return msg;
```

#### ドロップダウン用配列作成
アプリの詳細を取得するためには 固有の GUID が必要なので、ドロップダウンで選択できるように配列に格納しておきます。

```javascript:ドロップダウン用配列
var ret = [];
ret.push({"未選択" : ""});
msg.payload.resources.forEach(x =>
{
    var itm = [];
    itm[x.entity.name] = x.metadata.guid;
    ret.push(itm);
});

msg.options = ret;
return msg;
```

#### Cloud Foundry アプリ詳細取得
ドロップダウンで選択された GUID を使用して、アプリの詳細情報を取得します。

```javascript:App詳細取得
msg.url = "https://api.ng.bluemix.net/";
msg.url += "/v2/apps/" + msg.guid + "/stats" ;
msg.method = "GET";
msg.headers = {
    'Authorization': msg.cfAuth.token_type + " " + msg.cfAuth.access_token,
    "Content-Type": "application/x-www-form-urlencoded"
};
msg.payload = {
};
return msg;
```

#### 取得した値の加工
使用メモリ量やディスク量は `バイト単位` になっていて見づらいので、`メガ単位` にします。 <br>
ここでは、`change ノード` で **JSONata** を使ってみます。

##### メモリ使用量
```javascript:メモリ使用量
$round(payload.stats.usage.mem / 1000000, 2)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311258/a5cdc22d-830c-aa79-297b-9675c5105814.png)

##### ディスク使用量
```javascript:ディスク使用量
$round(payload.stats.usage.disk / 1000000, 2)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311258/91e61af2-4f92-43f9-7724-ef9f223888d0.png)

##### CPU 使用量
CPU 使用量は`パーセンテージ` で表示したいので、少し違う加工をします。

```javascript:CPU使用量
$round(payload.stats.usage.cpu * 100, 3)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311258/35ff762e-c574-205a-76f8-0029e0240b4f.png)

### こだわりポイント
この画面は、

- 画面の初期化(アプリ一覧取得)
- ユーザ操作待ち
- 操作後の結果表示

という３つの状態を持っているので、画面下部に状態を表示するエリアを設けています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311258/256bc362-4e8b-5b4b-9377-12245d483d1a.png)

枝分かれしたフローを接続し、状態に応じたメッセージを表示するようにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311258/0804febf-f0ca-bee1-f450-e16321cfc9fe.png)


## 今回のフロー
### メインフロー

```
[{"id":"ea4953fa.d6725","type":"subflow","name":"CF認可","info":"","category":"","in":[{"x":50,"y":30,"wires":[{"id":"3827a45a.128e3c"}]}],"out":[{"x":700,"y":240,"wires":[{"id":"8f9021c2.dcc","port":0}]}],"env":[],"color":"#DDAA99"},{"id":"42036e2f.e65ce","type":"function","z":"ea4953fa.d6725","name":"トークン取得","func":"msg.url = msg.payload.authorization_endpoint ;\nmsg.url += \"/oauth/token\";\nmsg.method = \"POST\";\n//(補足) Y2Y6は、username:cf (passwordなし）をBase64でエンコードしたものになります。\nmsg.headers = {\n    'Authorization': \"Basic Y2Y6\",\n    \"Content-Type\": \"application/x-www-form-urlencoded\"\n};\nmsg.payload = {\n    'grant_type': \"password\",\n    'username'  : IBM Cloud のユーザ名,\n    'password': IBM Cloud のパスワード,\n    'response_type' : 'token'\n};\nreturn msg;","outputs":1,"noerr":14,"initialize":"","finalize":"","x":420,"y":120,"wires":[["56819ad7.db63c4"]]},{"id":"56819ad7.db63c4","type":"http request","z":"ea4953fa.d6725","name":"認可エンドポイントに要求","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":500,"y":160,"wires":[["8f9021c2.dcc"]]},{"id":"3827a45a.128e3c","type":"http request","z":"ea4953fa.d6725","name":"認可エンドポイント取得","method":"GET","ret":"obj","paytoqs":"ignore","url":"https://api.ng.bluemix.net/v2/info","tls":"","persist":false,"proxy":"","authType":"","x":190,"y":80,"wires":[["42036e2f.e65ce"]]},{"id":"8f9021c2.dcc","type":"change","z":"ea4953fa.d6725","name":"認可情報確保","rules":[{"t":"set","p":"cfAuth","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":560,"y":220,"wires":[[]]},{"id":"194aa920.eaa857","type":"comment","z":"ea4953fa.d6725","name":"出力は msg.cfAuth","info":"","x":630,"y":280,"wires":[]},{"id":"c65f47d8.d266c8","type":"tab","label":"RestartSequence","disabled":false,"info":""},{"id":"2ef71c61.3e4684","type":"inject","z":"c65f47d8.d266c8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"","topic":"","payload":"reboot","payloadType":"str","x":110,"y":60,"wires":[["6bd77466.0457ec","c68af762.6f8ed8"]]},{"id":"823c481.6d530b8","type":"comment","z":"c65f47d8.d266c8","name":"https://apidocs.cloudfoundry.org/247/","info":"","x":140,"y":20,"wires":[]},{"id":"7a1374ba.156aec","type":"function","z":"c65f47d8.d266c8","name":"AppList 取得","func":"msg.url = \"https://api.ng.bluemix.net/\";\nmsg.url += \"/v2/apps\";\nmsg.method = \"GET\";\nmsg.headers = {\n    'Authorization': msg.cfAuth.token_type + \" \" + msg.cfAuth.access_token,\n    \"Content-Type\": \"application/x-www-form-urlencoded\"\n};\nmsg.payload = {\n};\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":490,"y":100,"wires":[["cbbe7dbd.d220b"]]},{"id":"cbbe7dbd.d220b","type":"http request","z":"c65f47d8.d266c8","name":"","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":550,"y":120,"wires":[["96c333b7.611c","1b9045c7.a55eda"]]},{"id":"6bd77466.0457ec","type":"change","z":"c65f47d8.d266c8","name":"ドロップダウン用配列作成","rules":[],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":60,"wires":[["5585377a.3f5428"]]},{"id":"96c333b7.611c","type":"function","z":"c65f47d8.d266c8","name":"ドロップダウン用配列作成","func":"var ret = [];\nret.push({\"未選択\" : \"\"});\nmsg.payload.resources.forEach(x =>\n{\n    var itm = [];\n    itm[x.entity.name] = x.metadata.guid;\n    ret.push(itm);\n});\n\nmsg.options = ret;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":720,"y":200,"wires":[["db4de15e.2b735","99e68255.48a4a"]]},{"id":"db4de15e.2b735","type":"ui_dropdown","z":"c65f47d8.d266c8","name":"","label":"","tooltip":"","place":"Select option","group":"93b2d5f.6f8ef28","order":1,"width":0,"height":0,"passthru":false,"multiple":false,"options":[{"label":"","value":"","type":"str"}],"payload":"","topic":"","x":840,"y":240,"wires":[["f3b2585e.03e558","80995ae.8df5fa8"]]},{"id":"f3b2585e.03e558","type":"ui_text","z":"c65f47d8.d266c8","group":"93b2d5f.6f8ef28","order":6,"width":0,"height":0,"name":"","label":"ステータス:","format":"{{msg.payload}}","layout":"row-left","x":810,"y":340,"wires":[]},{"id":"99500971.c5b698","type":"function","z":"c65f47d8.d266c8","name":"App 詳細 取得","func":"msg.url = \"https://api.ng.bluemix.net/\";\nmsg.url += \"/v2/apps/\" + msg.guid + \"/stats\" ;\nmsg.method = \"GET\";\nmsg.headers = {\n    'Authorization': msg.cfAuth.token_type + \" \" + msg.cfAuth.access_token,\n    \"Content-Type\": \"application/x-www-form-urlencoded\"\n};\nmsg.payload = {\n};\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":460,"y":480,"wires":[["b6be316f.384dd"]]},{"id":"b6be316f.384dd","type":"http request","z":"c65f47d8.d266c8","name":"","method":"use","ret":"obj","url":"","tls":"","x":470,"y":520,"wires":[["d3e80650.909118","8a2d3a64.697d28"]]},{"id":"c43f0e98.7240d","type":"change","z":"c65f47d8.d266c8","name":"","rules":[{"t":"set","p":"guid","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":250,"y":440,"wires":[["9330daff.2c04f8"]]},{"id":"5585377a.3f5428","type":"subflow:ea4953fa.d6725","z":"c65f47d8.d266c8","name":"","x":350,"y":100,"wires":[["7a1374ba.156aec"]]},{"id":"9330daff.2c04f8","type":"subflow:ea4953fa.d6725","z":"c65f47d8.d266c8","name":"","x":290,"y":480,"wires":[["99500971.c5b698"]]},{"id":"34f77e2b.be6062","type":"change","z":"c65f47d8.d266c8","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"$round(payload.stats.usage.mem / 1000000, 2)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":700,"y":560,"wires":[["60cd6bcd.1a0004"]]},{"id":"c1532a21.e8d8f8","type":"ui_gauge","z":"c65f47d8.d266c8","name":"","group":"93b2d5f.6f8ef28","order":2,"width":6,"height":4,"gtype":"gage","title":"CPU","label":"units","format":"{{value}}%","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":890,"y":680,"wires":[]},{"id":"f231cc0f.3993f","type":"ui_gauge","z":"c65f47d8.d266c8","name":"","group":"93b2d5f.6f8ef28","order":4,"width":6,"height":4,"gtype":"gage","title":"Disk","label":"units","format":"{{value}}","min":0,"max":"512","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":890,"y":620,"wires":[]},{"id":"60cd6bcd.1a0004","type":"ui_gauge","z":"c65f47d8.d266c8","name":"","group":"93b2d5f.6f8ef28","order":3,"width":6,"height":4,"gtype":"gage","title":"Memory","label":"units","format":"{{value}}","min":0,"max":"256","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":900,"y":560,"wires":[]},{"id":"baf4c521.f58528","type":"change","z":"c65f47d8.d266c8","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"$round(payload.stats.usage.disk / 1000000, 2)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":700,"y":620,"wires":[["f231cc0f.3993f"]]},{"id":"3f296c82.a7eff4","type":"change","z":"c65f47d8.d266c8","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"$round(payload.stats.usage.cpu * 100, 3)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":700,"y":680,"wires":[["c1532a21.e8d8f8","8a2d3a64.697d28"]]},{"id":"8a2d3a64.697d28","type":"debug","z":"c65f47d8.d266c8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":910,"y":740,"wires":[]},{"id":"c68af762.6f8ed8","type":"change","z":"c65f47d8.d266c8","name":"アプリ一覧取得中メッセージ","rules":[{"t":"set","p":"payload","pt":"msg","to":"ちょっと待ってね","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":520,"y":340,"wires":[["f3b2585e.03e558"]]},{"id":"99e68255.48a4a","type":"change","z":"c65f47d8.d266c8","name":"アプリ一覧完了メッセージ","rules":[{"t":"set","p":"payload","pt":"msg","to":"準備できました","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":520,"y":280,"wires":[["f3b2585e.03e558"]]},{"id":"d3e80650.909118","type":"function","z":"c65f47d8.d266c8","name":"JSONata 使いたくて…","func":"msg.payload = msg.payload[\"0\"];\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":410,"y":620,"wires":[["34f77e2b.be6062","baf4c521.f58528","3f296c82.a7eff4"]]},{"id":"80995ae.8df5fa8","type":"link out","z":"c65f47d8.d266c8","name":"","links":["17c013ea.a5cd4c"],"x":995,"y":240,"wires":[]},{"id":"17c013ea.a5cd4c","type":"link in","z":"c65f47d8.d266c8","name":"","links":["80995ae.8df5fa8"],"x":115,"y":440,"wires":[["c43f0e98.7240d"]]},{"id":"6403508a.b390a","type":"comment","z":"c65f47d8.d266c8","name":"ドロップダウン選択後","info":"","x":150,"y":400,"wires":[]},{"id":"1b9045c7.a55eda","type":"debug","z":"c65f47d8.d266c8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1020,"y":200,"wires":[]},{"id":"93b2d5f.6f8ef28","type":"ui_group","z":"","name":"dashboard","tab":"bceebcc7.c1085","order":2,"disp":true,"width":18,"collapse":false},{"id":"bceebcc7.c1085","type":"ui_tab","name":"Tab 3","icon":"dashboard","order":3}]
```

### 認可フロー

```
[{"id":"ea4953fa.d6725","type":"subflow","name":"CF認可","info":"","category":"","in":[{"x":50,"y":30,"wires":[{"id":"3827a45a.128e3c"}]}],"out":[{"x":700,"y":240,"wires":[{"id":"8f9021c2.dcc","port":0}]}],"env":[],"color":"#DDAA99"},{"id":"42036e2f.e65ce","type":"function","z":"ea4953fa.d6725","name":"トークン取得","func":"msg.url = msg.payload.authorization_endpoint ;\nmsg.url += \"/oauth/token\";\nmsg.method = \"POST\";\n//(補足) Y2Y6は、username:cf (passwordなし）をBase64でエンコードしたものになります。\nmsg.headers = {\n    'Authorization': \"Basic Y2Y6\",\n    \"Content-Type\": \"application/x-www-form-urlencoded\"\n};\nmsg.payload = {\n    'grant_type': \"password\",\n//    'client_id' : \"cf\",\n    'username'  : IBM Cloud ユーザ名,\n    'password': IBM Cloud パスワード,\n    'response_type' : 'token'\n};\nreturn msg;","outputs":1,"noerr":14,"initialize":"","finalize":"","x":420,"y":120,"wires":[["56819ad7.db63c4"]]},{"id":"56819ad7.db63c4","type":"http request","z":"ea4953fa.d6725","name":"認可エンドポイントに要求","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":500,"y":160,"wires":[["8f9021c2.dcc"]]},{"id":"3827a45a.128e3c","type":"http request","z":"ea4953fa.d6725","name":"認可エンドポイント取得","method":"GET","ret":"obj","paytoqs":"ignore","url":"https://api.ng.bluemix.net/v2/info","tls":"","persist":false,"proxy":"","authType":"","x":190,"y":80,"wires":[["42036e2f.e65ce"]]},{"id":"8f9021c2.dcc","type":"change","z":"ea4953fa.d6725","name":"認可情報確保","rules":[{"t":"set","p":"cfAuth","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":560,"y":220,"wires":[[]]},{"id":"194aa920.eaa857","type":"comment","z":"ea4953fa.d6725","name":"出力は msg.cfAuth","info":"","x":630,"y":280,"wires":[]}]
```

## 最後に
ちょっと不便だなと思ったことを解決するアイディアを、すぐに形にできる Node-RED 大好きです。すこし古い情報でアプリを作ってしまったので、新しいバージョンの Cloud Foundry APIを使って実装しなおしたいです。