Tech 4 Mine :-) 忘れぬ先のテックメモ

ゆるエンジニアな私が、多少なり役に立ったひらめきやらTipsを忘れる前に書いていくブログです。

Web3.0なONT IDの簡単お試し用キットを作ってみる

Overview

Ontologyというブロックチェーン企業が押し出しているWeb3.0基準のIdentityサービス、ONT IDでは、QRコードで複数のサービスの認証を紐づけたりユーザの証明が出来るようです。
ONT ID:https://ontid.ont.io/

そこで、ちょっとしたご縁もあったので認証のために使ってみたら、意外ととりあえず動かす(HelloWorld的な)のが大変だったので、Dockerを利用してぽんぽーんとお試し出来るようにしてみました。

この記事の内容はこちら ー> https://github.com/Mine9999/ontlogin-sample-docker

実行環境

docker v20.10.11
docker-compose 1.25.0
ubuntu 20.04.3 LTS on wsl2(互換のLinuxコンソールならなんでも)
+ネット環境
+ONTOアプリ (https://onto.app/)
フロントはVue.js、バックエンドはGolangで構成されています。(今回はコード自体はいじりません)

実行方法

まずは実行方法から言っちゃいましょう、必要なものは↑に書いたものと、そのまま実行するのであればローカルホストの3000番と8080番ポートを利用します。

※ここからはLinuxコンソール(Docker,Docker-compose インストール済み想定)でお送りします。

最初にやることはリポジトリのクローンですね。

$ git clone https://github.com/Mine9999/ontlogin-sample-docker.git

クローンがおわったら、出来たディレクトリに移動します。

$ cd ontlogin_sample_docker

クローンしたフォルダの中に、biuld.shがあるのでそれを実行します。

$ ./build.sh

go: downloading github.com/go-chi/cors v1.2.0
go: downloading github.com/go-chi/chi/v5 v5.0.3
...
patch-package 6.4.7
Applying patches...
ontlogin@0.0.8 ✔
## ここまででバックエンドのGoのビルド完了
...
vite v2.5.0 building for production...
transforming...
✓ 15 modules transformed.
rendering chunks...
dist/index.html                  0.38 KiB
dist/assets/index.c2ecea99.js    1.71 KiB / brotli: 0.74 KiB
dist/assets/vendor.c94fb908.js   65.01 KiB / brotli: 22.47 KiB
## ここまででフロント側vue.jsのビルド完了

ビルドが完了すると、それぞれ実行ファイルが生成されるので、準備完了です。
コンテナを立ち上げてアクセス出来るようになります。

$ docker-compose up -d
Creating network "ontlogin-sample-docker_default" with the default driver
Creating ontlogin-sample-docker_backend_1 ... done
Creating ontlogin-sample-docker_web_1     ... done

それでは http://localhost:8080 にアクセスしてみましょう。

ボタンが表示されるのでSing in with ONT LOGINを押します。

そしてでてきたQRコードをONTOのアプリで読み込みます。

認証が成功すると読み取ったウォレットのアドレス(ONT ID)が表示されます。

利用シーンや注意点など

他にもメールアドレスや、色々と必要な認証情報がセット出来るようですが、デモではIDのみ取得するようにしています。
QRコードを読むときは、ONTOアプリをアイデンティティーモードにして置く必要があります。  アンドロイドの場合 プロフィール → システム設定 → アイデンティティーモードをON

実際には、フロント側のコードを既存のページに埋め込んだり、リダイレクト先を指定したりして使うことになりそうですね。
認証時にアプリ側に出てくる情報や、実際に認証しに行くチェーンは↓で設定されています。
https://github.com/Mine9999/ontlogin-sample-docker/blob/main/backend/service/service.go

また、結果を受けてONT IDを表示しているところは↓です、ここを書き換えて特定のページにリダイレクトしたり、取得した認証情報が登録済みかなどの検証を行うイメージでしょうか。 https://github.com/Mine9999/ontlogin-sample-docker/blob/3cdb44b651adebbadc4640b2c0650566bcf3f7fb/frontend/src/App.vue#L45

参考にさせて頂いた元コードなど

バックエンドの実装
https://github.com/ontology-tech/ontlogin-sample-go

フロントエンドの実装
https://github.com/ontology-tech/ontlogin-sdk-js/tree/main/example/vue-demo

traefikでリバースプロキシ ON Docker With TLS

あうとらいん

コンテナはなーんにも考えずにサクッと作ってデプロイしたい・・・ってことでリバプロにまるっと任せたい!
ということでDocker上で簡単に構築できて、コンテナの追加にも影響がないTraefikを使ってみました。

環境

traefik v2.5.2
Docker(ce) 20.10.3
docker-compose 1.28.2
DNSプロバイダ ConoHa
サーバ環境ConoHa VPS(Ubuntu18.4)
証明書発行 Let's Encrypt

Docker-compose での基本設定

Composeの共通的な部分はここでは触れません。
特殊な理由がなければ好みの問題かと思いますが、traefikの設定は複数の書き方があります。
公式ドキュメントにある通りDockerだけでもyaml、toml、cliで記述出来ます。
今回はcomposeファイルに記載したいので、cliの書き方をベースにTraefikコンテナの起動コマンド設定を書いていきます。

Traefik特有な部分を以下に見ていきます、ということでyamlファイルどん。

version: "3.3"

services:
  traefik:
    image: "traefik:v2.5.2"
    container_name: "traefik"
    restart: always
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.websecure.http.tls=true"
      - "--entrypoints.websecure.http.tls.certResolver=myresolver"
      - "--entrypoints.websecure.http.tls.domains[0].main=yourdomain.com"
      - "--entrypoints.websecure.http.tls.domains[0].sans=*.yourdomain.com"
      - "--certificatesresolvers.myresolver.acme.dnschallenge=true"
      - "--certificatesresolvers.myresolver.acme.dnschallenge.provider=conoha"
      # - "--certificatesresolvers.myresolver.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53"
      # - "--certificatesresolvers.myresolver.acme.dnschallenge.delaybeforecheck=5"
      # - "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
      - "--certificatesresolvers.myresolver.acme.email=yoursubmitmail@email.com"
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    environment:
      - "CONOHA_REGION=tyoX"
      - "CONOHA_TENANT_ID=xxxxxxxxxxxxxxxxxxx"
      - "CONOHA_API_USERNAME=yourusername"
      - "CONOHA_API_PASSWORD=yourpassword"
      - "CONOHA_POLLING_INTERVAL=30"
      - "CONOHA_PROPAGATION_TIMEOUT=3600"
      - "CONOHA_TTL=3600"
    volumes:
      - "./letsencrypt:/letsencrypt"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./logs/:/var/log/traefik/"
    networks:
      - proxy
networks:
  proxy:
    driver: bridge
    ipam:
      driver: default

解説

command部分の各設定をみていきます。今回はあくまでTLS化することを主体としているため、「とりあえず動かす」ためには不要な箇所も多いです。

      ## ロバイダ指定、trueにすることでDockerコンテナを能動的に取得、プロキシ下に置く
      - "--providers.docker=true"
      ## デフォルトでコンテナを管理化に収めるかのフラグ、falseにした場合、対象コンテナに traefik.enable=true の設定が必要
      - "--providers.docker.exposedbydefault=false"
      ## Webアクセスをどこで許可するかの指定、webはアクセスポイント(AP)名で任意に設定する。80番ポートで受けるリクエストはWebというAPとして扱う
      - "--entrypoints.web.address=:80"
      ## リダイレクト先のAP名を指定する。websecureというapにリダイレクトする設定
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      ## リダイレクト先のAPがtls化されている場合はスキーマがhttpからhttpsへ変わるためhttpsを指定
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      ## こちらはtls用のap設定、443でtlsを受けるよう定義
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.websecure.http.tls=true"
      ## TLS化のために後述の証明書リザーバーを指定する
      - "--entrypoints.websecure.http.tls.certResolver=myresolver"
      ## 取得したいドメイン名を指定、ワイルドカード証明書はDNSチャレンジで証明書を取得する場合のみ有効
      - "--entrypoints.websecure.http.tls.domains[0].main=yourdomain.com"
      ## 子の証明書にワイルドカード証明書を指定
      - "--entrypoints.websecure.http.tls.domains[0].sans=*.yourdomain.com"
      ## DNSチャレンジで証明書を取得するためtrueに設定
      - "--certificatesresolvers.myresolver.acme.dnschallenge=true"
      ## プロバイダ指定、プロバイダ名はTraefik公式ドキュメントを参照のこと
      - "--certificatesresolvers.myresolver.acme.dnschallenge.provider=conoha"
      ### コメントアウト部分はLet's Encryptのテスト用サーバ設定とdnsチャレンジがうまく行かなかった場合のディレイ設定
      # - "--certificatesresolvers.myresolver.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53"
      # - "--certificatesresolvers.myresolver.acme.dnschallenge.delaybeforecheck=5"
      # - "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
      ## 証明書の発行等の通知を受けるメールアドレス
      - "--certificatesresolvers.myresolver.acme.email=yourmail@email.com"
      ## acmeファイルの格納先、後続の関係で固定推奨
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"

この辺の詳細や追加情報はTraefik let's Encrypt等で検索すると公式ドキュメントがあります。
今回はエントリーポイントにwebとwebsecureを作り、80番ポートを443番ポートにリダイレクト、http → https にリダイレクトする形の設定をしています。

Port部分は特に触れませんが、80番と443番をマッピングしています。
次はenvironment部分です。
commandのdnsチャレンジプロバイダによって全く異なりますが、今回どっぷりハマってしまったこともあり注意して追いかけてみます。

      ## REGIONはConoHaのAPIで表示されているものを入れましょう、tyo1、tyo2(東京リージョンの場合)などです
      - "CONOHA_REGION=tyoX"
      ## この辺りは素直にConoHaのアカウント情報を入力
      - "CONOHA_TENANT_ID=xxxxxxxxxxxxxxxxxxx"
      - "CONOHA_API_USERNAME=yourusername"
      - "CONOHA_API_PASSWORD=yourpassword"
      ## ココでハマりました、DNSの浸透速度が遅いせいか、TIMEOUT値やTTLの値がデフォルトのままだとチャレンジ失敗します
      - "CONOHA_POLLING_INTERVAL=30"
      - "CONOHA_PROPAGATION_TIMEOUT=3600"
      - "CONOHA_TTL=3600"

というわけで上記の通り、ConoHaのDNSは浸透が遅いのか、タイミング問題なのかは不明ですが、TIMEOUTとTTLの値を伸ばし、それに伴いポーリングの感覚も30秒に広げています。
これにより、デフォルト値ではacmeレコードをDNSに登録したところで、浸透するより早くTIMEOUT扱いになることがなくなりました。
その際のログが下記のように出ます。

traefik    | time="2021-09-02T14:30:23Z" level=error msg="Unable to obtain ACME certificate for domains \"yourdomain.com....

続いてはVolume部です、ここも定型的に入れてしまっていいでしょう。

      ## acmeファイル(証明書)の格納場所をマウントする
      - "./letsencrypt:/letsencrypt"
      ## Dockerをプロバイダとして登録するため、ソケットをマウント
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      ## traefikのログをマウント
      - "./logs/:/var/log/traefik/"

最後にネットワークについてですが、これはtraefik用のDocker networkを作っているだけなので割愛します。
重要なのは、ここで作成したTraefik用ネットワークを、管理対象のコンテナにも反映することです(external利用)。

コンテナ側のdocker-compose

version: "3.3"

services:
  web:
    image: webapps image
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.web_router.rule=Host(`web.yourdomain.com`)"
      - "traefik.http.routers.web_router.entrypoints=websecure"
    expose:
      - "3000"

networks:
    default:
        external:
            name: traefik_proxy

実際に動かすimageや起動コマンドは各個の環境に合わせてください。(ココでは開設に必要な部分のみ記載しています)

特にtraefik経由でリバプロさせるために重要な部分はlabels部分ですね。
ここにコンテナ別の必要な設定を記載していきます。
traefik.enable=true ・・・traefikの管理対象であること明示するフラグ、必須
traefik.http.routers.web_router.rule ・・・HostやPathでルーティング先を選別するための設定、上記ではドメインの先頭にweb~とついていたらこのコンテナにルーティングするように定義している traefik.http.routers.web_router.entrypoints ・・・どのAPからのアクセスを対象とするかを指定、websecureとしているがここはtraefik側の設定で記載が必要(※)
※ コンテナごとに設定かけなくもないがひっじょーうに長くなりかねないのでtraefikに巻いてしまうことをおすすめする。

このWebアプリケーションがデフォルト3000番で待受ていた場合、exposeで3000番ポートを内部公開する。
Traefikでは内部公開されているポートを自動的に拾い、リダイレクトする際に対象のポートを使ってくれるため、ホスト側のポートをマウントしないexposeで事足りる。

最後にnetworksの定義だが、externalをつけてtraefikを起動した際に生成されたネットワークに参加させる必要がある。
ここではtraefikフォルダでtraefikのdocker-composeを起動したので、traefik_proxyが対象となる。

あとがき

最近筆が止まっていたので(色々とお仕事頂いていたのと、子どもをひたすら愛でていた)あとから追記編集するつもりでバリバリと書きなぐっていくことにしました。
肝心のTraefikですが、非常に使いやすいですね。日本語の資料が少ないですが、公式のドキュメントが手厚いのでなんとか使えています。
ロゴがGolangのキャラクターをパロってるので、なかなかお硬い現場では気を使いますね!
Traefikは何よりもコンテナを上げ下げしたときに動的にルーティングの追加・削除をしてくれるところが素晴らしいです。
軽量なリバプロということでPoundを使っていたこともありましたが、使いやすさや機能面含め、シンプルな環境ではリバプロのベストプラクティスになりうるのでは、と感じています。
以下その他の役に立った気がするTipsを載せておきます。(適宜追記予定)
それでは良いコンテナライフを・・・!

その他Tips

ダッシュボードをセキュアにする

command:
      - "--api.dashboard=true"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.myresolver.acme.dnschallenge=true"
      - "--certificatesresolvers.myresolver.acme.dnschallenge.provider=yourprovider"
      - "--certificatesresolvers.myresolver.acme.email=your.address@email.com"
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(`your.domain.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.middlewares.dashboard-auth.digestauth.realm=realmword"
      - "traefik.http.middlewares.dashboard-auth.digestauth.users=account:realmword:abcdefgh1234567890xxxxxxxxxxxxxx"
      - "traefik.http.routers.dashboard.entrypoints=websecure"
      - "traefik.http.routers.dashboard.middlewares=dashboard-auth"
      - "traefik.http.routers.dashboard.tls.certresolver=myresolver"
      - "traefik.http.routers.dashboard.tls.domains[0].main=your.domain.com"

URLから指定のディレクトリを削除する

以下をリバプロ内のアプリケーションのcomposeファイルに記述する

labels:
      ## yourdomain.com/sample/app1 -> yourdomain.com/app1 となる
      - "traefik.http.middlewares.samplemiddle.stripprefix.prefixes=/sample"
      - "traefik.http.routers.samplerouter.middlewares=samplemiddle@docker"

Headerにパスを追加する

labels:
      - "traefik.http.middlewares.testHeader.headers.customrequestheaders.X-Script-Name=test"
      - "traefik.http.middlewares.testHeader.headers.customresponseheaders.X-Custom-Response-Header=value"

オフラインサーバにnpmboxでForeverをインストールする

新たに構築する際はいいのですが、運用中の機能追加や構成変更で閉域網にnode moduleを入れたいなーなんてこと、ありませんか?ありますよね?
というわけで、今回はnodeプロセスをデーモン化するのにForeverを使いたくなり、色々と調べて実際に出来た方法を記して行きます。

環境など

必要な資材をオンライン環境のあるマシンで準備し、オフライン環境のマシンに移行してセットアップする流れとなります。
オン/オフ サーバ2台も用意するのは面倒なので、DockerHubでNode用のコンテナイメージを取得して検証してみます。
Dockerのインストールや起動コマンドなどはここでは省略します。
Nodeのコンテナイメージを使うので、npmやnode自体はインストールされているものとします。

オンラインサーバで資材を作成

まずはオンラインで資材を作成するためにnpmboxをグローバルインストールします。

root@a377a7db92f5:/# npm install -g npmbox

added 74 packages, and audited 338 packages in 16s

途中でWARNでバージョンが古いやつがいるとかなんとか言われますがここではスルーしておきます。
次はForeverをnpmboxで固めます。

root@a377a7db92f5:/# npmbox forever
Boxing forever...

Packing /forever.npmbox...

root@a377a7db92f5:/# ls -ltr | grep npmbox
-rw-r--r--   1 root root 5057784 Mar  6 01:08 forever.npmbox

途中ズラッとログが流れて、Packing~となれば完了です。forever.npmboxというファイルが出来ています。
次にnpmbox自体の資材をまとめて行くために、グローバルのNode Modulesの場所や、npmboxのコマンドを確認します。

root@a377a7db92f5:/# npm config list | grep location
; node bin location = /usr/local/bin/node

root@a377a7db92f5:/# ls -ltr /usr/local/bin/ | grep npm*box
lrwxrwxrwx 1 root root        39 Mar  6 01:05 npmunbox -> ../lib/node_modules/npmbox/bin/npmunbox
lrwxrwxrwx 1 root root        37 Mar  6 01:05 npmbox -> ../lib/node_modules/npmbox/bin/npmbox

npmのコンフィグから、nodeのバイナリの位置を確認、同フォルダをnpmboxとnpmunboxが対象となるように検索します。
npmboxとunboxはどちらもlib/node_modules/npmboxを参照しているようなので、これを持っていってあげれば良さそう、ということでtarしておきます。

root@a377a7db92f5:~# tar -cvzf npmbox.tgz /usr/local/lib/node_modules/npmbox

ここまでで、npmboxとforeverのインストール用資材は揃いました。

資材の移動

本来であれば、ftpなどか、デバイスを経由して資材を移動しますが、今回はコンテナ間(node_1→node_2)を移動します。

~Documents$ docker cp node_1:/root/forever.npmbox ./
~Documents$ docker cp node_1:/root/npmbox.tgz ./
~Documents$ ls -ltr | grep npmbox
-rwxrwxrwx 1 user user 5057784 Mar  6 10:08 forever.npmbox
-rwxrwxrwx 1 user user 4482734 Mar  6 10:16 npmbox.tgz

~Documents$ docker cp ./forever.npmbox node_2:/root/
~Documents$ docker cp ./npmbox.tgz node_2:/root/

しました。
というわけでnode_2(オフライン想定のコンテナ)のコンソールに入り、インストール作業をしていきます。

オフラインサーバでの構築

まずはnpmboxのインストール用に、tarした後にnpmのグローバルモジュール置き場にnpmboxフォルダを移動します。

root@44ebad748d6e:~# tar -xvzf npmbox.tgz

root@44ebad748d6e:~# ls -ltr
total 9324
-rwxrwxrwx 1 node node 5057784 Mar  6 01:08 forever.npmbox
-rwxrwxrwx 1 node node 4482734 Mar  6 01:16 npmbox.tgz
drwxr-xr-x 3 root root    4096 Mar  6 01:29 usr

解凍時のログは省略しますが、解凍されたフォルダが出来ています。
基本的にディレクトリ構成は同じなので、特に調べ直しませんが、npmのグローバルモジュール置き場に移動させます。

root@44ebad748d6e:~# cd /usr/local/lib/node_modules/
root@44ebad748d6e:/usr/local/lib/node_modules# cp -r /root/usr/local/lib/node_modules/npmbox ./
root@44ebad748d6e:/usr/local/lib/node_modules# ls -ltr
total 8
drwxr-xr-x 10 root staff 4096 Mar  3 05:50 npm
drwxr-xr-x  4 root root  4096 Mar  6 01:31 npmbox

残るはコマンドを設置するだけです。
今回はlnコマンドで配置したnpmboxを参照するようにリンクを作成します。

root@44ebad748d6e:/usr/local/lib/node_modules# cd /usr/local/bin/

root@44ebad748d6e:/usr/local/bin# ln -s /usr/local/lib/node_modules/npmbox/bin/npmbox npmbox
root@44ebad748d6e:/usr/local/bin# ln -s /usr/local/lib/node_modules/npmbox/bin/npmunbox npmunbox
root@44ebad748d6e:/usr/local/bin# ls -ltr
total 73968
-rwxr-xr-x 1 root staff 75737496 Mar  3 05:50 node
lrwxrwxrwx 1 root staff       38 Mar  3 05:50 npx -> ../lib/node_modules/npm/bin/npx-cli.js
lrwxrwxrwx 1 root staff       38 Mar  3 05:50 npm -> ../lib/node_modules/npm/bin/npm-cli.js
-rwxrwxr-x 1 root root       116 Mar  3 23:28 docker-entrypoint.sh
lrwxrwxrwx 1 root root        19 Mar  3 23:28 nodejs -> /usr/local/bin/node
lrwxrwxrwx 1 root root        29 Mar  3 23:28 yarnpkg -> /opt/yarn-v1.22.5/bin/yarnpkg
lrwxrwxrwx 1 root root        26 Mar  3 23:28 yarn -> /opt/yarn-v1.22.5/bin/yarn
lrwxrwxrwx 1 root root        45 Mar  6 01:32 npmbox -> /usr/local/lib/node_modules/npmbox/bin/npmbox
lrwxrwxrwx 1 root root        47 Mar  6 01:32 npmunbox -> /usr/local/lib/node_modules/npmbox/bin/npmunbox

ここまで出来たらnpmboxやnpmunboxを実行してみましょう、引数なしで実行すればヘルプが表示されるはずです。
実行されないようであれば、パスの指定やコマンド指定が間違ってなかったか見直してみてください。

npmboxのコマンドがちゃんと反映されているのを確認したら、資材を配置したディレクトリに戻ってforeverのインストールを行います。
npmunboxコマンドをつかってグローバルインストール、インストール後に動作確認してみます。

root@44ebad748d6e:~# npmunbox -g forever.npmbox

Unboxing forever.npmbox...
  Unpacking forever.npmbox...
  Installing forever...
  Done.
root@44ebad748d6e:~# forever list
info:    No forever processes running

お疲れさまでした、ここまででforeverのインストールまで完了しました。

終わりに

npmboxは依存関係まで合わせてパッケージ化してくれるので非常に便利ですね、ありがたい限りです。
最近はOpenAPI GeneratorでNode-expressのテンプレソースをいじったりしてたので、その後のデプロイ関係で今回のnpmboxのネタが出来ました。
覚えてるうちに時間があればOpenAPI generator(のNode-express)についてもまとめておきたいですね。

Pug(Jade)で役に立ったアレコレ

Pug(Jade)で役に立ったアレコレ

node.jsでviewを書く時にPugを使い始めました。
アレコレ実現までに時間がかかったものを忘れないように残しておきます。

scriptタグ内でNodeから渡された値を利用する①

res.renderでパラメータをviewに渡したときに、scriptタグ内で直接使おうとしたらうまくイカなかった。
js // node側の設定 res.render('sample',{ param1: _param1, param2: _param2 }) ````js //- sample.pugの記述 p(hidden='')#p1 #{param1} p(hidden='')#p2 #{param2}

script. let p1 = document.getElementById('p1').textContent; let p2 = document.getElementById('p2').textContent;

console.log('param1:' + _p1);
console.log('param2:' + _p2);

````

pタグをHidden属性で定義して、そのテキストに使いたいパラメータを定義する。
getElementByIdを使ってscript内でテキストを取得、変数に格納する、この2段階でscriptタグ内で変数が使えるようになりました。

scriptタグ内でNodeから渡された値を利用する②

画面ロード時に表を作りたかったので配列をnodeから渡す時に配列全体を文字列可してやろうとしたら"'・・・。

script.
  window.onload = () => {
    funcHoge(#{data});
  }

ちなみにdataの中身は配列です。

let data = ['a','b','c','d'];

ですがこのまま渡すと、シングルクォートなどの特殊文字がHTMLように変換されてしまいます。(それが"や'など)
ということで、pugで特殊文字を含む文字列を渡せるようにします。

script.
  window.onload = () => {
    funcHoge(!{data}); // # -> ! に変更
  }

さらにさらにこのままだと文字として展開されたあとにスクリプトを処理するので、配列全体を文字列化、変数に定義した後に関数にわたすことにします。

let data = ['a','b','c','d'];
data = '["' + data.join('","') + '"]';
// dataを '["a","b","c","d"]' のように文字列化する。

viewの呼び出し側で上記の通り1個の文字列として加工して、view側のpugで下記のように記述します。

script.
  window.onload = () => {
    let _data = !{data};
    // デコード後は let _data = ["a","b","c","d"]; となる。
    funcHoge(_data);
  }

多次元配列の場合も同様に、個別にjoin()したあとに全体を文字列としてつなげます。
その際に、'["' と '"]' は自前で追加しないとjoinでは消えてしまうため、入れるのをお忘れなきよう。

繰り返し処理にIndexをつかう

each ~ in でリストなどを繰り返し処理(表化)するときに、選択した対象のIndex(行数)を取得したり、行をユニークに見分けたい。 ごくごく単純なことですが、下記の書き方で出来るようです。

  each value, index in values
   ~

例えば下記のようにして、セレクトボックスの選択しているパラメータが重複していないかチェックするのに利用しました。

  each value, index in values
    select(id=`index_${index}` name="id" onChange=`dupeCheck('index_${index}');`)
      each ...
        option(...)

細かいことは別記事にしますが、セレクトボックスごとにidを割り振り、値を変更したタイミングでdupeCheckで他の行で同じものが選択されていないかチェックしています。

雑記

特に困った(調べたり理解に時間がかかった)ものだけと思って書いてみたら意外と少ない。。。もっと色々悩んだはずなんですが、喉元すぎればってやつでしょうか。
忘れてるだけのものもありそうなので思い出し次第、また出くわし次第追記、出来たら良いなぁ。

Docker HubからDockerコマンドを使わずにイメージをダウンロードする

こんな状況に

ネットは繋がるけど、ユーザ権限が制限されてるWindowsからネットに繋がってない開発機に色々入れる必要がある。
Dockerは入れられたけどコンテナイメージは入れてなかったサーバにイメージ入れたいけど、使える端末はWindowsでOS機能が制限されていてDocker Desktopのデーモンが起動できない。

Docker Registory HTTP API

DockerにはAPIがあり、ざっくりと認証トークン取得→マニフェスト取得→BLOB取得→イメージ統合することでdocker save相当のイメージファイルがゲットできるようです。
マニフェスト取得までは自力でやってみましたが、既にPythonPowerShellでもスクリプト化してくれている方がいたので、挫折ついでにいくつか使ってみました。

Dockerを使わずにDocker Imageを取得するツールあれこれ

. docker-drag(docker_pull.py) 今回試したものその1。
docker-dragというPythonスクリプト、requestsを入れる必要有り。

. download-frozen-image-v2.sh

今回試したものその2。
Mobyプロジェクトに含まれるDocker Imageダウンロード用シェルスクリプト、jqとgoコマンドを入れる必要有り。

. Scopeo

試してないものの、GitHubでスター2.4Kと人気なプロダクトのようです。
今回はWindows非対応とのことで除外。

お試し1:docker-drag

以下の環境でお試ししています。
Windows 10
Python v3.8.2

Pythonについてはここではあまり触れません、インストール後、環境変数設定しておいてください。
そもそもPythonインストールなんて出来ないよ!って方はお試し2を御覧ください。

Pythonのインストールが終わったら依存モジュールをインストールします。

pip install requests

pipで入れられない!って方はPyPiのHPから、requestsと依存モジュール4個をダウンロードして、それぞれローカルインストールしましょう。
requestsのリポジトリから依存関係は下記のとおりです。

'chardet>=3.0.2,<4',
'idna>=2.5,<3',
'urllib3>=1.21.1,<1.26,!=1.25.0,!=1.25.1',
'certifi>=2017.4.17'

環境が整ったらReadmeにある通り実行するだけです。

// python docker_pull.py [image名]:[タグ]
python docker_pull.py mysql/mysql-server:8.0

プロキシ環境でhttps(Port 443) タイムアウトする場合はPythonのプロキシ設定をしましょう。

お試し2:download-frozen-image-v2.sh

以下の環境でお試ししています。
Windows 10
cygwin(WSLが利用不可な環境だったため) jq go 1.15

このシェルが、goとjqに依存するため、実行前にこの2つをセットアップしておきます。
goについては、Windows用のインストーラダウンロードして予めインストールしておきます。
bash的なことをするためcygwinをインストールして、ライブラリを追加する時に、jqを追加しておきます。ほかは特に変更なくて大丈夫でした。
WSLで実行される場合はaptやらyumやらでjqを調達する必要があります。(試してない)

セッティングが終わったら、以下の様にコマンドを実行することでイメージをダウンロード出来ます。

// ./download-frozen-image-v2.sh [DLディレクトリ] [image名]:[タグ]
./download-frozen-image-v2.sh ./ ubuntu:latest

プロキシ環境下ではPython同様、Proxy設定して実行してみてください。

PythonとかCygwinとか無理なんだけど

っていう方はPowerShellで同じようなものを作成されてる方がいましたので、そちらを参考にしてみてください。
PowerShell見ると眠くなる呪いさえなければ・・・

dockerless_docker_downloader

もっと便利なツールなどあればぜひとも教えて下さい!

robot.hear/respondなどのマッチ条件を動的に定義する

仕組み?

DBにマッチ条件の文字を登録->スクリプト内で読み込み->ループ処理
やあ、やぁ、こんにちは、おはよう的なサムシングに対して、こんにちは!ぼくはぼっと!と返す処理を作ります。
そして最終的にいい感じにします。

ファイル構成

  scripts(Hubotのscriptsディレクトリ)  
  ├ index.js  
  ├ register_api.js  
  └db  
    ├ files  
    │  └ definition.db  
    ├ nedb_wrapper.js  
    ├ file_db_controller.js  
    └ mem_db_controller.js  

多分こうなります。気持ちいらないものもありますがご愛嬌ってことで。。。

HowTo

NeDBを利用してデータ操作を行うので、こちらの記事でNeDBの実装をしておきます。
Hubotと上記のNeDBを前提にしています。

データの準備

ひとまずJson形式で反応するキーワードリストを作りましょう。

const json_data = {
    key: "keywords",
    value: ["やあ","やぁ","こんにちは","おはよう"]
}

これをDBにinsertします。今回はファイル、メモリどちらでもOKですが、登録内容の確認にも便利なので最初はファイルDBとして初期化することをオススメします。
ログ出力やエラー処理は省略。

const db = require('./db/file_db_controller');

db.insertQuery(json_data, db.GetConnection);

データの準備ができました。scriptフォルダ以下に早速待受処理を書いていきます。

value正規表現にして判定に利用する

現状は1レコードのみなので、適当にDBからレコードを抽出、Valueの配列をfor文で回します。

const query = {
    key: "keywords"
}
db.findAllQuery(query, db.GetConnection)
.then(param => {
    for(let word of param.value){
        let regexp = new RegExp(word, 'i');

        robot.hear(regexp, (res) => {
            res.send("こんにちは!ぼくはぼっと!");
        });

    }
})

はい、できました。
挨拶は大事ですが、これでは応答が固定で、ひたすら挨拶する残念な子になってしまいますね。

応答メッセージもセットにする

せっかくJSON形式を使うので応答メッセージもおなじレコードに入れ込みましょう。
json_dataに要素を追加します。

const json_data = {
    key: "keywords",
    value: ["やあ","やぁ","こんにちは","おはよう"],
    response: "こんにちは!ぼくはぼっと!"  // responseとして回答テキストを追加
}

読み込み~応答部分も変更しましょう。

const query = {
    key: "keywords"
}
db.findAllQuery(query, db.GetConnection)
//.then(param => {
.then(params => {
    for(let param of params){
        for(let word of param.value){
            let regexp = new RegExp(word, 'i');

            robot.hear(regexp, (res) => {
                //res.send("こんにちは!ぼくはぼっと!");
                res.send(param.response);
            });

        }
    }
})

複数のレコードがあってもいいようにfindクエリの結果をfor文で回すように変更しました。
あとはres.send部分が変数化されたくらいです。

API経由でJsonパラメータを受け取ってみる

最後にレコードの取り込み用のAPIを追加しましょう。
ServerURL/api/v1/registerへJson形式でPOSTすることにします。
Json形式のデータを受け付けるので、BodyParserを使います。
※送るデータは{value:["よいではないか"], response:"あーれぇー"}のような形にします。valueは配列にするのを忘れずに。

// register_api.js
const db = require('./db/file_db_controller');
const path = require('path');
const bodyParser = require('body-parser');

module.exports = (robot) => {
    robot.router.use(bodyParser.urlencoded({ extended: true }));
    robot.router.use(bodyParser.json());

    robot.router.post('/api/v1/register', async (req, res) => {
        if (!req.body) return;

        const json_data = {
            key: "keywords",
            value: req.body.value,
            response: req.body.response
        }

        await db.insertQuery(json_data, db.GetConnection);
        robot.loadFile(path.resolve(__dirname, "./"), 'index.js');
        res.send(200);
    })
}

DBへ受け付けたレコードを流し込んだら、robot.loadFileコマンドでjsファイルを再読み込み、先程追加したレコードを有効化します。
すべて終わったら、レスポンスコード200でも返しておきましょう。

ついでに最終的なindex.jsはこんな感じ。

// index.js
const db = require('./db/file_db_controller');

module.exports = (robot) => {
    const query = { key: "keywords" }

    db.findAllQuery(query, db.GetConnection)
    .then(params => {
        for(let param of params){
            for(let word of param.value){
                let regexp = new RegExp(word, 'i');
                console.log("set hearing word: " + word);

                robot.hear(regexp, (res) => {
                    res.send(param.response);
                });
            }
        }
    })
}

ということで完成です。
やあ、というキーワードで、ややあんとにお とかでも反応するのが嫌な場合は、wordの部分を、"^" + word にするなど、先頭一致等の条件をいい具合煮付けてください。
あとは煮るなり焼くなり、認証つけるなり入力フォーム作るなりご自由にドウゾ。

Node.jsにnedb用のWrapperクラスとファンクションを作る

目的?

何をするにもデータの取り回しは大切、ということでjavascriptで簡単にDB用の諸々を準備します。
NEDBを利用してファイルDBとメモリDBを構築しやす。
nedb 1.8.0
node 10.16.0

ファイル構成

ライブラリ用ディレクトリ └db ├ files │ └ definition.db ├ nedb_wrapper.js ├ file_db_controller.js └ mem_db_controller.js

Wrapperクラスを作る

まずはコンストラクタでファイルDB/メモリDBそれぞれの設定が出来るようにする。

let DataStore = require('nedb');

module.exports = class {

    constructor(filename) {
        this.memonly = false;
        if (!filename) this.memonly = true;
        else if (typeof filename !== 'string') console.log('filename is not string. :' + filename);

        // DBコネクション生成 
        if (!this.memonly) {
            this.db = new DataStore({
                filename: filename,
                inMemoryOnly: this.memonly,
                timestampData: true,
                autoload: true,
                onload: err => { if(!err) return; console.log('database load error. : ' + err); }
            });
        }
        else {
            this.db = new DataStore({
                inMemoryOnly: this.memonly,
                timestampData: true
            });

        }

        /** 同一のDBを利用するためにコネクションをここから取得する */
        this.GetConnection = this.db;
    }

    //クエリ定義エリア
    //ココから共通的に使うクエリを定義していく。

}

this.memonlyフラグでインスタンス化のときにファイル指定があるかを確認、生成するDBの種類を変更する。
一応ファイル名の型チェックを行い、文字列以外であればエラーログを出す。
memonly=falseの場合はファイルDBとして定義し(ifルート)trueの場合はオンメモリDBの設定を定義する(falseルート)
最後にthis.GetConnectionに定義したDBコネクションを渡して完了。

上記コードの最後、クエリ定義エリアに下記の様にクエリを定義する。
今回はinsertとselectを作ってみます。

    insertQuery(query) {
        return new Promise((docs) => {
            this.db.insert(query, function (err, res) {
                if (res || !err) docs(res);
                else console.log("[insertQuery]error : " + err);

            });
        });
    }

    findAllQuery(query) {
        return new Promise((docs) => {
            this.db.find(query, function (err, res) {
                if (res || !err) docs(res);
                else console.log("[findAllQuery]error : " + err);
            });
        });
    }

どちらもqueryにJSON型のパラメータを指定して利用します。
ココまでをnedb_wrapper.jsとして保存します。

ファイルDB用コントローラ

definition.dbというファイルを読み込み、または生成してファイルDB用インスタンスを生成します。

let DataBase = require('./nedb_wrapper');

const fs = require('fs');
const path = require('path');
const filepath = path.resolve(__dirname, '../') + '/db/files/definition.db';

try{
    fs.statSync(filepath);
    console.log('db file exist.');
}catch(err){
    fs.writeFileSync(filepath, '');
    console.log('db file created.');
}

const db = new DataBase(filepath);
module.exports = db;

メモリDB用コントローラ

細かい設定はWrapper側でやっているので、メモリ用に生成する場合はファイルパスにNullを指定して呼び出します。

let DataBase = require('./nedb_wrapper');

const filepath = null;
const db = new DataBase(filepath);

module.exports = db;

使い方

使い方としては読み込み対象をfile/memのどちらかを選択する以外は共通です。
queryは適当なJsonパラメータ(ex {key:"keyword"}など)

const db = require('./db/file_db_controller');
const query = {
    key: "keyword"
}

db.findAllQuery(query, db.GetConnection).then(paramList => {/*なにか処理*/});