DRYな備忘録

Don't Repeat Yourself.

Firebase Cloud Messaging(FCM)でより簡単にWebブラウザにPush通知を送るサンプル

前回、下記のエントリでProgressive Web Appにおけるそこそこ生のWeb-Push-Protocolについておおまかな仕組みを触れました

otiai10.hatenablog.com

ので、今回はFirebase Cloud Messagingを使って、もっと手軽にWebブラウザにPush通知を送るのをやってみた備忘録です。

公式ドキュメント

必要なもの

  • GCM Sender ID
    • ブラウザ(デバイス)側で、Subscriptionを発行するときに、このアプリケーションの所属を明らかにするために必要
    • WebPushProtocolにおけるApplicationServerPublicKeyをFirebaseがラップしている要素だと思えばよい
  • FCM Server Key
    • 自前サーバ側で、FCMを叩くときに、アプリケーションの所属と妥当性を明らかにするために必要
    • WebPushProtocolにおけるApplicationServerPrivateKeyをFirebaseがラップしている要素だと思えばよい

どっちもFirebase consoleで手に入る。

  1. Firebase consoleに行き、[プロジェクトを追加]する(図)
  2. [ウェブアプリにFirebaseを追加]を選択(3つ並んだ◯の右の赤いやつ)
  3. messagingSenderIdというのがあるので、これがGCM_SENDER_IDです
  4. つづいて、左上、[Overview]の隣の設定アイコンから[プロジェクトの設定]を選択
  5. 上部のタブのうち[クラウドメッセージング]を選択 6.サーバーキーというのがあるので、これがFCM_SERVER_KEYです

f:id:otiai10:20170622010233p:plain

やること

  • ブラウザのタブで動くmain.js
    • firebaseアプリケーションの初期化
    • messagingモジュールの初期化
      • 自動で/firebase-messaging-sw.jsをServiceWorkerとしてregisterしようとする
    • Notificationの許可と、DeviceToken取得できたら自前サーバにそれを送る
  • バックグラウンドで動き続けるfirebase-messaging-sw.js
    • ブラウザタブがactiveでないときにPush通知が来たときのハンドラを設定できる
  • 自前のサーバserver.js
    • ブラウザから送りつけられてきた各ユーザのDeviceTokenの保存
    • 何らかのきっかけでFCMのエンドポイントを叩く
      • payloadのtoというフィールドでPush通知を投げたい相手のDeviceTokenを指定する
    • もちろんjsである必要はない全く無い

うるせえ動くコード見せろ

ウッス

github.com

% git clone git@github.com:otiai10/firebase-messaging-sample.git
% cd firebase-messaging-sample
% GCM_SENDER_ID={上記の} \
FCM_SERVER_KEY={上記の(けっこう長い)} \
node server.js

こんな感じでうごく

f:id:otiai10:20170622022753p:plain

注意点・ハマったポイント

manifest.jsonのgcm_sender_idは"103953800507"

  • 問題: ブラウザのコンソールで「Messaging: Please change your web app manifest’s ‘gcm_sender_id’ value to ‘103953800507’ to use Firebase messaging. (messaging/incorrect-gcm-sender-id).」と叱られる
  • 原因: これは、実際にPush通知を送る主体はFirebaseのサーバなので、FirebaseのアプリケーションIDを登録する必要があるため
  • 解決: ハードコードで103953800507を指定する manifest.json

setBackgroundMessageHandlerで設定したハンドラが呼ばれてない

  • 問題: firebase-messaging-sw.jsにおいて、setBackgroundMessageHandlerを使ってハンドラを登録しているのにもかかわらず、ぜんぜん呼ばれてる気配が無い
  • 原因:
    • その1: ブラウザのタブがactiveな時は必ず、ブラウザタブのmain.jsにおけるmessaging.onMessageが呼ばれるので、ServiceWorkerには入らない
    • その2: これが上記で「癖がある」って言ったやつなんですけど、自前サーバ側でFirebase叩くときに、そのペイロード中にnotificationというフィールドのデータが入っていると、そのnotificationで指定したtitle,body,iconでNotificationが生成され、ServiceWorkerには入らない。ふしぎだね。
  • 解決: まずブラウザのタブを閉じる。そんで、サーバからのFCMへのリクエストのトップにnotificationというフィールドを含めない

雑感

  • 暗号化その他もろもろFirebaseがやってくれるので、やっぱり素でWeb-Push-Protocolに乗るより断然お手軽だった
  • 癖があるところとか、ほげほげKEYが多すぎて混乱するところとかはあるけども
  • 酒が弱くなってる。よいことだ。

DRYな備忘録として

Using Google App Engine

Using Google App Engine

webブラウザにPush通知送るサーバとjsのサンプル

このドキュメントは

以下の2つのドキュメントをよりプリミティブに理解するためのDRYな備忘録です。

  1. Adding Push Notifications to a Web App  |  Web  |  Google Developers
  2. The Web Push Protocol  |  Web  |  Google Developers

f:id:otiai10:20170619113329p:plain

背景

かつて サーバからブラウザにプッシュ通知を送りたい(非WebSocket、非ロングポーリング) - DRYな備忘録 これ書いたけど、改めてProgressive Web Appのドキュメント行ったらアホみたいに分かりづらくなってて不条理を感じたので書きます。

サーバからPush通知がブラウザに対して送れて、ブラウザのjsが閉じててもServiceWorkerが生きてるからNotificationが出る、っていうやつです。最近だとウェブのGmailとかFacebook Messanger若干うざいやつとかでよく見る気がします。

この公式ドキュメント、パッとサンプル落としてきてハイ動くでしょ!っていうタイプなのか最小限の要素を理解させつつ動かすタイプなのか、完全にどっち付かずのドキュメントになっていて、若干わかりにくいです。

今回は、WebPushProtocolにおける暗号化されたbodyを送る、というところを除き、クライアントのコードとサーバのコードを書いたので、そのログを備忘録として晒します。

概念の確認

サービスとしては、3つです

  1. ブラウザ
    • ネイティブアプリ開発における「デバイス」だと理解して問題ないです
    • ブラウザではふたつのことをします
      • ServiceWorkerを登録し、登録したServiceWorkerにプッシュをsubscribeさせ、PushSubscriptionモデルを取得します
        • これが、ApplePushNotificationにおけるDevice Token的なものです
      • ServiceWorker内部では、タブのJavaScriptが死んでもbackgroundで動き続けるイベントリスナーを登録します
        • pushが来たとき、というイベントリスナーもここで定義します
  2. 自前サーバ
    • ブラウザで登録・取得されたPushSubscriptionモデルのデータを、自作アプリケーションにおけるユーザなどと紐付けて保存しておく必要があります
    • 何らかのトリガーで(たとえばユーザによる他ユーザへの呼びかけなど)下記の「Pushサービス」に「このsubscription endpointへこの内容でプッシュ通知送ってちょんまげ」というリクエストをする役割があります
  3. Pushサービス
    • 各デバイスに対してPushを実際に送るサービスです
    • Web-Push-Protocolのインターフェースを満たすサービスである必要があります
      • Chromeではfcm.googleapis.com
      • Firefoxではupdates.push.services.mozilla.com
      • が、それぞれPushサービスとして採用されます
    • 僕らは自前サーバからこのエンドポイント叩くだけなのでその違いを意識する必要は無い ← ここだいじ!

うるせえ動くコード見せろ

ウッス

github.com

こんな感じに動きます

f:id:otiai10:20170619200505p:plain

実際の処理

  1. ブラウザに登録されるServiceWorker
    • ソースコード: service-worker.js
    • やること
      • 自分自身に何かイベントがあったらそれをハンドリングする
        • install, activate, push
    • 必要なもの
      • 無い。ぶっちゃけ登録されるServiceWorkerがやるべき責務は小さい。
  2. ウェブページのmain
    • ソースコード: main.js
    • やること
      • ServiceWorkerをブラウザにregisterする
      • そしたらregistration.pushManager.subscribe()というのを呼ぶ
      • 上記で得られたsubscriptionモデルを自作サーバへ送りつける
        • このsubscriptionというのが、Pushを提供するGCMサーバのエンドポイントと、このデバイス(ブラウザ)のDeviceToken的なものを持っている
    • 必要なもの
      • ApplicationServerPublicKeyが必要
        • サーバのみが知りうるApplicationServerPrivateKeyに対をなすもの
  3. Pushをトリガーする自前サーバ
    • ソースコード: server.js(もちろんjsで書く必要は無い)
    • やること
      • ウェブページのjsから送られてきたsubscriptionの保存
      • 任意のトリガーでそのsubscriptionへPushを送る
        • 厳密にはPushを"送る"のはgcmなりMozilaPushServiceなので、「Pushサービスが提供したエンドポイントを叩く」のほうが正確
        • また、Pushサービスを叩くときのリクエストはWeb-Push-Protocolで定められた方法で暗号化されている必要があるが、今回はこれはweb-pushライブラリにすべて任せている
    • 必要なもの
      • ApplicationServerPrivateKeyが必要

注意点・ハマったところ

ServiceWorkerの更新ができない

  • 問題: ページをリロードしても編集したServiceWorkerが適用されていない
  • 原因: ServiceWorkerのライフサイクルは「ブラウザ自体」と「ウェブページのドメイン」によって定められるのであって、「ウェブページのタブ(いわゆるいつものブラウザのjsランタイム)」ではないので、ページをリロードしても更新されない
  • 解決: getRegistraionからのunregisterを呼ぶ。あるいはchrome://serviceworker-internals/に行き該当ウェブページに登録されているServiceWorkerを[Unregister]ボタンで削除するなりする必要がある

Subscriptionの更新ができない

InvalidStateError: Registration failed - A subscription with a different applicationServerKey (or gcm_sender_id) already exists; to change the applicationServerKey, unsubscribe then resubscribe.
  • 問題: registration.pushManager.subscribe()を呼ぶと上記のようなエラーが出ることがある。
  • 原因: エラーメッセージでは「別のapplicationServerKeyを使ったsubscriptionがすでにあるので、applicationServerKeyを変えてunsubscribeしてからもっかいsubscribeしてね」と言っている。基本的にはApplicationServerPublicKeyApplicationServerPrivateKeyの組み合わせは、1つのプロジェクトについてただ1つだけなので、このようなエラーは起きないと思うが、開発中とかだと起きるかもしれませんねハイ。上記のサンプルでは、npmのpost-installで1度だけVAPIDを作るようにしてます
  • 解決: エラーメッセージにおとなしく従う

ApplicationServerKeyとは

InvalidAccessError: Failed to execute 'subscribe' on 'PushManager': The provided applicationServerKey is not valid.
Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.
  • 問題: subscribeするときに、上記のようなエラーが出ることがある。どうやらapplicationServerKeyがよくないようだが、そもそもApplicationServerKeyって何なのよ。
  • 原因: 今回いちばんハマったところなんですが、ApplicationServerKeyっていうのは、WebPushProtocolの文脈でのただの言い方で、その他いろいろな文脈とは関係ない、ただの、定められた方法で生成されたPublicKeyとPrivateKeyのペアというだけです。ここにGCM_API_KEYとかFCM_SERVER_KEYとか突っ込んで困る、というケースが多そう。まあ僕もなんですけど。RSAとかも無関係、名前似てるけど。
  • 解決: ほんで、この「Keyのペア」は、今回はweb-pushライブラリのgenerateVAPIDKeysに全部まかせています。PushサーバとしてGCMやMozilaPushServiceを叩くことこそあれど、今回のサンプルはGCM, FCM, MPSを関知しない、Web-Push-Protocolを用いたアプリケーションの構成のサンプルという位置づけです。

雑感

DRYな備忘録として

Building Progressive Web Apps: Bringing the Power of Native to the Browser

Building Progressive Web Apps: Bringing the Power of Native to the Browser

はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-

はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-

Goのコードから複数の異なるDockerホストに対してコンテナの起動を実装する

2017/06/19 追記

  • エラーハンドリングにバグがあったので修正しました
    • channelに何か流したとき、channelから取り出されないと流し込んだ側をブロックするのを忘れていました
- errored <- err
+ go func() {
+   errored <- err
+ }()

前回までで以下のことをやったので

今回は、Goのソースコードが動いているマシンじゃないマシンに対してDockerコンテナの起動を指示する実装をしてみたいなと思いました。

準備

  • 複数のDockerホストを再現するためにdocker-machineを入手しとく
  • ドライバとしてVirtualBoxをインストールしとく
  • 前回までで使っていたclient.NewEnvClientclient.NewClient実装の違いを見とく
  • docker-machine create --driver virtualbox hoge
    • docker-machine env hoge で得られるDOCKER_HOSTとかDOCKER_CERT_PATHとかあとで使う
  • docker-machine create --driver virtualbox fuga
    • docker-machine env fuga で得られるDOCKER_HOSTとかDOCKER_CERT_PATHとかあとで使う

動くコード

package main

import (
    "bufio"
    "context"
    "fmt"
    "net/http"
    "path/filepath"

    "github.com/docker/docker/api/types"
    "github.com/docker/docker/api/types/container"
    "github.com/docker/docker/api/types/mount"
    "github.com/docker/docker/api/types/network"
    "github.com/docker/docker/client"
    "github.com/docker/go-connections/tlsconfig"
)

// WorkerMachine is a host on which any container runs,
// This shoule also have file mout source path.
type WorkerMachine struct {
    Host        string
    CertPath    string
    MountSource string
}

// NewClient provides Docker client for **THIS** host machine.
func (machine WorkerMachine) NewClient() (*client.Client, error) {
    options := tlsconfig.Options{
        CAFile:             filepath.Join(machine.CertPath, "ca.pem"),
        CertFile:           filepath.Join(machine.CertPath, "cert.pem"),
        KeyFile:            filepath.Join(machine.CertPath, "key.pem"),
        InsecureSkipVerify: false,
    }
    tlsc, err := tlsconfig.Client(options)
    if err != nil {
        return nil, err
    }
    httpClient := &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: tlsc,
        },
    }
    headers := map[string]string{}
    return client.NewClient(machine.Host, "1.30", httpClient, headers)
}

// This is the definition of docker host machines
// on which any tasks would be executed.
var machines = []WorkerMachine{
    WorkerMachine{
        Host:        "tcp://192.168.99.100:2376",
        CertPath:    "/Users/otiai10/.docker/machine/machines/hoge",
        MountSource: "/Users/otiai10/tmp/hoge",
    },
    WorkerMachine{
        Host:        "tcp://192.168.99.101:2376",
        CertPath:    "/Users/otiai10/.docker/machine/machines/fuga",
        MountSource: "/Users/otiai10/tmp/fuga",
    },
}

// task function defines what to do on that machine.
// Because hijacking stdout of container returns io.Reader
// and also because the first out put would be lost when container starts BEFORE hijacking,
// this function should return channel which can message error,
func task(index int, machine WorkerMachine) (errored chan error) {

    errored = make(chan error)

    c, err := machine.NewClient()
    if err != nil {
        go func() {
            errored <- err
        }()
        return
    }
    defer c.Close()
    ctx := context.Background()

    // Ensure Target Image to exist inside this machine
    r, err := c.ImagePull(ctx, "otiai10/foo", types.ImagePullOptions{})
    if err != nil {
        go func() {
            errored <- err
        }()
        return
    }
    defer r.Close()
    fmt.Printf("[%d][IMAGE PULL][START]", index)
    for scanner := bufio.NewScanner(r); scanner.Scan(); {
        fmt.Printf(".")
    }
    fmt.Printf("\n[%d][IMAGE PULL][FINISH]\n", index)

    // Create container in this machine
    containerconfig := &container.Config{Image: "otiai10/foo"}
    hostconfig := &container.HostConfig{
        Mounts: []mount.Mount{
            mount.Mount{
                Type:   mount.TypeBind,
                Source: machine.MountSource,
                Target: "/var/data",
            },
        },
        AutoRemove: true,
    }
    networkingconfig := &network.NetworkingConfig{}
    body, err := c.ContainerCreate(
        ctx,
        containerconfig, hostconfig, networkingconfig,
        fmt.Sprintf("work-%d", index),
    )
    if err != nil {
        go func() {
            errored <- err
        }()
        return
    }

    // Hijack container's Stdout, and it should be hijacked BEFORE container starts
    hijacked, err := c.ContainerAttach(ctx, body.ID, types.ContainerAttachOptions{
        Stream: true,
        Stdout: true,
    })
    if err != nil {
        go func() {
            errored <- err
        }()
        return
    }
    go func() {
        for scanner := bufio.NewScanner(hijacked.Reader); scanner.Scan(); {
            fmt.Printf("[%d][CONTAINER STDOUT]\t%s\n", index, scanner.Text())
        }
        hijacked.Close()
        fmt.Printf("[%[1]d][COMPLETED] Congrats! `work-%[1]d` is completed without errors!\n", index)
        errored <- nil
    }()

    // OK, now it's time to start the container!
    if err := c.ContainerStart(ctx, body.ID, types.ContainerStartOptions{}); err != nil {
        go func() {
            errored <- err
        }()
        return
    }

    return
}

func main() {
    for i, machine := range machines {
        errored := task(i, machine)
        if err := <-errored; err != nil {
            panic(err)
        }
        close(errored)
    }
}

これを実行すると、

f:id:otiai10:20170607171124p:plain

となり、複数の異なるホストマシンで、異なるContainerのプロセスが、同期的に逐次実行されていることがわかる。

なお、二回目からは、各ホストマシンにimageがすでにあるので

f:id:otiai10:20170607171538p:plain

こういう感じになるし、あえて

% eval $(docker-machine env fuga)
% docker images -a
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
otiai10/foo         latest              92af8256ff46        39 minutes ago      193 MB
% docker rmi otiai10/foo
%

このようにfugaの方だけでotiai10/fooimageを消しとくと、

f:id:otiai10:20170607172406p:plain

fugaでのみdocker pullでまるっとimageを落としてくる必要があり、hogefugaがDockerホストマシンとして全く別物であることが確認できる。

雑感

  • Dockerちょっとずつわかってきた
  • 去年ずっとJSとSwiftだったので、ひさしぶりにGo触ってて超たのしい
  • GoたのしすぎてJSのタスクがぜんぜん進まない…

DRYな備忘録として

Docker

Docker

Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス

Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス

マイクロサービスアーキテクチャ

マイクロサービスアーキテクチャ

【GAE/Go】goappで "There are too many files in your application" と怒られる

追記 2018/08/01

gcloud components update 来てたので、した。

追記

gcloud components updateするとgoogle-cloud-sdk以下のファイルが更新されて魔改造が吹っ飛ぶ。かなしみ。

問題

% goapp serve
INFO     2017-06-07 01:58:24,616 devappserver2.py:692] Skipping SDK update check.
INFO     2017-06-07 01:58:24,646 api_server.py:272] Starting API server at: http://localhost:52623
INFO     2017-06-07 01:58:24,648 dispatcher.py:205] Starting module "default" running at: http://localhost:8080
INFO     2017-06-07 01:58:24,650 admin_server.py:116] Starting admin server at: http://localhost:8000
/Users/otiai10/.google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/mtime_file_watcher.py:157: UserWarning: There are too many files in your application for changes in all of them to be monitored. You may have to restart the development server to see some changes to your files.
  'There are too many files in your application for '

となって、編集検知のオートビルドされなくてちょっと困る。

調査

なんかdevappserverのコード魔改造するしかなさそう。今んとこ。

解決

appengine/tools/devappserver2/mtime_file_watcher.pyを以下のように編集。まずはわかりやすいように。

diff --git a/watcher_common.py b/watcher_common.py
index fe47078..6541de6 100755
--- a/watcher_common.py
+++ b/watcher_common.py
@@ -47,6 +47,11 @@ def ignore_file(filename, skip_files_re=None):
   Returns:
     Boolean value, True if the filename can be ignored.
   """
+  if not filename.startswith(os.getcwd()):
+    return True
+  if filename.find('/node_modules/') is not -1:
+    return True
+
   if skip_files_re and skip_files_re.match(filename):
     return True
   filename = os.path.basename(filename)
  1. カレントディレクトリ以下のファイル以外は監視対象から除外
  2. node_modulesは監視対象から除外

適宜

適宜、たとえば

diff --git a/watcher_common.py b/watcher_common.py
index fe47078..ad1f641 100755
--- a/watcher_common.py
+++ b/watcher_common.py
@@ -47,6 +47,11 @@ def ignore_file(filename, skip_files_re=None):
   Returns:
     Boolean value, True if the filename can be ignored.
   """
+
+  # Watch ONLY current directory if env "GOAPP_WATCH_CWD" is found
+  if os.getenv('GOAPP_WATCH_CWD') and not filename.startswith(os.getcwd()):
+    return True
+
   if skip_files_re and skip_files_re.match(filename):
     return True
   filename = os.path.basename(filename)
@@ -67,7 +72,11 @@ def _remove_pred(lst, pred):

 def ignore_dir(dirpath, dirname, skip_files_re):
   """Report whether a directory should not be watched."""
+
+  # List of directory names to be ignored; e.g. "node_modules", "vendor" or so
+  _IGNORED_DIRS = ('node_modules')
   return (dirname.startswith(_IGNORED_PREFIX) or
+         (dirname in _IGNORED_DIRS) or
           skip_files_re and skip_files_re.match(os.path.join(dirpath, dirname)))

のようにしたらオシャレなのではないか。魔改造には変わりないけど。

DRYな備忘録として

はじめてのGoogle App Engine Go言語編 (I・O BOOKS)

はじめてのGoogle App Engine Go言語編 (I・O BOOKS)

Go言語プログラミング入門on Google App Engine

Go言語プログラミング入門on Google App Engine

プログラミング Google App Engine

プログラミング Google App Engine

CSS書きたくなさすぎ問題2017

css書きたくない。できればjsも書きたくない。js必要なの嫌。軽くやりたい。という個人の日記です。

参考

Gridだけやりたいやつは除外した。

  • Bootstrap
  • Materialize
  • mui
  • (追記)UIkit
  • (追記)Semantic UI
  • Pure
  • Bulma
  • Skelton
  • Spectre.css
  • Kube
  • Vuetify
  • Fictoan
  • avalanche
  • Beuter
  • Vanilla
  • Milligram
  • InvisCSS
  • Look
  • mini
  • Cutestrap
  • Buefy
  • siimple
  • Mobi
  • Modulr
  • Grd
  • Frow
  • Picnic CSS
  • Basscss
  • Blaze CSS
  • Furtive
  • LOTUS
  • Leaf CSS

後半疲れたっていうのもあり、僕的には、Skelton、Spectre、Fictoan、Milligram、Look、あたりが今後使ってみたいです。

Bootstrap

Bootstrap · The world's most popular mobile-first and responsive front-end framework.

大御所だし、最初に言っといてやるか感

Materialize

Documentation - Materialize

f:id:otiai10:20170530145913p:plain

material-uiのcssだけ版。個人事業で使ってるけど、部分的にjs要るし、ちょっとだるみ出てきた。これ使うならMaterial-UIでええやろ感ある。なお、Material-UIは個人趣味開発で使ってる。

mui

MUI - Material Design CSS Framework

f:id:otiai10:20170530150136p:plain

Materializeと同じ雰囲気を感じる。jsあるやん。

(追記)UIkit

Thanks to id:k-holy, id:kvx

UIkit

jQuery is required as well.

アッ、ハイ。

(追記)Semantic UI

Thanks to id:oukayuka, id:wazly

Semantic UI

Just link to these files in your HTML along with the latest jQuery.

アッ、ハイ。

Reactでやりたいんだったら Semantic UI React があるけど、僕が常日頃だるいなって思ってるのは、デザインとフロントのアーキテクチャがベタベタでいいのか?っていうアレ。変じゃね?なんでデザイン策定でReact使うかjQuery使うかみたいな二択せなアカンねんと。両方つかうとか論外。

Pure

Pure

f:id:otiai10:20170530150038p:plain

cssオンリーつったらいの一番に出てくるやつ

Bulma

Bulma: a modern CSS framework based on Flexbox

f:id:otiai10:20170530150843p:plain

わりといい感じ。demadoに使ってる。

Skelton

Skeleton: Responsive CSS Boilerplate

f:id:otiai10:20170530145504p:plain

こういうのでいいんだよ、こういうので。

Spectre.css

f:id:otiai10:20170530151001p:plain

よさそう。

Kube

f:id:otiai10:20170530151207p:plain

パンくずとかもあっておもしろそうだけど、JSけっこう書かされる気がする。

Vuetify

vuetifyjs.com

Vueコンポーネントベース。却下。

Fictoan

FICTOAN • Intuitive, minimalist responsive HTML+CSS boilerplate

f:id:otiai10:20170530151936p:plain

Overview読んで、あ〜これ好き、ってなった。今度使いたい

avalanche

avalanche | A package based CSS framework.

プロジェクト構成まで口出してこないでほしい

Beuter

Beauter | A simple framework for beautiful sites

f:id:otiai10:20170530152449p:plain

完全に好みで申し訳ないんだけど、トップページのUIがあんまり好きくなかったので印象悪い

Vanilla

Documentation

f:id:otiai10:20170530152631p:plain

menu itemがらみのクリック可領域がちょっと直感的ではない。そのへんもカスタマイズしろ、ってことなのかな

Milligram

Milligram - A minimalist CSS framework.

f:id:otiai10:20170530153019p:plain

一瞬Vaporかな?って思ったけどちがう。圧巻の2kb。良いかもしれない。

InvisCSS

Invis CSS

なんかそういうことじゃねえんだよなー、っていう。僕のペインとスタートラインが違う。

Look

Look | Minimalistic CSS framework

f:id:otiai10:20170530153833p:plain

FieldsetsとかSegmentsとかおもしろそう。最後のCubeワロタ。

mini

mini.css - Minimal, responsive, style-agnostic CSS framework

f:id:otiai10:20170530154652p:plain

全体的にカクカクしてた

Cutestrap

Cutestrap

f:id:otiai10:20170530154909p:plain

さすがに機能薄すぎるんじゃねえかなと思った。

Buefy

Buefy: lightweight UI components for Vue.js based on Bulma

Vueのコンポーネントかー、残念。

siimple

siimple - Minimal CSS framework for flat and clean websites

f:id:otiai10:20170530155216p:plain

名前がややこしい。Almost Flat UIに似た印象を受けた。

Mobi

Mobi.css

Mobile Firstらしいけど、たぶん使わない。見た目が無骨すぎて。

Modulr

Modulr.css Frontend Framework

f:id:otiai10:20170530155717p:plain

色に対する並々ならぬ関心が感じられるが、これ↑残念すぎるだろ。

Grd

Grd - A CSS grid framework using Flexbox

もう頭使いたくないんです。ゆるして。

Frow

Frow CSS

f:id:otiai10:20170530160100p:plain

ほぼほぼGridとFormだけって感じ。でもFormはスタイリッシュでいい感じ。

Picnic CSS

Picnic CSS

f:id:otiai10:20170530160404p:plain

なかなかよい。「ここはすまんやけどjs書いてくれ」っていう態度も好印象です。書かねえけどな。

Basscss

Basscss

f:id:otiai10:20170530160556p:plain

Form置いてけぼりでいいんならアリなんじゃないですかね。

Blaze CSS

Blaze CSS - Open Source Modular CSS Toolkit

f:id:otiai10:20170530161108p:plain

なんかどっかで見たことあるんだよなあこの雰囲気。

Furtive

Furtive CSS

f:id:otiai10:20170530161232p:plain

Skeltonをちょっとカラフルにした印象がある。よさそう。

LOTUS

f:id:otiai10:20170530161407p:plain

フォームはシュッとしてるけどぜんたいてきに柔らかい印象。

Leaf CSS

Leaf BETA 1.0 - CSS Framework

f:id:otiai10:20170530161656p:plain

Material Designの軽量実装っぽい。いい感じなのだけれど、あまりメンテされなさそう。

雑感

  • 調べればキリがないわコレ
  • なんか適当な軸でマッピングにしてビジュアライズしてくれ誰か頼む
  • プログラマCSS考えさせるデザイナと仕事すんのつらい。デザイナからCSSフレームワーク納品されたら泣いて喜ぶ。せめてガイドラインぐらい作ってくれ。「え?出来ないんですか?ww」じゃねえよ、おめえのsketchファイルmarginあちこちバラバラだから!

GoのコードからDockerコンテナへのディスクボリュームのマウントを実装する

前々回、前回

で、GoのコードからDockerイメージのpullとDockerコンテナのrunを実装できたので、実践的なアプリケーションをつくっていきたいのだけれど、今回はdocker runにおける--volumeの指定がしたい。

実行するDockerイメージのサンプル

Dockerfile

FROM centos

ADD main.sh /bin/

ENTRYPOINT main.sh

main.sh

#!/bin/sh

dest="copy_`date`.txt"
cat /var/data/foo.txt > /var/data/${dest}

docker build . -t foo としてfooというtagのimageをつくる。

実行するGoのコード

   cc := &container.Config{
        Image: "foo",
        // Volumes: map[string]struct{}{"/var/data": struct{}{}},
        // これ要らんっぽいな... 🤔
    }
    hc := &container.HostConfig{
        // これが味噌
        Mounts: []mount.Mount{
            mount.Mount{
                Type:   mount.TypeBind,
                Source: "/Users/otiai10/tmp/hoge",
                Target: "/var/data",
            },
        },
        AutoRemove: true,
    }
    nc := &network.NetworkingConfig{}

    // あとは前回、前々回とおなじです。
    body, err := c.ContainerCreate(ctx, cc, hc, nc, "bar")
    if err != nil {
        panic(fmt.Errorf("ContainerCreate: %v", err))
    }
    fmt.Printf("Created Container:\n%+v\n", body)

    if err := c.ContainerStart(ctx, body.ID, types.ContainerStartOptions{}); err != nil {
        panic(fmt.Errorf("ContainerStart: %v", err))
    }

確認

% ls -l ~/tmp/hoge
total 8
-rw-r--r--  1 otiai10  staff  16  5 29 16:48 foo.txt
% go run main.go
Created Container:
{ID:e2b6e7aa7251f13d8e823dc4a4dddb327b3863e9632211698126b46d5975e724 Warnings:[]}
List of containers
 - e2b6e7aa7251f13d8e823dc4a4dddb327b3863e9632211698126b46d5975e724 (foo)
% ls -l ~/tmp/hoge
total 16
-rw-r--r--  1 otiai10  staff  16  5 30 11:59 copy_Tue May 30 02:59:06 UTC 2017.txt
-rw-r--r--  1 otiai10  staff  16  5 29 16:48 foo.txt
%

無事、ホスト(厳密にはGoのコードを実行する環境ではなく、このclient.Clientが向いてるdockerホストマシン)の/Uers/otiai10/tmp/hogeが、コンテナから見たときの/var/dataにマウントされていることが確認された。

参考と道のり

f:id:otiai10:20170530120709p:plain

雑感

  • ドキュメント散在してるなー、って思った
  • 急がず焦らず、じっくり読まないといけない

DRYな備忘録として

Docker

Docker

Dockerエキスパート養成読本[活用の基礎と実践ノウハウ満載!] (Software Design plus)

Dockerエキスパート養成読本[活用の基礎と実践ノウハウ満載!] (Software Design plus)

GoのコードからDockerイメージのpullを実装する(bufio.Scannerかわいい)

前回エントリ↓でコードからのイメージのpullが動かなくてあっれおかしーなーとなって悔しかったのでリベンジです。

otiai10.hatenablog.com

tl;dr

  • client.ImagePullの返り値はio.ReadCloser型とerror
  • このio.ReadCloserが、イメージのpullのprogressなどを表すHTTPのストリーム
  • このストリームへの書き込みが終わる(つまりdocker pullが完了する)まで待つ必要があった

以下読まなくてよいです

動かないコード

func main() {

    c, err := client.NewEnvClient()
    if err != nil {
        panic(fmt.Errorf("NewEnvClient: %v", err))
    }
    ctx := context.Background()

    // "debian"(:latestは省略可)のpullを試みる
    rp, err := c.ImagePull(ctx, "debian", types.ImagePullOptions{})
    if err != nil {
        panic(fmt.Errorf("ImageSave: %v", err))
    }
    defer rp.Close()
    // エラーさえ無ければイメージのpullが成功していると誤解していた

    cc := &container.Config{Image: "debian"}
    hc := &container.HostConfig{AutoRemove: true}
    nc := &network.NetworkingConfig{}
    body, err := c.ContainerCreate(ctx, cc, hc, nc, "bar")
    if err != nil {
        panic(fmt.Errorf("ContainerCreate: %v", err))
        // 結局ここで"debianなんてイメージは無い!"と叱られる
    }
    fmt.Printf("Created Container:\n%+v\n", body)

}

動くコード

前後省略します

func main() {

    /* 省略(クライアントの初期化とかする) */

    // "debian"(:latestは省略可)のpullを試みる
    rp, err := c.ImagePull(ctx, "debian", types.ImagePullOptions{})
    if err != nil {
        panic(fmt.Errorf("ImageSave: %v", err))
    }
    defer rp.Close()

    // {{{ こっからが味噌!!

    // こういう構造のバッファが書き込まれる
    payload := struct {
        ID             string `json:"id"`
        Status         string `json:"status"`
        Progress       string `json:"progress"`
        ProgressDetail struct {
            Current uint16 `json:"current"`
            Total   uint16 `json:"total"`
        } `json:"progressDetail"`
    }{}

    // bufio.Scannerマジでかわいい
    scanner := bufio.NewScanner(rp)
    for scanner.Scan() {
        json.Unmarshal(scanner.Bytes(), &payload)
        fmt.Printf("\t%+v\n", payload)
    }

    // }}} ここまでが味噌!!

    /* 省略(得られたイメージを指定してコンテナつくったりする) */

}

上記では、bufio.Scannerを使ってHTTPのEOFまで待っている。ついでに、毎バッファ書き込みされるレスポンスを取り出して上げている。
↓こういうきれいな感じのペイロードが送られてくる。

f:id:otiai10:20170526152637p:plain

Scanner使わないんだったらioutil.ReadAllかなー他にストリームが終わるの同期的に待つのどうすっかなーと思ってググりました。

今回のように毎ペイロードが独立して意味のあるものであれば、Scanner使うのが良い気がするものの、もっと良い(単にReaderのEOFを待つための)方法があれば教えてくださいm(_ _)m

雑感

  • Goのコードからgithub.com/docker/docker/clientを使って (1) イメージのpull (2) コンテナ起動 が実現されたので、これでほぼ心置きなく俺々Dockerクライアントを自作できますね!やったね!
  • 腰椎椎間板ヘルニアになっちゃった。腰痛ぇ。

DRYな備忘録として

Docker

Docker

Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス

Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス