multipassでOSX上にmicro-k8sを立てる。
はじめに
この記事はCyberAgent Advent Calendarの20日目の記事になります。
ローカルにkuberentesを立てる時、皆さんどうしてますか?
うん、多くの人は minikube をたてて試していることでしょう。
しかし 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 とかいうUbuntuのVM管理ツールが便利だったので使う。
リリースページにある pkg
ファイルから入れるのが早い。
なんでmultipassだといいのん?
現状で「手っ取り早くOSX上でUbuntuのVMが欲しい」と言う用途に対して一番適したツールだからです。
$ 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
わっしょーい🤗
んーでも何にもないね😷 coredns
や kube-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
衝撃的なくらい簡単なんだが・・・。
(ちなみにいえば service
を type: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の紹介
ラーメンを美味しく料理するための香辛料ではなくてですね、
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.tpl
と xo_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のコードを生み出す方法をみました。
非常に楽できますが、モデル生成後は生み出したコードを編集せずに使う前提なので癖も強く、
依存性も強いので、しっかり動きを理解することで本番環境に入れることをお勧めします。