nがひとつ多い。

えぬなおの技術的なことを書いていくとこ。

multipassでOSX上にmicro-k8sを立てる。

はじめに

この記事はCyberAgent Advent Calendarの20日目の記事になります。

adventar.org

ローカルにkuberentesを立てる時、皆さんどうしてますか?
うん、多くの人は minikube をたてて試していることでしょう。

github.com

しかし minikube は色々なプラグインや特定のCRDが動かないなどお試しするにも難しい部分も多いです。
さて、実はubuntuコミュニティのなかで micro-k8s というものが開発されているらしいです。

MicroK8s - Fast, Light, Upstream Developer Kubernetes

しかもみてください、

[FEATURES]
Istio
GPGPU bindings
Daily builds
Local storage
Local registry
Updates
Dashboard
Metrics
Upgrades
Ingress
DNS
Conformant

Istio とか GPGPU binding も動くらしいです。
すごい、試してみたいです。

よしMacにインストール・・・ってあれ!?

公式のGetting Starttedが・・・あ・・・あ・・・。

$ sudo snap install microk8s --classic

Ubuntu専用のインストーラ使ってんじゃねーか、ってことで、VMに入れるかって話になってくる。

multipassを使ってぶち込む。

えーまた Vagrant とか使ってVM使うのん?めんどくないん? ってことで、以下のサイトでおすすめされていた multipass とかいうUbuntuVM管理ツールが便利だったので使う。

discourse.ubuntu.com

github.com

リリースページにある pkg ファイルから入れるのが早い。

https://github.com/CanonicalLtd/multipass/releases/download/2018.11.1-pre3/multipass-2018.10.1-full-141-g2b7f2bd-Darwin.pkg

なんでmultipassだといいのん?

現状で「手っ取り早くOSX上でUbuntuVMが欲しい」と言う用途に対して一番適したツールだからです。

$ multipass launch --name nnao45-k8s-vm --mem 4G --disk 40G --cpus 2
Launched: nnao45-k8s-vm

VMデプロイがこれで終わり。やばない?
(注意点だが、 デフォルトのメモリ1Gでやると、Addonがろくに動かないのである程度増やして指定しておくといい。4Gもあれば色々遊ぶには十分でしょう。)

$ multipass exec nnao45-k8s-vm  -- sudo snap install microk8s --classic
2018-12-18T11:36:00Z INFO Waiting for restart...
microk8s v1.13.0 from 'canonical' installed

はい、kubernetesインスコ終了っす。 一応NWも通しておく。

$ multipass exec nnao45-k8s-vm -- sudo iptables -P FORWARD ACCEPT

このように kubectl execっぽく multipass exec hoge-vm -- 形式でホストからゲストVMにシームレスにOSコマンドを入れることができるのがおすすめポイントです。

minikubeとの差別点としては、純粋なUbuntu VMとして管理がしやすいでの、ちょっとVMに小細工する事も出来る所が魅力です。

各種セットアップ

multipassの中に入る

$ multipass shell nnao45-k8s-vm 

VMに入れる。

/snap/bin/microk8s.status

micro-k8sのステータスっつーかアドオンが見える。
プラグインで管理されてるのねん。

$ multipass exec nnao45-k8s-vm -- /snap/bin/microk8s.status
microk8s is running
addons:
gpu: disabled
storage: disabled
registry: disabled
ingress: disabled
dns: disabled
metrics-server: disabled
istio: disabled
dashboard: disabled

kubectl

とりあえず入れておく。
これも脳死したいのでsnapで入れる。

$ multipass exec nnao45-k8s-vm -- sudo snap install kubectl --classic

kubectl 1.13.0 from Canonical✓ installed

やばい簡単すぎる逆に大丈夫か?

そしてmicrok8sのkubeconfigを所定の位置においておく。

$ multipass exec nnao45-k8s-vm -- sh -c '/snap/bin/microk8s.config > /home/multipass/.kube/kubeconfig'

ドキドキ

$ multipass exec nnao45-k8s-vm -- /snap/bin/kubectl get node
NAME            STATUS   ROLES    AGE   VERSION
nnao45-k8s-vm   Ready    <none>   25m   v1.13.0

わっしょーい🤗

んーでも何にもないね😷 corednskube-dns もない。

$ multipass exec nnao45-k8s-vm -- /snap/bin/kubectl get all --all-namespaces 
NAMESPACE   NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
default     service/kubernetes   ClusterIP   10.152.183.1   <none>        443/TCP   28m

(実は /snap/bin/microk8s.kubectl コマンドがあり、これだけでもデフォルトの kubeconfig だけでいいなら十分。が、何かと kubeconfig は自分好みに加筆することもあるだろうし、プラグインとかも入れる人は入れておけば良い)

アドオンの有効化

とりあえず

dns
dashboard
metrics-server

これくらいは有効化しておきたいのでしておきます。

$ multipass exec nnao45-k8s-vm -- /snap/bin/microk8s.enable dns dashboard metrics-server 
Enabling DNS
Applying manifest
service/kube-dns created
serviceaccount/kube-dns created
configmap/kube-dns created
deployment.extensions/kube-dns created
Restarting kubelet
DNS is enabled
Enabling dashboard
secret/kubernetes-dashboard-certs created
serviceaccount/kubernetes-dashboard created
deployment.apps/kubernetes-dashboard created
service/kubernetes-dashboard created
service/monitoring-grafana created
service/monitoring-influxdb created
service/heapster created
deployment.extensions/monitoring-influxdb-grafana-v4 created
serviceaccount/heapster created
configmap/heapster-config created
configmap/eventer-config created
deployment.extensions/heapster-v1.5.2 created
dashboard enabled
Enabling metrics-server
clusterrolebinding.rbac.authorization.k8s.io/metrics-server:system:auth-delegator created
rolebinding.rbac.authorization.k8s.io/metrics-server-auth-reader created
apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io created
serviceaccount/metrics-server created
configmap/metrics-server-config created
deployment.extensions/metrics-server-v0.2.1 created
service/metrics-server created
clusterrole.rbac.authorization.k8s.io/system:metrics-server created
clusterrolebinding.rbac.authorization.k8s.io/system:metrics-server created
metrics-server enabled

ドキドキ

$ multipass exec nnao45-k8s-vm -- /snap/bin/kubectl get all --all-namespaces 
NAMESPACE     NAME                                                  READY   STATUS    RESTARTS   AGE
kube-system   pod/heapster-v1.5.2-64874f6bc6-p4l8v                  4/4     Running   0          45s
kube-system   pod/kube-dns-6ccd496668-rmsf8                         3/3     Running   0          2m2s
kube-system   pod/kubernetes-dashboard-654cfb4879-v457l             1/1     Running   0          116s
kube-system   pod/metrics-server-v0.2.1-6f76659f47-nhx4g            2/2     Running   0          31s
kube-system   pod/monitoring-influxdb-grafana-v4-6679c46745-d5gll   2/2     Running   0          116s

NAMESPACE     NAME                           TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)             AGE
default       service/kubernetes             ClusterIP   10.152.183.1     <none>        443/TCP             6m12s
kube-system   service/heapster               ClusterIP   10.152.183.93    <none>        80/TCP              116s
kube-system   service/kube-dns               ClusterIP   10.152.183.10    <none>        53/UDP,53/TCP       2m2s
kube-system   service/kubernetes-dashboard   ClusterIP   10.152.183.208   <none>        443/TCP             116s
kube-system   service/metrics-server         ClusterIP   10.152.183.61    <none>        443/TCP             114s
kube-system   service/monitoring-grafana     ClusterIP   10.152.183.143   <none>        80/TCP              116s
kube-system   service/monitoring-influxdb    ClusterIP   10.152.183.151   <none>        8083/TCP,8086/TCP   116s

NAMESPACE     NAME                                             READY   UP-TO-DATE   AVAILABLE   AGE
kube-system   deployment.apps/heapster-v1.5.2                  1/1     1            1           116s
kube-system   deployment.apps/kube-dns                         1/1     1            1           2m2s
kube-system   deployment.apps/kubernetes-dashboard             1/1     1            1           116s
kube-system   deployment.apps/metrics-server-v0.2.1            1/1     1            1           114s
kube-system   deployment.apps/monitoring-influxdb-grafana-v4   1/1     1            1           116s

NAMESPACE     NAME                                                        DESIRED   CURRENT   READY   AGE
kube-system   replicaset.apps/heapster-v1.5.2-56c546dbb8                  0         0         0       61s
kube-system   replicaset.apps/heapster-v1.5.2-64874f6bc6                  1         1         1       46s
kube-system   replicaset.apps/heapster-v1.5.2-6bc7c4965d                  0         0         0       116s
kube-system   replicaset.apps/kube-dns-6ccd496668                         1         1         1       2m2s
kube-system   replicaset.apps/kubernetes-dashboard-654cfb4879             1         1         1       116s
kube-system   replicaset.apps/metrics-server-v0.2.1-6f76659f47            1         1         1       32s
kube-system   replicaset.apps/metrics-server-v0.2.1-7d7d77666c            0         0         0       114s
kube-system   replicaset.apps/monitoring-influxdb-grafana-v4-6679c46745   1         1         1       116s

わっしょーい🤗

kube proxy

さてせっかくだからgrafanaにアクセスしてみたいわけです。

$ multipass exec nnao45-k8s-vm -- /snap/bin/kubectl cluster-info
Kubernetes master is running at http://localhost:8080
Heapster is running at http://localhost:8080/api/v1/namespaces/kube-system/services/heapster/proxy
KubeDNS is running at http://localhost:8080/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server is running at http://localhost:8080/api/v1/namespaces/kube-system/services/https:metrics-server:/proxy
Grafana is running at http://localhost:8080/api/v1/namespaces/kube-system/services/monitoring-grafana/proxy
InfluxDB is running at http://localhost:8080/api/v1/namespaces/kube-system/services/monitoring-influxdb:http/proxy

localhostって言われてもね・・・。そういえばVMのIPは・・・?

$ multipass list 
Name                    State             IPv4             Release
nnao45-k8s-vm           RUNNING           192.168.64.4     Ubuntu 18.04 LTS

192.168.64.4 らしい。じゃあ、

http://192.168.64.4:8080/api/v1/namespaces/kube-system/services/monitoring-grafana/proxy

f:id:nnao45:20181218213101p:plain

衝撃的なくらい簡単なんだが・・・。

(ちなみにいえば servicetype:NodePort で作れば multipassのVMのIPでリッスンするので それでもアクセスできます。)

metrics-server

kubectk topコマンドはもちろん使えます。

$ multipass exec nnao45-k8s-vm -- /snap/bin/kubectl top pod --all-namespaces
NAMESPACE     NAME                                              CPU(cores)   MEMORY(bytes)
kube-system   heapster-v1.5.2-64874f6bc6-p4l8v                  2m           32Mi
kube-system   kube-dns-6ccd496668-rmsf8                         5m           22Mi
kube-system   kubernetes-dashboard-654cfb4879-v457l             0m           12Mi
kube-system   metrics-server-v0.2.1-6f76659f47-nhx4g            0m           15Mi
kube-system   monitoring-influxdb-grafana-v4-6679c46745-d5gll   3m           23Mi

$ multipass exec nnao45-k8s-vm -- /snap/bin/kubectl top node
NAME            CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
nnao45-k8s-vm   378m         37%    1395Mi          36%

外からkubectlを叩く

まぁホストVMからkubectlを叩きたいわけです。 でもmicro-k8sの場合は何にも難しくなくてkubectl cluster-infoで見た通り、
わかりやすく kuberenetes サービスの endpoint<ゲストVMのIP>:8080を向いています。 ので、何にも手を加えずにアクセスできます。

$ multipass exec nnao45-k8s-vm -- /home/multipass/.kube/kubeconfig > ./kubeconfig

終わりです・・・圧倒的に簡単・・・!

$ cat ./kubeconfig
apiVersion: v1
clusters:
- cluster:
    server: http://192.168.64.4:8080
  name: microk8s-cluster
contexts:
- context:
    cluster: microk8s-cluster
    user: admin
  name: microk8s
current-context: microk8s
kind: Config
preferences: {}
users:
- name: admin
  user:
    username: admin
$ kubectl --kubeconfig ./kubeconfig get pods --all-namespaces                                                                                                                                                                  18-12-18 22:00:36
NAMESPACE     NAME                                              READY   STATUS    RESTARTS   AGE
kube-system   heapster-v1.5.2-64874f6bc6-p4l8v                  4/4     Running   1          38m
kube-system   kube-dns-6ccd496668-rmsf8                         3/3     Running   0          39m
kube-system   kubernetes-dashboard-654cfb4879-v457l             1/1     Running   0          39m
kube-system   metrics-server-v0.2.1-6f76659f47-nhx4g            2/2     Running   0          38m
kube-system   monitoring-influxdb-grafana-v4-6679c46745-d5gll   2/2     Running   0          39m

最後に初期化をお手持ちのシェルで自動化

bashrcやzshrcに書いておくとちょうどいい関数をご用意しました。
適当にカスタマイズしてご使用ください。

microk8s-init(){
  if ! which multipass >/dev/null 2>&1; then
    echo "Please intall multipass"
    return 1
  fi

  # Set the VM Name.
  local MICROK8S_VM_NAME="nnao45-k8s-vm"

  # Setup the VM.
  multipass launch --name ${MICROK8S_VM_NAME} --mem 4G --disk 40G --cpus 2

  # Echo the VM IP
  local MICROK8S_VM_IP=$(multipass list | tail -n1 | awk '{print $3}')
  echo ${MICROK8S_VM_NAME}"'s" IP is ${MICROK8S_VM_IP}

  # Install the Kubernetes.
  multipass exec ${MICROK8S_VM_NAME} -- sudo snap install microk8s --classic
  multipass exec ${MICROK8S_VM_NAME} -- sudo iptables -P FORWARD ACCEPT

  # Wait during wake up the microk8s.
  echo "Initial Setup is Starting."

  multipass exec ${MICROK8S_VM_NAME} -- sh -c 'while [ ! $(/snap/bin/microk8s.status > /dev/null; echo $?) -eq 0 ]; do echo -n .; sleep 1; done'

  echo "Initial Setup is Done."

  # Install & Setup the dns metrics-server addons.
  multipass exec ${MICROK8S_VM_NAME} -- /snap/bin/microk8s.enable dns metrics-server

  # Install & Setup the kubectl
  multipass exec ${MICROK8S_VM_NAME} -- sudo snap install kubectl --classic
  multipass exec ${MICROK8S_VM_NAME} -- sh -c '/snap/bin/microk8s.config > /home/multipass/.kube/kubeconfig'
  multipass exec ${MICROK8S_VM_NAME} -- cat /home/multipass/.kube/kubeconfig > ./${MICROK8S_VM_NAME}-kubeconfig

  echo "microk8s-init is done."
}
$ microk8s-init
Launched: nnao45-k8s-vm
nnao45-k8s-vm's IP is 192.168.64.4
2018-12-19T10:25:57Z INFO Waiting for restart...
microk8s v1.13.0 from 'canonical' installed
Initial Setup is Starting.
....Initial Setup is Done.
Enabling DNS
Applying manifest
service/kube-dns created
serviceaccount/kube-dns created
configmap/kube-dns created
deployment.extensions/kube-dns created
Restarting kubelet
DNS is enabled
Enabling metrics-server
clusterrolebinding.rbac.authorization.k8s.io/metrics-server:system:auth-delegator created
rolebinding.rbac.authorization.k8s.io/metrics-server-auth-reader created
apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io created
serviceaccount/metrics-server created
configmap/metrics-server-config created
deployment.extensions/metrics-server-v0.2.1 created
service/metrics-server created
clusterrole.rbac.authorization.k8s.io/system:metrics-server created
clusterrolebinding.rbac.authorization.k8s.io/system:metrics-server created
metrics-server enabled
kubectl 1.13.0 from Canonical✓ installed
microk8s-init is done.

$ kubectl --kubeconfig ./nnao45-k8s-vm-kubeconfig get all --all-namespaces
NAMESPACE     NAME                                         READY   STATUS    RESTARTS   AGE
kube-system   pod/kube-dns-6ccd496668-lrcw8                3/3     Running   0          76s
kube-system   pod/metrics-server-v0.2.1-6f76659f47-d48zn   2/2     Running   0          39s

NAMESPACE     NAME                     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)         AGE
default       service/kubernetes       ClusterIP   10.152.183.1    <none>        443/TCP         81s
kube-system   service/kube-dns         ClusterIP   10.152.183.10   <none>        53/UDP,53/TCP   78s
kube-system   service/metrics-server   ClusterIP   10.152.183.87   <none>        443/TCP         71s

NAMESPACE     NAME                                    READY   UP-TO-DATE   AVAILABLE   AGE
kube-system   deployment.apps/kube-dns                1/1     1            1           77s
kube-system   deployment.apps/metrics-server-v0.2.1   1/1     1            1           71s

NAMESPACE     NAME                                               DESIRED   CURRENT   READY   AGE
kube-system   replicaset.apps/kube-dns-6ccd496668                1         1         1       76s
kube-system   replicaset.apps/metrics-server-v0.2.1-6f76659f47   1         1         1       39s
kube-system   replicaset.apps/metrics-server-v0.2.1-7d7d77666c   0         0         0       71s

参考までにMacBook Proデュアルコア16Gメモリでのでの3分くらいでできます。

感想

とても簡単にMac上に kubernetes を構築することができました。 Istio もデプロイできるようなので、ぜひ一度お試しください。

Goでxo/xo入門

はじめに

この記事はGo Advent Calendar 2018の19日目の記事です。
結構今更ですが、僕は本番環境で投入しているものの、あんまり知られてないようなので改めて書いてみます。

自動DBモデリングツールxo/xoの紹介

github.com

ラーメンを美味しく料理するための香辛料ではなくてですね、

xo is a command-line tool to generate Go code based on a database schema or a custom query.

動いている任意のDBエンジンからテーブルを抽出して、Goの構造体とかをコードで生成するツールです。
みなさんはどうやってから構造体を生成してますか? どーのこーのして生成しているでしょうが、hoge_countがbigint型だから〜goだとintか〜あーUNSIGNEDだからuintか〜とか考えながら書き書きするとしたら、ちょっと面倒だなって感じです。

そこをいい感じに動いているDBから自動的に作ってくれるもんで、非常に楽になるわけです。

色々なDBに対応

公式のREADMEに書いてあるんですが、

がサポートされています。すごいですね。
もちろん機能的なサポートは各DB異なりますが、詳細は公式をどうぞ。

インストール

ツールも当然Goで書かれているのでインストールは簡単です。

$ go get -u github.com/xo/xo

# install with oracle support (see notes below)
$ go get -tags oracle -u github.com/xo/xo

はい、これで xo コマンドが使えるようになりました。

DBの用意

当然ながらDBを用意していただく必要があります。 便利な時代なので docker を使います。
今回はみんな大好き mysql を選びました。

$ docker run --name test-mysql --rm -d -e MYSQL_ROOT_PASSWORD=my-pw -p 3306:3306 mysql:8.0.0

環境変数とかパスワードとかバージョンは適当に変えてください。

DBの下準備

動いているDBから自動的にモデルとなるコードを生成してくれる コードなもんで、
適当なDBとテーブルを作っておきます。

まずmysqlに繋いで、

$ mysql -uroot -p'my-pw' -h 0.0.0.0

テスト用のDBとテーブルを作ってみました。

CREATE DATABASE IF NOT EXISTS `test-xo-db`;
USE `test-xo-db`

CREATE TABLE `test-xo-table` (
  id int(11) NOT NULL,
  hoge_count int(11) NOT NULL,
  delete_flg int(1) UNSIGNED  NOT NULL,
  created datetime NOT NULL,
  updated datetime NOT NULL
);

ALTER TABLE `test-xo-table`
 ADD PRIMARY KEY (`id`);

ALTER TABLE `test-xo-table`
  ADD INDEX index_name(`delete_flg`);

そこらへんに転がってそうなテーブルですね。

げってぃんぐすたーてっど

まず適当なプロジェクトを GOPATH 配下とかに作ります。

$ mkdir -p $GOPATH/path/to/my-project/models
$ cd $GOPATH/path/to/my-project

そうしたら、早速DBのモデルを作ってしまいましょう。

$ xo 'mysql://root:my-pw@0.0.0.0/test-xo-db' -o models

上記のコマンドも、環境ごとに変えてください、いつものやつです。

そうすると以下のようなファイルがmodels配下にできます。

$ ls -l models
-rw-r--r--  1 nnao45  nnao45  4242 12 18 17:33 testxotable.xo.go
-rw-r--r--  1 nnao45  nnao45  2137 12 18 17:33 xo_db.xo.go

中をみてみましょう。

testxotable.xo.go

// Package models contains the types for schema 'test-xo-db'.
package models

// Code generated by xo. DO NOT EDIT.

import (
        "errors"
        "time"
)

// TestXoTable represents a row from 'test-xo-db.test-xo-table'.
type TestXoTable struct {
        ID        int       `json:"id"`         // id
        HogeCount int       `json:"hoge_count"` // hoge_count
        DeleteFlg uint      `json:"delete_flg"` // delete_flg
        Created   time.Time `json:"created"`    // created
        Updated   time.Time `json:"updated"`    // updated

        // xo fields
        _exists, _deleted bool
}

// Exists determines if the TestXoTable exists in the database.
func (txt *TestXoTable) Exists() bool {
        return txt._exists
}

// Deleted provides information if the TestXoTable has been deleted from the database.
func (txt *TestXoTable) Deleted() bool {
        return txt._deleted
}

// Insert inserts the TestXoTable to the database.
func (txt *TestXoTable) Insert(db XODB) error {
        var err error

        // if already exist, bail
        if txt._exists {
                return errors.New("insert failed: already exists")
        }

        // sql insert query, primary key must be provided
        const sqlstr = `INSERT INTO test-xo-db.test-xo-table (` +
                `id, hoge_count, delete_flg, created, updated` +
                `) VALUES (` +
                `?, ?, ?, ?, ?` +
                `)`

        // run query
        XOLog(sqlstr, txt.ID, txt.HogeCount, txt.DeleteFlg, txt.Created, txt.Updated)
        _, err = db.Exec(sqlstr, txt.ID, txt.HogeCount, txt.DeleteFlg, txt.Created, txt.Updated)
        if err != nil {
                return err
        }

        // set existence
        txt._exists = true

        return nil
}

// Update updates the TestXoTable in the database.
func (txt *TestXoTable) Update(db XODB) error {
        var err error

        // if doesn't exist, bail
        if !txt._exists {
                return errors.New("update failed: does not exist")
        }

        // if deleted, bail
        if txt._deleted {
                return errors.New("update failed: marked for deletion")
        }

        // sql query
        const sqlstr = `UPDATE test-xo-db.test-xo-table SET ` +
                `hoge_count = ?, delete_flg = ?, created = ?, updated = ?` +
                ` WHERE id = ?`

        // run query
        XOLog(sqlstr, txt.HogeCount, txt.DeleteFlg, txt.Created, txt.Updated, txt.ID)
        _, err = db.Exec(sqlstr, txt.HogeCount, txt.DeleteFlg, txt.Created, txt.Updated, txt.ID)
        return err
}

// Save saves the TestXoTable to the database.
func (txt *TestXoTable) Save(db XODB) error {
        if txt.Exists() {
                return txt.Update(db)
        }

        return txt.Insert(db)
}

// Delete deletes the TestXoTable from the database.
func (txt *TestXoTable) Delete(db XODB) error {
        var err error

        // if doesn't exist, bail
        if !txt._exists {
                return nil
        }

        // if deleted, bail
        if txt._deleted {
                return nil
        }

        // sql query
        const sqlstr = `DELETE FROM test-xo-db.test-xo-table WHERE id = ?`

        // run query
        XOLog(sqlstr, txt.ID)
        _, err = db.Exec(sqlstr, txt.ID)
        if err != nil {
                return err
        }

        // set deleted
        txt._deleted = true

        return nil
}

// TestXoTablesByDeleteFlg retrieves a row from 'test-xo-db.test-xo-table' as a TestXoTable.
//
// Generated from index 'index_name'.
func TestXoTablesByDeleteFlg(db XODB, deleteFlg uint) ([]*TestXoTable, error) {
        var err error

        // sql query
        const sqlstr = `SELECT ` +
                `id, hoge_count, delete_flg, created, updated ` +
                `FROM test-xo-db.test-xo-table ` +
                `WHERE delete_flg = ?`

        // run query
        XOLog(sqlstr, deleteFlg)
        q, err := db.Query(sqlstr, deleteFlg)
        if err != nil {
                return nil, err
        }
        defer q.Close()

        // load results
        res := []*TestXoTable{}
        for q.Next() {
                txt := TestXoTable{
                        _exists: true,
                }

                // scan
                err = q.Scan(&txt.ID, &txt.HogeCount, &txt.DeleteFlg, &txt.Created, &txt.Updated)
                if err != nil {
                        return nil, err
                }

                res = append(res, &txt)
        }

        return res, nil
}

// TestXoTableByID retrieves a row from 'test-xo-db.test-xo-table' as a TestXoTable.
//
// Generated from index 'test-xo-table_id_pkey'.
func TestXoTableByID(db XODB, id int) (*TestXoTable, error) {
        var err error

        // sql query
        const sqlstr = `SELECT ` +
                `id, hoge_count, delete_flg, created, updated ` +
                `FROM test-xo-db.test-xo-table ` +
                `WHERE id = ?`

        // run query
        XOLog(sqlstr, id)
        txt := TestXoTable{
                _exists: true,
        }

        err = db.QueryRow(sqlstr, id).Scan(&txt.ID, &txt.HogeCount, &txt.DeleteFlg, &txt.Created, &txt.Updated)
        if err != nil {
                return nil, err
        }

        return &txt, nil
}

xo_db.xo.go

// Package models contains the types for schema 'test-xo-db'.
package models

// Code generated by xo. DO NOT EDIT.

import (
        "database/sql"
        "database/sql/driver"
        "encoding/csv"
        "errors"
        "fmt"
        "regexp"
        "strings"
)

// XODB is the common interface for database operations that can be used with
// types from schema 'test-xo-db'.
//
// This should work with database/sql.DB and database/sql.Tx.
type XODB interface {
        Exec(string, ...interface{}) (sql.Result, error)
        Query(string, ...interface{}) (*sql.Rows, error)
        QueryRow(string, ...interface{}) *sql.Row
}

// XOLog provides the log func used by generated queries.
var XOLog = func(string, ...interface{}) {}

// ScannerValuer is the common interface for types that implement both the
// database/sql.Scanner and sql/driver.Valuer interfaces.
type ScannerValuer interface {
        sql.Scanner
        driver.Valuer
}

// StringSlice is a slice of strings.
type StringSlice []string

// quoteEscapeRegex is the regex to match escaped characters in a string.
var quoteEscapeRegex = regexp.MustCompile(`([^\\]([\\]{2})*)\\"`)

// Scan satisfies the sql.Scanner interface for StringSlice.
func (ss *StringSlice) Scan(src interface{}) error {
        buf, ok := src.([]byte)
        if !ok {
                return errors.New("invalid StringSlice")
        }

        // change quote escapes for csv parser
        str := quoteEscapeRegex.ReplaceAllString(string(buf), `$1""`)
        str = strings.Replace(str, `\\`, `\`, -1)

        // remove braces
        str = str[1 : len(str)-1]

        // bail if only one
        if len(str) == 0 {
                *ss = StringSlice([]string{})
                return nil
        }

        // parse with csv reader
        cr := csv.NewReader(strings.NewReader(str))
        slice, err := cr.Read()
        if err != nil {
                fmt.Printf("exiting!: %v\n", err)
                return err
        }

        *ss = StringSlice(slice)

        return nil
}

// Value satisfies the driver.Valuer interface for StringSlice.
func (ss StringSlice) Value() (driver.Value, error) {
        v := make([]string, len(ss))
        for i, s := range ss {
                v[i] = `"` + strings.Replace(strings.Replace(s, `\`, `\\\`, -1), `"`, `\"`, -1) + `"`
        }
        return "{" + strings.Join(v, ",") + "}", nil
}

// Slice is a slice of ScannerValuers.
type Slice []ScannerValuer

しゅ、しゅごい・・・・!

生み出されるコードのうち重要な部分の紹介

type XODB interface

// XODB is the common interface for database operations that can be used with
// types from schema 'test-xo-db'.
//
// This should work with database/sql.DB and database/sql.Tx.
type XODB interface {
        Exec(string, ...interface{}) (sql.Result, error)
        Query(string, ...interface{}) (*sql.Rows, error)
        QueryRow(string, ...interface{}) *sql.Row
}

XODB と言うインターフェスが生成されますが、
基本的にはこのインターフェイスを通して、他の箇所で初期化し終わった sql.DB 型の変数を通していく事となります。

XOLog

// XOLog provides the log func used by generated queries.
var XOLog = func(string, ...interface{}) {}

XOLog は関数です。よく見ると何もありませんが、これは開発者が好きなログライブラリによる関数を突っ込めるように何も定義されていないのです。つまりこれを、

var XOLog = func(str string, args ...interface{}) {
    log.Infof("Run Query: %s, With Args: %v", str, args)
}

みたいな感じに定義してあげる事で好きなようにカスタマイズできるわけです。

xoをカスタマイズする。

さて、xo/xoの入門したのちにあることをに気づきます。

「あれ・・・?これって毎回モデリングして初期化した時に、どうやって新しく関数を定義すればいいの・・・?」

ここで、出てくるのが、goの template です。 (goの template って言うのは自動でgoのコードを生み出しまくる、jinja2使ったことがあればそれです)

xoのtemplateを指定する。

https://github.com/xo/xo/tree/master/templates

xo/xo レポジトリのtemplatesディレクトリに、デフォルトで使われている template が置いてあります。

xo コマンドで任意のtemplateディレクトリを指定して生成する場合、以下のように行います。

$ xo 'mysql://root:my-pw@0.0.0.0/test-xo-db' -o models --template-path templates/

こうすることによって、カスタマイズされたテンプレートを使うことができます。

例えば、XODB、XOLogを拡張してみる。

先ほどの xo/xo レポジトリのtemplatesディレクトリの xo_db.go.tplxo_package.go.tpl を編集してみます。

// XODB is the common interface for database operations that can be used with
// types from schema '{{ schema .Schema }}'.
//
// This should work with database/sql.DB and database/sql.Tx.
type XODB interface {
    Exec(string, ...interface{}) (sql.Result, error)
    Query(string, ...interface{}) (*sql.Rows, error)
    QueryRow(string, ...interface{}) *sql.Row
    QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
    QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}

// XOLog provides the log func used by generated queries.
var XOLog = func(str string, args ...interface{}) {
    log.Infof("Run Query: %s, With Args: %v", str, args)
}
// Package {{ .Package }} contains the types for schema '{{ schema .Schema }}'.
package {{ .Package }}

// Code generated by xo. DO NOT EDIT.

import (
    "database/sql"
    "database/sql/driver"
    "encoding/csv"
    "errors"
    "fmt"
    "regexp"
    "strings"
    "time"
    "context"
    "log"
)

簡単にできました。これで、コンテキスト付きのクエリ発行やロギング使用ができることが可能です。
もちろん、どちらもテンプレート内にある仕様やインターフェースを満たしている範囲での編集をしている事に注意してください。

例えば、独自関数を拡張してみる。

mysqlの場合は、まぁ適当でいいんですが、 mysql.type.go.tpl とかに追記しておくと、どんなテーブルでも独自関数が生成されるようになります。

例えばフルスキャンするような関数を定義したい場合は以下のように追記します。

// {{ .Name }}ByAll retrieves all rows from '{{ $table }}'
func {{ .Name }}ByAll(db XODB) ([]*{{ .Name }}, error) {
    // sql query
    const sqlstr = `SELECT ` +
        `{{ colnames .Fields }} ` +
        `FROM {{ $table }} `

    // define context
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    // run query
    XOLog(sqlstr)
    result, err := db.QueryContext(ctx, sqlstr)
    if err != nil {
        return []*{{ .Name }}{}, err
    }
    {{ $short }}l := []*{{ .Name }}{}

    for result.Next() {
        {{ $short }} := &{{ .Name }}{
            _exists: true,
        }

        if err := result.Scan({{ fieldnames .Fields (print "&" $short) }}); err != nil {
            return []*{{ .Name }}{}, err
        }

        {{ $short }}l = append({{ $short }}l, {{ $short }})
    }

    return {{ $short }}l, nil
}

注意としては、goのtemplateのように定義された構造体のキーを {{ .Name }} で引いていたり、
{{ $short }}変数に代入しているので、一見非常にわかりにくいです。

でも、各種他のデフォルトで定義されているものを参考にして試行錯誤すれば、あまり苦労せずにかけると思います。

終わりに。

xo/xo を使ってDBから動的にgoのコードを生み出す方法をみました。
非常に楽できますが、モデル生成後は生み出したコードを編集せずに使う前提なので癖も強く、
依存性も強いので、しっかり動きを理解することで本番環境に入れることをお勧めします。