まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:nodejs

こんなツールが私以外に需要あるかどうかわからなかったのですが、作った本人である自分は結構使う機会があるので公開することにします。

ツールの用途を一言でいうと「エクセルファイル(xls, xlsx)のテキストビューワ」です。ターミナルやコマンドプロンプトで作業している時に、ふと「このエクセルファイルの中身なんだっけ?」と確認したくなることがあります。多くは業務に必要な情報がエクセルファイルの中に格納されているケースで、「エクセルを起動して編集したいわけじゃないけど、ちょっと内容を確認したい」という場合がほとんどです。あと人によってはエクセルをインストールしているとも限らないので、LibreOffice などの代替製品をインストールしていたりするんだけど、起動に時間がかかって面倒だったり・・・ 理由は様々あると思いますが、「ちょっとした理由でエクセルファイルの中身だけ確認したい」けど「ターミナル作業中にアプリケーションを切り替えるのが面倒」といった、自分にはよくあるケースで役立つと考えています。

ツールそのものはこちらからダウンロードできるようにしておきました。自作の Node.js アプリですが、先日このブログでも紹介した pkg を使って単体で実行可能なバイナリ化しました(Linux, Windows, MacOS 用。ただし MacOS 用は未検証です)。リンク先から自分の PC プラットフォームに合うものを選んでダウンロードしてください。ダウンロード後は PATH の通ったフォルダに "xls-viewer" という名前で(Windows の場合は "xls-viewer.exe" という名前で)保存しておくとコマンドプロンプトやターミナルから直接実行できるようになります。この名前で保存しておくことで、どのプラットフォーム向けであっても "xls-viewer" というツール名で実行できるようになり、以下ではこの前提で説明を続けます(ファイル名を変えずに保存した場合は自分のプラットフォーム向けのファイル名に読み替えてください)。


【エクセルファイルのサンプル】
以下でその使い方を紹介しますが、このサンプル・エクセルファイルを使った例を紹介します。こちらも併せてダウンロードしておいてください:
https://github.com/dotnsf/xls-viewer/raw/main/sample01.xlsx


ちなみにこのサンプル・エクセルファイル sample01.xlsx の中身をエクセルそのもので開くと以下のようなものです。2つのシート Sheet1 と Sheet2(2は全角文字)から構成されています。Sheet1 は(実は1箇所だけ式を使ってますが)特に文字装飾要素も使わない、ごくシンプルなシートです:
2024072301



一方 Sheet2 は式も使っていますし、日本語や文字装飾も併用しています。またわざと空の行や空の列も含めています。このエクセルファイルをサンプルとして xls-viewer を使う例を以下で紹介します:
2024072302


【xls-viewer の使い方】
以下で xls-viewer の使いかたを紹介します。準備として xls-viewer(ファイル名を変えていない場合は元のファイル名に読みかえてください)を PATH の通ったフォルダ(/usr/local/bin など)に配置してください。そしてコマンドプロンプトやターミナルを開き、サンプルエクセルファイル sample01.xlsx のあるフォルダに移動しておきます。

まずは単純に(オプションなしで)実行して見ます。xls-viewer に続いて対象のエクセルファイル名(今回の場合は sample01.xlsx)をフルパスまたは相対パスで指定して実行します。今回は実行時のパスに sample01.xlsx が存在している想定なので、パス指定なしでも実行できるはずです。成功すると以下のような表示になります(見ている人の環境によっては変な所で改行されているように見えるかもしれませんが、実際には必要な半角スペースのパディングなども加えて正しく改行されている、はず)。各シートから表示されているテキスト情報を抜き出して表示しています:
$ xls-viewer sample01.xlsx
Sheet1 :
+----+------+------+---+--+-----+
| 1  | 3    |      |   |  |     |
+----+------+------+---+--+-----+
| 3  | 2    | safa |   |  | saf |
+----+------+------+---+--+-----+
| 2  | sa   |      |   |  |     |
+----+------+------+---+--+-----+
| 43 | safd |      |   |  |     |
+----+------+------+---+--+-----+
|    |      |      | 5 |  |     |
+----+------+------+---+--+-----+

Sheet2 :
+----------------------+--+----------------------+--+------------+
| 少し長めのテキストを |  |                      |  | 太字       |
| 入力                 |  |                      |  |            |
+----------------------+--+----------------------+--+------------+
|                      |  | 更に123を追加したテ  |  | Italic     |
|                      |  | キスト               |  |            |
+----------------------+--+----------------------+--+------------+
|                      |  |                      |  | 色付き     |
+----------------------+--+----------------------+--+------------+
| aaa                  |  | 3                    |  | 下線       |
+----------------------+--+----------------------+--+------------+
|                      |  | 6                    |  |            |
+----------------------+--+----------------------+--+------------+
|                      |  |                      |  |            |
+----------------------+--+----------------------+--+------------+
|                      |  |                      |  |            |
+----------------------+--+----------------------+--+------------+
|                      |  |                      |  | 2行とばし |
+----------------------+--+----------------------+--+------------+
|                      |  |                      |  |            |
+----------------------+--+----------------------+--+------------+
|                      |  |                      |  |            |
+----------------------+--+----------------------+--+------------+
|                      |  |                      |  |            |
+----------------------+--+----------------------+--+------------+
| 3行とばし           |  |                      |  |            |
+----------------------+--+----------------------+--+------------+

指定したファイル(sample01.xlsx)に含まれている2つのシート毎に結果が表示されています。このように特にオプションを指定しない場合は全てのシートを対象に表示します。

特定のシートだけを対象にしたい場合は、"--sheets=(シート名)" というオプションで指定します。チート名は半角カンマ(,)で分けて複数指定することも可能です(Sheet2の "2" は全角なので注意してください):
$ xls-viewer --sheets=Sheet2 sample01.xlsx
Sheet2 :
+----------------------+--+----------------------+--+------------+
| 少し長めのテキストを |  |                      |  | 太字       |
| 入力                 |  |                      |  |            |
+----------------------+--+----------------------+--+------------+
|                      |  | 更に123を追加したテ  |  | Italic     |
|                      |  | キスト               |  |            |
+----------------------+--+----------------------+--+------------+
|                      |  |                      |  | 色付き     |
+----------------------+--+----------------------+--+------------+
| aaa                  |  | 3                    |  | 下線       |
+----------------------+--+----------------------+--+------------+
|                      |  | 6                    |  |            |
+----------------------+--+----------------------+--+------------+
|                      |  |                      |  |            |
+----------------------+--+----------------------+--+------------+
|                      |  |                      |  |            |
+----------------------+--+----------------------+--+------------+
|                      |  |                      |  | 2行とばし |
+----------------------+--+----------------------+--+------------+
|                      |  |                      |  |            |
+----------------------+--+----------------------+--+------------+
|                      |  |                      |  |            |
+----------------------+--+----------------------+--+------------+
|                      |  |                      |  |            |
+----------------------+--+----------------------+--+------------+
| 3行とばし           |  |                      |  |            |
+----------------------+--+----------------------+--+------------+

デフォルト指定だと列名や行番号は表示されません。表示したい場合は "--label=1" オプションを付けて実行します:
$ xls-viewer --sheets=Sheet2 --label=1 sample01.xlsx
Sheet2 :
+====+======================+===+======================+===+============+
|    | B                    | C | D                    | E | F          |
+====+======================+===+======================+===+============+
| 2  | 少し長めのテキストを |   |                      |   | 太字       |
|    | 入力                 |   |                      |   |            |
+====+----------------------+---+----------------------+---+------------+
| 3  |                      |   | 更に123を追加したテ  |   | Italic     |
|    |                      |   | キスト               |   |            |
+====+----------------------+---+----------------------+---+------------+
| 4  |                      |   |                      |   | 色付き     |
+====+----------------------+---+----------------------+---+------------+
| 5  | aaa                  |   | 3                    |   | 下線       |
+====+----------------------+---+----------------------+---+------------+
| 6  |                      |   | 6                    |   |            |
+====+----------------------+---+----------------------+---+------------+
| 7  |                      |   |                      |   |            |
+====+----------------------+---+----------------------+---+------------+
| 8  |                      |   |                      |   |            |
+====+----------------------+---+----------------------+---+------------+
| 9  |                      |   |                      |   | 2行とばし |
+====+----------------------+---+----------------------+---+------------+
| 10 |                      |   |                      |   |            |
+====+----------------------+---+----------------------+---+------------+
| 11 |                      |   |                      |   |            |
+====+----------------------+---+----------------------+---+------------+
| 12 |                      |   |                      |   |            |
+====+----------------------+---+----------------------+---+------------+
| 13 | 3行とばし           |   |                      |   |            |
+====+----------------------+---+----------------------+---+------------+

上のラベルを付けた結果を見るとわかりますが、対象のシート内の有効なエリアだけが表示されています(A 列目や1行目にはデータがないので表示されていません)。常に A1 セルも含めて表示する場合は "--a1=1" オプションを付けて実行します:
$ xls-viewer --sheets=Sheet2 --label=1 --a1=1 sample01.xlsx
Sheet2 :
+====+===+======================+===+======================+===+============+
|    | A | B                    | C | D                    | E | F          |
+====+===+======================+===+======================+===+============+
| 1  |   |                      |   |                      |   |            |
+====+---+----------------------+---+----------------------+---+------------+
| 2  |   | 少し長めのテキストを |   |                      |   | 太字       |
|    |   | 入力                 |   |                      |   |            |
+====+---+----------------------+---+----------------------+---+------------+
| 3  |   |                      |   | 更に123を追加したテ  |   | Italic     |
|    |   |                      |   | キスト               |   |            |
+====+---+----------------------+---+----------------------+---+------------+
| 4  |   |                      |   |                      |   | 色付き     |
+====+---+----------------------+---+----------------------+---+------------+
| 5  |   | aaa                  |   | 3                    |   | 下線       |
+====+---+----------------------+---+----------------------+---+------------+
| 6  |   |                      |   | 6                    |   |            |
+====+---+----------------------+---+----------------------+---+------------+
| 7  |   |                      |   |                      |   |            |
+====+---+----------------------+---+----------------------+---+------------+
| 8  |   |                      |   |                      |   |            |
+====+---+----------------------+---+----------------------+---+------------+
| 9  |   |                      |   |                      |   | 2行とばし |
+====+---+----------------------+---+----------------------+---+------------+
| 10 |   |                      |   |                      |   |            |
+====+---+----------------------+---+----------------------+---+------------+
| 11 |   |                      |   |                      |   |            |
+====+---+----------------------+---+----------------------+---+------------+
| 12 |   |                      |   |                      |   |            |
+====+---+----------------------+---+----------------------+---+------------+
| 13 |   | 3行とばし           |   |                      |   |            |
+====+---+----------------------+---+----------------------+---+------------+

各列の幅はデフォルトだと半角20文字となっています。この値を変えて表示するには "--row_max_width" オプションを指定して実行します。以下の例では「半角10文字」で表示しています。B2 セルや D3 セルの長めのテキストが半角10文字で折り返している様子が確認できます:
$ xls-viewer --sheets=Sheet2 --label=1 --a1=1 --row_max_width=10 sample01.xlsx
Sheet2 :
+====+===+============+===+============+===+============+
|    | A | B          | C | D          | E | F          |
+====+===+============+===+============+===+============+
| 1  |   |            |   |            |   |            |
+====+---+------------+---+------------+---+------------+
| 2  |   | 少し長めの |   |            |   | 太字       |
|    |   | テキストを |   |            |   |            |
|    |   | 入力       |   |            |   |            |
+====+---+------------+---+------------+---+------------+
| 3  |   |            |   | 更に123を  |   | Italic     |
|    |   |            |   | 追加したテ |   |            |
|    |   |            |   | キスト     |   |            |
+====+---+------------+---+------------+---+------------+
| 4  |   |            |   |            |   | 色付き     |
+====+---+------------+---+------------+---+------------+
| 5  |   | aaa        |   | 3          |   | 下線       |
+====+---+------------+---+------------+---+------------+
| 6  |   |            |   | 6          |   |            |
+====+---+------------+---+------------+---+------------+
| 7  |   |            |   |            |   |            |
+====+---+------------+---+------------+---+------------+
| 8  |   |            |   |            |   |            |
+====+---+------------+---+------------+---+------------+
| 9  |   |            |   |            |   | 2行とばし |
+====+---+------------+---+------------+---+------------+
| 10 |   |            |   |            |   |            |
+====+---+------------+---+------------+---+------------+
| 11 |   |            |   |            |   |            |
+====+---+------------+---+------------+---+------------+
| 12 |   |            |   |            |   |            |
+====+---+------------+---+------------+---+------------+
| 13 |   | 3行とばし |   |            |   |            |
+====+---+------------+---+------------+---+------------+

デフォルトでは各セルの「表示されている内容」をそのまま表示していますが、セルの表示式が定義されている場合に、その式だけを表示するには "--formula=1" オプションを指定します。実行結果にはセルに定義されている式だけが表示されます:
$ xls-viewer --label=1 --a1=1 --formula=1 sample01.xlsx
Sheet1 :
+===+===+=====+===+===+===+===+
|   | A | B   | C | D | E | F |
+===+===+=====+===+===+===+===+
| 1 |   | 1+2 |   |   |   |   |
+===+---+-----+---+---+---+---+
| 2 |   |     |   |   |   |   |
+===+---+-----+---+---+---+---+
| 3 |   |     |   |   |   |   |
+===+---+-----+---+---+---+---+
| 4 |   |     |   |   |   |   |
+===+---+-----+---+---+---+---+
| 5 |   |     |   |   |   |   |
+===+---+-----+---+---+---+---+

Sheet2 :
+====+===+===+===+======+===+===+
|    | A | B | C | D    | E | F |
+====+===+===+===+======+===+===+
| 1  |   |   |   |      |   |   |
+====+---+---+---+------+---+---+
| 2  |   |   |   |      |   |   |
+====+---+---+---+------+---+---+
| 3  |   |   |   |      |   |   |
+====+---+---+---+------+---+---+
| 4  |   |   |   |      |   |   |
+====+---+---+---+------+---+---+
| 5  |   |   |   | 1+2  |   |   |
+====+---+---+---+------+---+---+
| 6  |   |   |   | D5*2 |   |   |
+====+---+---+---+------+---+---+
| 7  |   |   |   |      |   |   |
+====+---+---+---+------+---+---+
| 8  |   |   |   |      |   |   |
+====+---+---+---+------+---+---+
| 9  |   |   |   |      |   |   |
+====+---+---+---+------+---+---+
| 10 |   |   |   |      |   |   |
+====+---+---+---+------+---+---+
| 11 |   |   |   |      |   |   |
+====+---+---+---+------+---+---+
| 12 |   |   |   |      |   |   |
+====+---+---+---+------+---+---+
| 13 |   |   |   |      |   |   |
+====+---+---+---+------+---+---+

なお "--border=0" オプションを付けると、シートの枠が表示されなくなります:
$ xls-viewer --label=1 --a1=1 --border=0 sample01.xlsx
Sheet1 :
    A   B     C     D  E  F
 1  1   3
 2  3   2     safa        saf
 3  2   sa
 4  43  safd
 5                  5

Sheet2 :
     A  B                     C  D                     E  F
 1
 2      少し長めのテキストを                              太字
        入力
 3                               更に123を追加したテ      Italic
                                 キスト
 4                                                        色付き
 5      aaa                      3                        下線
 6                               6
 7
 8
 9                                                        2行とばし
 10
 11
 12
 13     3行とばし

これらのオプションは組み合わせて使うことも可能です。結果をファイルにリダイレクトしながら使えば後からファイルとして開くこともできて、個人的には我ながら重宝しているコマンドです。



上述の説明では実行可能ファイルだけを紹介していますが、実際にはソースコード含めて公開しています。作り方に興味ある方はこちらもどうぞ:
https://github.com/dotnsf/xls-viewer

2024072300


自分はプログラミングする時、特に制約事項がない場合は Node.js を使います。サーバーサイドアプリだけでなく、CLI 系のコマンドラインアプリも作れるし、React を併用してフロンドエンドアプリも作れます。HTML 内で JavaScript を使うケースも含めると JavaScript 系の言語は使い道が幅広くて便利、と思ってます。

そんな自分が最近作った CLI アプリ(コマンドラインアプリ)がそこそこ便利だったので公開したかったのですが、Node.js アプリは Node.js がインストールされた環境で動くのです。ある意味で当たり前のことを言っているのですが、プログラミングなどに詳しくない人からすると「プログラミング言語をインストールしないと動かない」というのはハードルが高いものです。なんとかして「ダウンロード→即実行可能」な形にできないかと調べてみました。今回のブログエントリはそんな経緯をまとめたものです。

ランタイムが Node.js の場合、このようなアプリケーションのパッケージ化ツールには nexe はじめ、いくつかの選択肢があるようですが、今回自分が使ったのは pkg というツールです※
2014072100


※ Git リポジトリは現在アーカイブされているので、今後のアップデートはされない可能性が高いと思っています。逆に言うと、良くも悪くも今後の変更はほぼないものだと思って使ってください。

自分が Linux(WSL2) で作った CLI アプリが Linux および Windows 向けにクロスコンパイルして実行可能バイナリ化できる所まで確認しました。自分の用途としてはここまでできれば実用的と言えるので、今後はこれを使っていこうかな、と考えています。


【pkg のインストール】
既に Node.js が導入済みの環境であれば、pkg のインストールは以下のコマンドを実行するだけなので超簡単です:
$ sudo npm install -g pkg

npm で pkg をグローバルインストールします。これで pkg コマンドが使えるようになります。


【pkg の使い方】
例えば 64bit Linux(WSL2) で app.js というファイル名の CLI アプリを作ったとします。この CLI アプリは Node.js を使う場合は以下のように実行できるものとします:
$ node app.js

ここに pkg をインストールして、3つの環境(64bit Linux, 64bit MacOS, 64bit Windows)向けの実行可能バイナリを作る場合は以下のコマンドを(特にオプションを付けずに)実行します:
$ pkg app.js

この場合は同じフォルダに app-linux, app-macos, app-win.exe という3つのファイルが生成され、それぞれ 64bit Linux, 64bit MacOS, 64bit Windows 向け実行可能バイナリになっています。

現在の環境と同じ環境(64bit Linux)向けの実行可能バイナリ(app)を作るのであれば、以下のコマンドを実行します("-o" オプションで出力する実行可能バイナリファイル名を指定します):
$ pkg -o app app.js

出力するファイルをクロスコンパイルしたい場合、つまり現在の環境とは異なる環境(例えば 64bit Windows)向けの実行可能バイナリ(app.exe)を作る場合は "-t" オプションでクロスコンパイル先の対象プラットフォームを指定します("node16" は "Node.js v16 でコンパイルする" の意味です):
$ pkg -t node16-win-x64 -o app.exe app.js

対象プラットフォームは node16 までは使えることを確認したのですが(ドキュメントも node16 までは書かれているのですが)、これ以上のバージョンがサポートされているかどうかは未確認です。

この pkg を使って作った実行可能バイナリは小規模なアプリでも 30MB 以上になります。私が作ったアプリも package.json には2つのライブラリしか登録していないのですが、それでも 45MB ほどになりました。おそらく node 本体とライブラリを全部まとめてパッケージングしてるんだろうな・・・と想像しています。


 

最近 OCP(Openshift Container Platform) について調べることが多かったので、何回かに分けてアウトプットしていこうと思います。

今回は「OCP のアップグレードパス」についてです。OCP のアップグレード(バージョンアップ)で特に頭の痛い問題の1つが「アップグレードパス」と呼ばれるバージョンアップ時に踏む段階のことです。

より具体的な例で考えてみましょう。例えば、現在 OCP 4.4.6 というバージョンを使っていると仮定します。なんらかの理由でこれを OCP 4.6.4 というバージョンにアップグレードしたい、という前提のもとで以下を記載していきます。


【アップグレードパス】
この辺りの事情にあまり詳しくない人だと「OCP 4.4.6 から OCP 4.6.4 へのアップグレードがそんなに不便なのか?」という疑問を持つかもしれません。わかりにくい所もあるのですが、最大の問題は「そもそも OCP 4.4.6 から OCP 4.6.4 へ直接アップグレードできるのか? 直接アップデートできない場合はどのような順で実行していけばアップデートできるのか?」という所から解決していく必要がある点にあります。この「どのような順で実行していく」のかという順序のことを「アップグレードパス」といいます。

例えばですが、仮に 4.4.6 から 4.6.4 への直接アップグレードが可能であった場合(実際はできないんですけど)、アップグレードパスは「 4.4.6 → 4.6.4 」ということになります。一方、もしも間に 4.5.0 をはさんで 4.4.6 から 4.5.0 にアップグレードし、4.5.0 から 4.6.4 にアップグレードするという2段階のアップグレードを行う必要がある場合(実際は更に複雑なんですが)、アップグレードパスは「 4.4.6 → 4.5.0 → 4.6.4 」ということになる、というわけです。


【アップグレードパスの調べ方】
さて、では実際に 4.4.6 から 4.6.4 へアップグレードする場合のアップグレードパスはどのように調べればよいのでしょうか? 実はこれが結構面倒だったりします。。

まず RedHat 提供のアップグレードパスを探すサービスがあります:
https://access.redhat.com/labs/ocpupgradegraph/update_path

ただこのサイト、比較的古いバージョンが対象だとやけに重くなってしまうのです。またアップグレード前後のバージョン差が大きいケースだと「アップグレードパスが見つからない」みたいな結果が表示されることもあり、なんかちょっとよくわからない(苦笑)感じだったりします。


というわけで、上記サービスに頼らないアップグレードパスの探し方を紹介します。具体的にはまずゴールとなるバージョン(今回だと 4.6.4)のリリースノートを調べる必要があります。OCP のバージョンごとのリリースノートはオンラインで参照することができ、その URL は以下の通りです:
https://mirror.openshift.com/pub/openshift-v4/clients/ocp/(バージョン)/release.txt

例えばバージョンが 4.6.4 であれば、以下の URL を参照します:
https://mirror.openshift.com/pub/openshift-v4/clients/ocp/4.6.4/release.txt

以下のような内容が表示されます:
2024030701


はじめの十数行だけを表示するとこのようになっています:
Client tools for OpenShift
--------------------------

These archives contain the client tooling for [OpenShift](https://docs.openshift.com).

To verify the contents of this directory, use the 'gpg' and 'shasum' tools to
ensure the archives you have downloaded match those published from this location.

The openshift-install binary has been preconfigured to install the following release:

---

Name:      4.6.4
Digest:    sha256:6681fc3f83dda0856b43cecd25f2d226c3f90e8a42c7144dbc499f6ee0a086fc
Created:   2020-11-11T15:13:14Z
OS/Arch:   linux/amd64
Manifests: 444

Pull From: quay.io/openshift-release-dev/ocp-release@sha256:6681fc3f83dda0856b43cecd25f2d226c3f90e8a42c7144dbc499f6ee0a086fc

Release Metadata:
  Version:  4.6.4
  Upgrades: 4.5.16, 4.5.17, 4.5.18, 4.5.19, 4.6.1, 4.6.2, 4.6.3
  Metadata:
    description: 
  Metadata:
    url: https://access.redhat.com/errata/RHBA-2020:4987
  :
  :

↑の赤字部分に着目します。"Release Metadata:" と書かれた部分の下にバージョンに関する情報が表示されています。まず "Version:" はこのリリースノートが対象としているバージョン(4.6.4)が表示されています。 そしてその下の行の "Upgrades:" に着目してください。この例だと "4.5.16, 4.5.17, 4.5.18, 4.5.19, 4.6.1, 4.6.2, 4.6.3" と書かれていますね。

これはつまり「バージョン 4.6.4 に直接アップグレードできるバージョンは 4.5.16, 4.5.17, 4.5.18, 4.5.19, 4.6.1, 4.6.2, 4.6.3 のいずれかのみ」であることを示しています。残念ながら元のバージョンである 4.4.6 が含まれていないので、少なくとも1回のアップグレードで 4.4.6 から 4.6.4 へはアップグレードできないことも分かります。

では最終ゴールである 4.6.4 へは(上のバージョンの中のどれでもいいんですが、なるべく回数を減らしたいので最も遠い) 4.5.16 からアップグレードするとしましょう。次に調べる必要があるのは「4.4.6 から 4.5.16 へ直接アップグレードできるのか?」です。

これも同様にして 4.5.16 のリリースノート(https://mirror.openshift.com/pub/openshift-v4/clients/ocp/4.5.16/release.txt)を開き、4.5.16 にアップグレード可能なバージョンを調べると、"4.4.13, 4.4.14, 4.4.15, 4.4.16, 4.4.17, 4.4.18, 4.4.19, 4.4.20, 4.4.21, 4.4.23, 4.4.26, 4.4.27, 4.4.28, 4.4.29, 4.4.30, 4.5.2, 4.5.3, 4.5.4, 4.5.5, 4.5.6, 4.5.7, 4.5.8, 4.5.9, 4.5.11, 4.5.13, 4.5.14, 4.5.15" という結果になることが分かります。残念ながら 4.4.6 は含まれていないので、更に段階を経たアップグレードが必要になることが分かります:
2024030702


同様にして、ここでの中継バージョンを 4.4.13 とみなし、4.4.13 のリリースノート(https://mirror.openshift.com/pub/openshift-v4/clients/ocp/4.4.13/release.txt)を調べると、、今度は "4.4.6" が含まれていることがわかります。4.4.6 から 4.4.13 へのアップグレードは可能でした:
2024030703


ここまでの結果を総合すると、4.4.6 から 4.6.4 へのアップグレードパスの1つとして
4.4.5 → 4.4.13 → 4.5.16 → 4.6.4

があることが確認できました。つまり 4.4.6 から 4.6.4 へは少なくとも3回に分けてアップグレードを実施する必要がある、ということになります。

今回の例では 4.4.6 から 4.6.4 という固定バージョンでのアップグレードパスと、その調べ方を紹介しましたが、アップグレードパスの調べ方はどのバージョンからどのバージョンへ向かう場合も同様です。上で紹介した方法を使って、目的バージョンから「このバージョンにアップグレードできる最も古いバージョンは?」を繰り返し調べていくことで、最短回数でのアップグレードパスを見つけることができるようになります。

・・・でも、ちょっと面倒ですよね。そこでこの処理をツール化してみました。


【アップグレードパスを調べるツール】
上で紹介した処理を自動的に調べるツールを Node.js で作ってみました。MIT ライセンスで公開しているので商用含めて利用・改変もご自由に:
https://github.com/dotnsf/ocp-upgrade-path


Node.js がインストールされた PC 上で、上記 URL からソースコードを clone またはダウンロードして展開します。最初の実行前に一回だけ "npm install" で外部ライブラリをインストールしておく必要があります。

実行時はコマンドラインから以下のように指定して実行します:
$ node app (現在のバージョン) (アップグレード後のバージョン)

上のように現在のバージョンが 4.4.6 、アップグレード後のバージョンが 4.6.4 であったとすると、以下のように指定して実行することになります:
$ node app 4.4.6 4.6.4

実行結果は以下のように出力されます:
$ node app 4.4.6 4.6.4
currentversion = 4.4.6
targetversion = 4.6.4

4.4.6 -> 4.4.13 -> 4.5.16 -> 4.6.4

不可能なパターンを指定するとアップグレードパスに impossible と表示されます(新しいバージョンから古いバージョンへのアップグレードを指定した例です):
$ node app 4.6.4 4.4.6
currentversion = 4.6.4
targetversion = 4.4.6

4.6.4 -> (impossible) -> 4.4.6

またバージョンで指定できるパターンは正式リリース対象の (数字).(数字).(数字) だけです。バージョン文字列内に "nightly" などの文字を含む正式リリースではないバージョンも存在していますが、このツールでは対象外としています。

ちゃんとテストしたわけではない(というかテストできない)のですが、現時点でリリースされていない未来のバージョンについても、その未来バージョンがリリースされて、リリースノートも同様に提供されていれば、その未来バージョンへのアップグレードパスも表示できるようになるはずです。

ウェブ化してもっと気軽に(Node.js がインストールされていなくても)使えるようにすることも考えたのですが、「OCP をバージョンアップする」作業が必要になるようなプロジェクトでは、それなりのエンジニアがプロジェクトに携わっていることが大半だと思ってサボっちゃいました。


「OCP アップグレード版の乗換案内」的なツールができたと思っています。役立つことがあれば嬉しいです。


【参考】
Successfully upgrade OpenShift cluster on a disconnected environment with troubleshooting guide.



ツイッター(X)が API 制約を含めて色々と不便になってしばらく経過しました。ツイッターでリツイートボットとか作ってた個人のものはほぼ全滅状態ですが、さすがに「しばらく待てばまた使えるようになる、、」などと楽観的には考えない方が良さそうな感じですね。

世の中には使いにくくなったツイッターの後釜を狙う SNS も多く出てきましたが、今のところはまだツイッターを使っている人が圧倒的多数の印象です。まあ開発者的にはツイッターはもう見限ってる人も少なくないんでしょうけど、SNS はあくまで「発信の場」であって、使っているのは開発者ばかりじゃないですからね。メッセージを届ける先の規模含めると、使い勝手が現状維持ならまだしばらくツイッターが使われていくのでしょう。


一方で、とはいえ多くの SNS が新たに生まれてきてはいます。実は自分も「お絵描き SNS」という位置づけの MyDoodles なんていうものを密かにリリースしていたりもするのですが、、、そんな中でツイッターの共同創業者の一人だったジャック・ドーシーさんが新たに立ち上げた分散型 SNS である Bluesky Social (以下 "Bluesky")はツイッターに似たインターフェースを持ち、リリースからしばらくは招待制で運用されていましたが、先ごろ招待制が撤廃されて誰でもアカウントを作れるようになりました。比較的早い段階から API も公開されており、開発者視点では「次に来るのはこれかも」と期待しています。


そんな Bluesky の API を使って、ツイッターでは(個人では事実上)作れなくなってしまったボットを作ってみることにしました。自分が作ろうと思ったのは拙作マンホールマップが持つ機能の1つである「今日のマンホール」を午前零時に自動でつぶやくボットです。実はこれまでは私自身が人間ボットになって(笑)今日のマンホールを手動でつぶやいていたのですが、これを Bluesky 内で自動化する、というものです。ボットとしての基本的な考え方はツイッターの頃と大きく変わるものではないのですが、分散 SNS である Bluesky の、まだ充分に熟しているとは言えない API を使って実装することはチャレンジでもあり、色々面倒な考慮も必要にはなるのですが、とりあえず作れそうだったので、その中身の解説をするのが本ブログエントリの目的です。なお以下で紹介する内容は 2024-02-12 時点の情報であることをご了承ください。


【開発環境】
まず Bluesky API を紹介している本家ページがこちらです:
https://www.docs.bsky.app/docs/get-started

2024021201


SDK としては Node.js と Python 向けに提供されているようですが、Curl からも使える REST API があるようなので REST API でプログラミングするつもりであれば事実上プログラミング言語に制約はないことになります。今回以下で紹介するプログラミングの環境としては自分が普段使っている Node.js を使うことにします。

Node.js の場合、@atproto/api というライブラリを使うことで比較的簡単に Bluesky API を使ったアプリケーションの開発ができるようになります。こんな感じでインストールして使います:
$ npm install @atproto/api


【認証】
Bluesky API を使って試しに自分自身の情報でも取得・・・ するのもいいですが、まずは認証を行う必要があります。ツイッターなどでは開発者向けページからアプリケーションを登録して API キーを発行して OAuth 2.0 で・・・ といった手順で認証を行っていました。Bluesky もいずれはそのような形態になるものと思いますが、現時点では OAuth2.0 はサポートされておらず、ユーザー ID とパスワードを使って直接ログインする形になるようです。ちょうど Bluesky の招待制も終わったことなので、自分はこのボット用(というかマンホールマップ用)のアカウントを1つ新たに作成しました。そして Node.js の場合はこのような形で認証(ログイン)を行います:
var { BskyAgent } = require( '@atproto/api' );

var agent = new BskyAgent({
  service: "https://bsky.social"
});
agent.login({
  identifier: "myname@email.com",
  password: "mypassword",
}).then( async function(){
  // ログイン成功後の処理へ
    :
    :
});

"@atoproto/api" ライブラリから BskyAgent をインポートして、メールアドレス(上では "myname@email.com" の部分)とパスワード(同 "mypassword" 部分)を指定してログインします。このあたりはソースコード内に直接記載するのは危険なので、コードの管理方法には注意が必要です。


【ツイートする】
ボットを作るには「ツイートする」(書き込む)機能が必要です。ログイン後に単にテキストをツイートするだけなら簡単です。特に @atproto/api ライブラリを使っていれば post メソッドを使って以下のようにログイン後の処理を一行追加するだけです:
var { BskyAgent } = require( '@atproto/api' );

var agent = new BskyAgent({
  service: "https://bsky.social"
});
agent.login({
  identifier: "myname@email.com",
  password: "mypassword",
}).then( async function(){
  var res = await agent.post( { text: 'Hello, world.'} );
  console.log( {res} );
});

これでログインしたユーザーの権限で "Hello, world." とつぶやくことができます。簡単ですよね、ここまでは。


【facets 処理(リッチテキスト処理)】
テキストをつぶやくのは簡単でした。ツイッターなどではこのテキスト内に他のユーザーへのメンション(@)が含まれていたり、ハッシュタグ(#)やリンクが含まれていると自動的に解釈してリッチテキスト化してくれて(リンクとかを付けてくれて)いましたが、Bluesky API はそうはいきません(そもそも Bluesky にはまだハッシュタグという概念がありません)。この辺りはプログラミング内でリッチテキスト化(Bluesky では「facets 検知」といいます)する必要があります。今回作ろうとしているボットも必ずあるページへのリンクを含む内容になっているので post 実行前の facets 検知が必須です。この辺から少し面倒になってきます。

具体的にはこのようなコードになります:
var { BskyAgent, RichText } = require( '@atproto/api' );

var agent = new BskyAgent({
  service: "https://bsky.social"
});
agent.login({
  identifier: "myname@email.com",
  password: "mypassword",
}).then( async function(){
  var text = "リンクを含むテキスト https://manholemap.juge.me/";

  var rt = new RichText({ text: text });
  await rt.detectFacets( agent );

  var res = await agent.post({
    $type: 'app.bsky.feed.post',
    text: rt.text,
    facets: rt.facets
  });

  console.log( {res} );
});

まずリッチテキスト化処理を行うために @atproto/api ライブラリから BskyAgent だけでなく RichText もインポートしておきます。そしてリンクなどのリッチテキスト化が必要なテキストを RichText で初期化した結果を変数 rt で受け取り、更に detectFacets() 処理を実行します。するとテキスト部分は rt.text に、facets 情報が rt.facets にそれぞれ格納されます。ツイッターではこの辺りの処理が自動化されていたので楽でしたが、Bluesky でもここまでライブラリが用意されているのでさほど大変ではないですね。

最後に facets 処理した結果を post します。先のプレーンテキストでは text 要素だけを post していましたが、リッチテキストの場合は少し情報を加えてあげる必要がありますが、これでテキスト内の URL 部分はリンクになってポストされます。


【オープングラフ処理(OGP対応)】
ツイッターなど多くの SNS ではリンクを含む情報をポストすると、そのリンク先の内容の一部(テキストや画像)が埋め込まれる形で表示されます。これは OGP と呼ばれる規格で、リンク先が OCP 規格に沿って作られたページであれば、その情報を使って実現できるものです。

Bluesky の場合もリンク先の情報を埋め込むことはできるのですが、API でつぶやく場合は(現状では)この部分を全て手作業で行う必要があります。具体的にはこんな感じです:
var { BskyAgent, RichText } = require( '@atproto/api' );
var { parse } = require( 'node-html-parser' );

var agent = new BskyAgent({
  service: "https://bsky.social"
});
agent.login({
  identifier: "myname@email.com",
  password: "mypassword",
}).then( async function(){
  var text = "リンクを含むテキスト https://manholemap.juge.me/";

  //. OGP
  var url = "https://manholemap.juge.me/";
  var resp = await fetch( url );
  var html = parse( await( resp.text() ) );
  var title = html.querySelector( "meta[property='og:description']" ).getAttribute( "content" );

  //. embed image
  var ogpImg = html.querySelector( "meta[property='og:image']" ).getAttribute( "content" );
  var blob = await fetch( ogpImg );
  var buffer = await blob.arrayBuffer();
  var response = await agent.uploadBlob( new Uint8Array( buffer ), { encoding: "image/jpeg" } );
  var embed_params = {
    $type: "app.bsky.embed.external",
    external: {
      uri: "https://manholemap.juge.me/",
      thumb: {
        $type: "blob",
        ref: {
          $link: response.data.blob.ref.toString()
        },
        mimeType: response.data.blob.mimeType,
        size: response.data.blob.size
      },
      title: title,
      description: title
    }
  };

  var rt = new RichText({ text: text });
  await rt.detectFacets( agent );

  var res = await agent.post({
    $type: 'app.bsky.feed.post',
    text: rt.text,
    facets: rt.facets,
    embed: embed_params
  });

  console.log( {res} );
});

まず URL のリンク先情報を解析するために node-html-parser ライブラリをインポートしておきます。そしてリンク先の URL(上では "https://manholemap.juge.me/")を fetch して HTML を取得します。OGP の規格に沿って作られたページであれば、その画像は HTML 内で <meta property="og:image" content="***" /> の *** 部分に指定されることになっているのでその値を取り出します。そして再度その画像 URL を fetch してバイナリデータを取り出し、agent 変数を使ってアップロードします。そのアップロード結果の情報等を使って OGP の埋め込みデータ(embed_params 変数)を作成します。埋め込みデータを作ることができたら、その値を embed パラメータに加えて post する、という流れになります。これで OGP に対応したポスト処理を Node.js で行うことができるようになりました。


【サンプルソースコード公開】
後はこの実行を自動化することになりますが、今回は自分が使っているサーバーの crontab を使って日本時間の午前零時ちょうどに実行するようにしました。具体的なコードは後述しますが、大まかには、
(1)日本時間午前零時になったら、マンホールマップの「今日のマンホール」取得 API を呼び出し、
(2)その結果からリンク先 URL やポストするテキストを生成し、
(3)リンク先 URL の OGP 情報を使って画像やタイトルなどを取り出して、埋め込み、
(4)facets 処理を加えた上でポストする

という流れを行っています。そしてそのサンプルソースコードを以下で公開しています:
https://github.com/dotnsf/bsky_manhotalk_bot/blob/main/motd.js


上では解説していない部分を1点だけ。上述のように現状の Bluesky API は認証情報を ID とパスワードを直接指定する形で実行する必要があります。上のリンク先を見るとわかるのですが、今回公開したサンプルソースコードには ID やパスワードは含まれておらず、このソースコードを実行する際の環境変数から取得するようにしています。したがってこのソースコードを実行する時に、
$ BSKY_ID=myname@email.com BSKY_PW=mypassword node motd.js

といった具合に BSKY_ID 環境変数と BSKY_PW 環境変数にそれぞれ Bluesky へのログイン ID とパスワードを指定して実行することで正しく動くようにしています。

そうして作ったマンホールマップ・ボットはこちらのアカウントで公開しています:
https://bsky.app/profile/manholemap.bsky.social

2024021200


↑ツイッターとちがって Bluesky のアカウントを持っていない人でも見ることはできますが、可能であればアカウントを作ってフォローしてください。「今日のマンホール」が登録されている日全てで午前零時になると教えてくれます(登録されていない日もあります)。

とりあえず現時点では「今日のマンホール」専用で動くボットですが、いずれハッシュタグが実装されたら #manhotalk ハッシュタグを検索して・・といった挙動や、ちょっとした会話にも挑戦するつもりでいます。

自作サービスを開発している途中で調査した内容のアウトプットです。

Node.js を使って PostgreSQL データベースを操作する場合、pg(node-postgres) ライブラリを使うのが定番だと思っています。実際これまで何度も使ってきているし、データの読み書き更新削除といった作業で特に困ったことはありませんでした。

しかし今回ちょっとしたことで詰まってしまいました。結果的には解決できたのですが、データベース内に定義されたテーブルの、列の定義情報を調べたいと思った時に「これどうやるんだろ?」となってしまいました。

もう少し具体的に説明します。例えば以下のような SQL を使って items テーブルを作成したとします:
CREATE TABLE items( id varchar(50) primary key, name varchar(100) not null, price int default 0, body text, image bytea, datetime timestamp );

このようにして作成した items テーブルの各列の定義情報(上の例の青字部分)を取り出す方法が分からなかったのでした(列名だけであれば select 文の実行結果の中に含まれるので、1行でもレコードが登録されていればそこから分かる、ということは知っていました。が、レコードが1件も登録されていないケースだったり、列名以外の型の情報まで必要な場合の取得方法が分かっていませんでした)。ちなみにこの情報は psql コマンドを使った場合はログイン後に
# \d items

というコマンドを実行することで取得できることは知っていました("items" の部分に知りたいテーブル名を指定して実行します):
db=# \d items
                          Table "public.items"
  Column  |            Type             | Collation | Nullable | Default
----------+-----------------------------+-----------+----------+---------
 id       | character varying(50)       |           | not null |
 name     | character varying(100)      |           | not null |
 price    | integer                     |           |          | 0
 body     | text                        |           |          |
 image    | bytea                       |           |          |
 datetime | timestamp without time zone |           |          |
Indexes:
    "items_pkey" PRIMARY KEY, btree (id)

この方法を知っていたので、これまであまり気にすることもありませんでした。ところがこれはあくまで psql コマンドを利用する際のコマンドであって、これをそのまま SQL として pg を使って実行すると(SQL ではないので当然ですが)エラーとなってしまいます。ではいったいどうすれば pg でこの情報をプログラムのコード内で取り出すことができるのだろうか・・・ というのが今回のブログエントリのテーマです。


結論として分かったのは、こんな感じでした:
・SQL としては実行結果にすべての列が含まれるような SELECT 文(例: "select * from items")を実行する
・実行結果からレコードを取り出す場合は result.rows を参照するが、実行結果の列情報は result.fields と result._types._types.builtins を参照することで取り出すことができる
・実行結果のレコードが0件でも(1件もレコードが登録されていなくても)、上の方法で列情報を取り出すことはできる

具体的なコードとしてはこのような感じです:
var PG = require( 'pg' );
var pg = new PG.Pool({
  connectionString: "postgres://user:pass@hostname:5432/db",
  idleTimeoutMillis: ( 3 * 86400 * 1000 )
});

  :
  :

if( pg ){
  var conn = await pg.connect();
  if( conn ){
    try{
      var sql = 'select * from items';
      var query = { text: sql, values: [] };
      conn.query( query, function( err, result ){
        if( err ){
          console.log( err );
        }else{
          var fields = r1.result.fields;
          var types = r1.result._types._types.builtins;
          var columns = [];
          fields.forEach( function( f ){
            var dt = Object.keys( types ).reduce( function( r, key ){
              return types[key] === f.dataTypeID ? key : r;
            }, null );
            columns.push( { column_name: f.name, type: dt } );
          });

            :
            :
        }
      });
    }catch( e ){
      console.log( e );
    }finally{
      if( conn ){
        conn.release();
      }
    }
  }
}


赤字の部分の解説をします。まず "postgres://(ユーザー名):(パスワード)@(PostgreSQL サーバー名):(ポート番号)/(DB名)" というフォーマットの接続文字列を使ってデータベースに接続します(正しく接続できるのであれば、このフォーマットである必要はありません)。 そして接続後に "select * from items" というシンプルな SQL を実行して、結果を result という変数で受け取ります。この SQL 実行結果(レコード情報)自体は result.rows という属性に配列形式で格納されているのですが、今回ここは使いません。

この SQL を実行することにより、指定したテーブル(今回の場合は items)の列名とデータ型IDの情報が result.fields に、データ型IDとデータ型の関係を示す表が result._types._types.builtins に格納されているはずです。これらを取り出し、各列のデータ型を ID ではなく文字列に変換しなおして、最終的に columns という配列変数に記録しています。

この columns の実行結果を参照すると、このような値になっているはずです:
    [
      {
        "column_name": "id",
        "type": "VARCHAR"
      },
      {
        "column_name": "name",
        "type": "VARCHAR"
      },
      {
        "column_name": "price",
        "type": "INT4"
      },
      {
        "column_name": "body",
        "type": "TEXT"
      },
      {
        "column_name": "datetime",
        "type": "TIMESTAMP"
      }
    ]

"integer" 型が、より正確な "INT4" という型になっていたりはしますが、当初取得したかった列の定義情報を取得することができました。なお、この方法であれば SQL の実行結果(result.rows)そのものを参照しているわけではないため、実行結果が0件であっても(レコードがまだ1件も登録されていない場合でも)実行できるようです。

サンプルソースコードはこちらからどうぞ:
https://github.com/dotnsf/pg_fieldtype


(2023-12-18 追記)
ちなみに MySQL の場合、その名もズバリの mysql ライブラリを使うのが定番だと思ってますが、こちらの場合はテーブル一覧("show tables")もテーブル定義("desc (テーブル名)")も、CLI で使う命令文をそのまま利用して取得することができるので、深く考えなくてもよいのでした。




このページのトップヘ