- 投稿日:2019-10-04T17:02:24+09:00
GolangでPKI入門 - 2
1.この記事の対象の人
- Golangで、自己署名CA証明書を作りたい人
- Golangで、自己署名CAで署名した「サーバ証明書」を作りたい人
- Golangで、自己署名CAで署名した「クライアント証明書」(クライアント認証用)を作りたい人
※作成した証明書と鍵はテスト用途でお使いください。
※作成した秘密鍵は適切に管理してください。2.Golangで自己署名CA証明書作成
以下エラー処理は省略しますので悪しからず
2048bitのCA用RSA秘密鍵、公開鍵を作成
privateCaKey, err := rsa.GenerateKey(rand.Reader, 2048) publicCaKey := privateCaKey.Public()自己署名CA証明書のサブジェクト作成
CN: ca01
OU: Example Org Unit
O: Example Org
C: JP
に設定// https://tools.ietf.org/html/rfc5280#section-4.1.2.6 subjectCa := pkix.Name{ CommonName: "ca01", OrganizationalUnit: []string{"Example Org Unit"}, Organization: []string{"Example Org"}, Country: []string{"JP"}, }自己署名CA証明書用構造体の設定
- CA証明書なので IsCA と BasicConstraintsValid は true に設定
https://tools.ietf.org/html/rfc5280#section-4.2.1.9
caTpl := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: subjectCa, NotAfter: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC), NotBefore: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC), IsCA: true, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, BasicConstraintsValid: true, }自己署名CA証明書作成
caCertificate, err := x509.CreateCertificate(rand.Reader, caTpl, caTpl, publicCaKey, privateCaKey)自己署名CA証明書をPEM形式で保存
var f *os.File f, err = os.Create("ca01.crt") err = pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: caCertificate}) err = f.Close()自己署名CAの秘密鍵をPEM形式で保存
derCaPrivateKey := x509.MarshalPKCS1PrivateKey(privateCaKey) err = pem.Encode(f, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: derCaPrivateKey}) err = f.Close()3.Golangでサーバ証明書作成
自己署名CAの秘密鍵で署名した「サーバ証明書」を作ります
2048bitのサーバ証明書用RSA秘密鍵、公開鍵を作成
privateSslKey, err := rsa.GenerateKey(rand.Reader, 2048) publicSslKey := privateSslKey.Public()サーバ証明書のサブジェクト作成
CN: svr01
OU: Example Org Unit
O: Example Org
C: JP
に設定// https://tools.ietf.org/html/rfc5280#section-4.1.2.6 subjectSsl := pkix.Name{ CommonName: "svr01", OrganizationalUnit: []string{"Example Org Unit"}, Organization: []string{"Example Org"}, Country: []string{"JP"}, }サーバ証明書用構造体の設定
- ブラウザでエラーが表示されないように、subjectAlternativeNameを指定します。
SubjectAlternativeNameを svr01.example.org に指定
DNSNames: []string{"svr01.example.org"}
- サーバ証明書用途なので、拡張キー使用法をサーバ認証に指定します。
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
sslTpl := &x509.Certificate{ SerialNumber: big.NewInt(123), Subject: subjectSsl, NotAfter: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC), NotBefore: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, DNSNames: []string{"svr01.example.org"}, }サーバ証明書作成
- サーバ証明書に署名する秘密鍵は「自己署名CAの秘密鍵」を指定します。
derSslCertificate, err := x509.CreateCertificate(rand.Reader, sslTpl, caTpl, publicSslKey, privateCaKey)サーバ証明書をPEM形式で保存
f, err = os.Create("svr01.crt") err = pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: derSslCertificate}) err = f.Close()サーバ証明書の秘密鍵をPEM形式で保存
f, err = os.Create("svr01.key") derPrivateSslKey := x509.MarshalPKCS1PrivateKey(privateSslKey) err = pem.Encode(f, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: derPrivateSslKey}) err = f.Close()4.Golangでクライアント証明書作成
自己署名CAの秘密鍵で署名した「クライアント証明書」を作ります
2048bitのクライアント証明書用RSA秘密鍵、公開鍵を作成
privateClientKey, err := rsa.GenerateKey(rand.Reader, 2048) publicClientKey := privateClientKey.Public()クライアント証明書のサブジェクト作成
CN: client01
OU: Example Org Unit
O: Example Org
C: JP
に設定// https://tools.ietf.org/html/rfc5280#section-4.1.2.6 subjectClient := pkix.Name{ CommonName: "client01", OrganizationalUnit: []string{"Example Org Unit"}, Organization: []string{"Example Org"}, Country: []string{"JP"}, }クライアント証明書用構造体の設定
- クライアント証明書用途なので、拡張キー使用法をクライアント認証に指定します。
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
cliTpl := &x509.Certificate{ SerialNumber: big.NewInt(456), Subject: subjectClient, NotAfter: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC), NotBefore: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, }クライアント証明書作成
- クライアント証明書に署名する秘密鍵は「自己署名CAの秘密鍵」を指定します。
derClientCertificate, err := x509.CreateCertificate(rand.Reader, cliTpl, caTpl, publicClientKey, privateCaKey)PKCS#12形式でクライアント証明書と秘密鍵を保存
クライアント認証用途なので、クライアント証明書とクライアント証明書の秘密鍵両方が必要です。ここでは、PKCS#12形式をつかって、クライアント証明書とクライアント証明書の秘密鍵をまとめて保存します。
PKCS#12形式を作成するために "software.sslmate.com/src/go-pkcs12" packageをimport。
PKCS#12形式のpasswordを pincode という文字列で指定
cert, err := x509.ParseCertificate(derClientCertificate) f, err = os.Create("client01.p12") //PKCS#12 [RFC7292] including client private keys, client certificates p12, err := pkcs12.Encode(rand.Reader, privateClientKey, cert, nil, "pincode") _, err = f.Write(p12) err = f.Close()5. コード
- 投稿日:2019-10-04T13:21:34+09:00
Go(Echo) + Vue.js + nginx の環境をDocker Composeで立てる。
はじめに
goa、ginと触ったので、Echoあたりも触ってみようと思ったのと、自分自身、サーバーサイドもフロントエンドも一括で担当することが多いこともあり、「どうせだ、docker環境でフロントエンドも含めて環境を作ってみっか。」というのが始まりです。
GoコンテナはAPIを配信、Vue.jsでAPIを叩いて、値を画面にレンダリング。
みたいな想定でやっています。また、nginx噛ませてルーティングさせてます。駄文だったらすいません。
先に言っておくと、長文です。なお、以下のようにしています。
Go
- Go Modules
- ホットリロード
を利用してます。
Vue.js
- vue-cli3
- aixos
を利用してます。
環境
- Mac OS X 10.14.6(Mojave)
- Docker version 19.03.2
- docker-compose version 1.24.1
※ ベースの環境だけ記述しておきます。GoのバージョンなどはDockerfileに記述してますので。
手順
初めのファイル構造
こっからスタートします。
nginxは後から設定するのでmyapp/etc/nginx
配下は一旦空にしています。myapp ├── docker │ ├── go │ │ └── Dockerfile │ └── vue │ └── Dockerfile ├── docker-compose.yml └── etc └── nginx └── .
各Dockerfileの中身
Go
非常にシンプルです。
Goのバージョンは1.13。
Go Modulesを利用するため、ENV GO111MODULE=on
を指定。
ホットリロード環境を作りたいのでfresh
をgo get
しています。myapp/docker/go/DockerfileFROM golang:1.13.1-alpine ENV GO111MODULE=on RUN apk update && \ apk add emacs curl git && \ go get github.com/pilu/freshVue.js
nodeのバージョンは、現時点(2019年10月)で最新の
8.16
。
vue-cli3を利用するので@vue/cli
をnpm install
しています。mysqpp/docker/vue/DockerfileFROM node:8.16-alpine RUN apk update && \ npm install -g npm && \ npm install -g @vue/clidocker-compose.ymlの中身(初期)
nginxの設定は後で足します。
まずは、GoとVue.jsのコンテナを作るところから始めていきたいので。docker-compose.ymlversion: '3' services: vue: build: context: ./docker/vue container_name: myapp-vue ports: - 8080:8080 volumes: - .:/app working_dir: /app tty: true # command: sh -c "cd assets && yarn install && yarn serve" go: build: context: ./docker/go container_name: myapp-go ports: - 8082:8082 volumes: - .:/go/src working_dir: /go/src/app tty: true # command: freshちなみに、
go
、vue
共にcommandがコメントアウトしているのは、一旦コンテナで作業する必要があるので、コメントアウトしています。
go
は、Go Modulesの初期設定をしてからホットリロードを設定しないとエラーになります(なりました)。
vue
は、コンテナ内でvue create
をしたいので、コメントアウトしています。コンテナ立ち上げ
まずはなにはともあれ、ビルドします。
$ docker-compose build
※ npm周りでWARNが出る可能性がありますが、ここは無視します。
up -d
でバックグラウンド立ち上げをします(オプション-d
無くてもいいですが、その場合は以降は別タブで作業してください)。$ docker-compose up -d
コンテナが立ち上がっているか確認。
$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 9897f0c5c15c nginx "nginx -g 'daemon of…" 9 seconds ago Up 7 seconds 0.0.0.0:10080->80/tcp myapp-nginx 9e8c34dacba8 kaikairanban_go "/bin/sh" 11 seconds ago Up 9 seconds 0.0.0.0:8082->8082/tcp myapp-go 7178d2a496dc kaikairanban_vue "docker-entrypoint.s…" 40 seconds ago Up 39 seconds 0.0.0.0:8080->8080/tcp myapp-vueこの時点でappディレクトリがmyappディレクトリ内にできているので、
ls -la
とかで確認してみてください。Goのコンテナを設定していく
appディレクトリに移動して、
main.go
ファイルを作ります。$ cd app && emacs main.go※ emacsの部分は、vimでもなんでもいいです。筆者はemacsを使っているのでこうなります。
main.go
の中身は一旦こんな感じにしておきます。main.gopackage main import ( "fmt" ) func main () { fmt.Println("Hello World !!") }
Goのコンテナに入ります。$ docker exec -it myapp-go /bin/sh /go/src/app # # コンテナ内で ls -la で確認してみるとこんな感じになっているはず。 /go/src/app # ls -la total 4 drwxr-xr-x 4 root root 128 Oct 3 08:35 . drwxr-xr-x 5 root root 160 Oct 3 08:25 .. -rw-r--r-- 1 root root 90 Oct 3 08:35 main.go drwxr-xr-x 2 root root 64 Oct 3 08:25 tmp
コンテナ内でGo Modulesの初期設定をします。/go/src/app # go mod init go: creating new go.mod: module app /go/src/app # ls -la total 8 drwxr-xr-x 5 root root 160 Oct 3 08:41 . drwxr-xr-x 5 root root 160 Oct 3 08:25 .. -rw-r--r-- 1 root root 20 Oct 3 08:41 go.mod -rw-r--r-- 1 root root 90 Oct 3 08:35 main.go drwxr-xr-x 2 root root 64 Oct 3 08:25 tmpgo.modができましたね。
Ctr + d
とかでコンテナの外に出て、go.mog
ファイルの中身を確認すると以下のうようになっています。go.modmodule app go 1.13
では、Echoを導入していきます。
コンテナの外からで良いので、以下のようにmain.go
を編集します。
なんてことはない、Echoのクイックスタート(ほぼ)そのままです。main.gopackage main import ( "net/http" "github.com/labstack/echo" ) func main () { e := echo.New() e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) e.Logger.Fatal(e.Start(":8082")) // e.Startの中はdocker-composeのgoコンテナで設定したportsを指定してください。 }
main.goが編集できたら、docker-compose.yml
のgoコンテナの記述部分のcommandのコメントアウトを外します。docler-compose.yml# docker-compose.ymlの一部抜粋 go: build: context: ./docker/go container_name: myapp-go ports: - 8082:8082 volumes: - .:/go/src working_dir: /go/src/app tty: true command: fresh # ここのコメントアウトを外す。保存したら、
docker-compose up
で更新します(一旦、一つ前のコンテナ内作業後、docker-compose down
でコンテナを停止しておいても良いです)。以下のような状態になったら、Echo導入完了&ホットリロードもOnになりました。
$ docker-compose up myapp-vue is up-to-date Recreating myapp-go ... done Recreating myapp-nginx ... done Attaching to myapp-vue, myapp-go, myapp-nginx myapp-go | 8:53:07 runner | InitFolders myapp-go | 8:53:07 runner | mkdir ./tmp myapp-go | 8:53:07 runner | mkdir ./tmp: file exists myapp-go | 8:53:07 watcher | Watching . myapp-go | 8:53:07 main | Waiting (loop 1)... myapp-go | 8:53:07 main | receiving first event / myapp-go | 8:53:07 main | sleeping for 600 milliseconds myapp-go | 8:53:08 main | flushing events myapp-go | 8:53:08 main | Started! (5 Goroutines) myapp-go | 8:53:08 main | remove tmp/runner-build-errors.log: no such file or directory myapp-go | 8:53:08 build | Building... myapp-go | 8:53:14 runner | Running... myapp-go | 8:53:14 main | -------------------- myapp-go | 8:53:14 main | Waiting (loop 2)... myapp-go | 8:53:14 app | myapp-go | ____ __ myapp-go | / __/___/ / ___ myapp-go | / _// __/ _ \/ _ \ myapp-go | /___/\__/_//_/\___/ v3.3.10-dev myapp-go | High performance, minimalist Go web framework myapp-go | https://echo.labstack.com myapp-go | ____________________________________O/_______ myapp-go | O\ myapp-go | 8:53:14 app | ⇨ http server started on [::]:8082上記の状態で、localhost:8082にアクセスすると、以下のような画面が表示されます。
見事に世界に挨拶ができましたね。
ちなみに、
go.mod
はこんな感じになっています。go.modmodule app go 1.13 require ( github.com/labstack/echo v3.3.10+incompatible github.com/labstack/gommon v0.3.0 // indirect golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc // indirect )Vueのコンテナを設定していく
コンテナが立ち上がっている状態で、vueのコンテナに入ります。
$ docker exec -it myapp-vue /bin/sh /app #vue-cliでvue環境を設定していきます。
/app # vue create assets
上記コマンドを打つと、質問形式で色々聞かれます。
? Your connection to the default yarn registry seems to be slow. Use https://registry.npm.taobao.org for faster installation? (Y/n) Y ←Yesにします。と聞かれた後、Vueの設定に入っていきます。
お好みで設定すれば良いと思いますが、今回は以下のようにします。Enterを無心で押していくと、以下のような感じで設定が始まります。
サクセスすると、以下のような表示になるかと思います。
ここまできたら、Ctr + d
でコンテナを抜けます。
docker-compose down
で一旦コンテナを落としておきます。
そして、docker-compose.yml
のvueコンテナの部分でコメントアウトにしていた部分のコメントアウトを外します。vue: build: context: ./docker/vue container_name: myapp-vue ports: - 8080:8080 volumes: - .:/app working_dir: /app tty: true command: sh -c "cd assets && yarn install && yarn serve" # ここのコメントアウトを外す。
バックグラウンドでコンテナを立ち上げた後、vueコンテナのlogだけを表示させてみます。$ docker-compose up -d $ docker-compose logs -f vue
以下のような感じでvueコンテナが立ち上がればOKです。
上記画面が出た状態で、localhost:8080にアクセスすると、以下のような画面が表示されます。
Vue.js Appの世界に無事、迎え入れられてもらうことができました。nginxのコンテナを作る
一旦、
docker-compose down
で、コンテナを落としておきます。
そして、docker-compose.yml
に追記&nginx.conf
を作ります。docker-compose.ymlversion: '3' services: vue: build: context: ./docker/vue container_name: myapp-vue ports: - 8080:8080 volumes: - .:/app working_dir: /app tty: true command: sh -c "cd assets && yarn install && yarn serve" go: build: context: ./docker/go container_name: myapp-go ports: - 8082:8082 volumes: - .:/go/src working_dir: /go/src/app tty: true command: fresh # こっから下を追加 nginx: image: nginx depends_on: - go container_name: myapp-nginx ports: - 80:80 environment: - TZ=Asia/Tokyo volumes: - ./etc/nginx/nginx.conf:/etc/nginx/nginx.confnginx.confworker_processes auto; events { worker_connections 1024; } http { server { listen 80; location /api/ { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_pass http://go:8082/; } location / { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_pass http://vue:8080/; } } }
準備ができたら、docker-compose up -d
でコンテナを立ち上げます。
docker-compose logs -f
などで、go、Vueどちらのコンテナも立ち上がって、準備ができたのを確認できたら、http://localhost でアクセスしてみましょう。
この画面になったらOKです。Vue側からGo側へリクエストを送り、値を得る。
axios
を使って、GoのAPIを叩き値を取ってくることを想定して、もう少しファイルを編集していきます。axiosを使えるようにする。
axiosを使えるようにするため、
package.json
(assetsディレクトリ内にあるはずです)を編集します。
dependencies
の部分にaxiosを追加します(バージョンは、2019年10月最新のものにしてます)。
併せて、console.log()
が入っているときにWaringが出ないようにするため、esLintConfg
のrules
に、
"no-console": "off"
を追加します。package.json{ ... "dependencies": { "core-js": "^2.6.5", "vue": "^2.6.10", "axios": "^0.19.0" }, ... ... "eslintConfig": { ... "rules": { "no-console": "off" }, ... }, ... }
グローバルにaxiosを使えるように、main.jsに登録をします。main.jsimport Vue from 'vue' import App from './App.vue' // ここから import axios from 'axios' Vue.prototype.$axios = axios // ここまでを追加 Vue.config.productionTip = false new Vue({ render: h => h(App), }).$mount('#app')ファイルが編集できたら、
docker-compose restart vue
で、設定を更新します。axiosで、GoのAPIを叩く。
今回は超簡易的に、
- ブラウザで http://localhost にアクセス。
- APIが叩かれる(GET)。
- Go側からレスポンスを返す。
console.log()
を使って、ブラウザのコンソール画面でレスポンスの中身を確認する。ということを行います。
では、
App.vue
を編集していきます。
Vueのcreatedフックのなかで、axiosを使うように記述します。
(変更箇所のあるscriptタグのブロックだけ以下に表示させてます。)App.vue// <template> のブロックがある <script> import HelloWorld from './components/HelloWorld.vue' export default { name: 'app', components: { HelloWorld }, // createdの中でaxiosを使います。get()の中のURLは、nginx.confで設定してるので、 /api/ になっています。 created () { this.$axios.get('http://localhost/api/') .then(response => { console.log(response) }) } } </script> // <style> のブロックがあるできたら、ブラウザの開発者ツールでコンソールタブを開いて、 http://localhost にアクセスしてみましょう。
(もしくは、リロードしてみましょう。)画像のように、レスポンスが返ってきているのが確認できたらOKです。
あとは、返ってきた値を上手く調理するなどして、画面にレンダリングするとかしてみてみるといいんじゃないでしょうか。
(その辺は、余裕あったら続きを書こうと思います。記録としても残しておく意味で。)以上。
- 投稿日:2019-10-04T00:28:42+09:00
client-goを利用してKubernetesのnamespaceを切り替える対話式CLIツールを作成した。
今回作ったツール - kns
Kubernetesによく触るのですが、Golangを書いたことがなかったので、ずっと触りたかったClient-goを使ってnamespaceを切り替えるツールを作成しました。
https://github.com/jlandowner/kns
namespaceを切り替えるツールとしては
kubectx
に同梱されているkubens
が有名かと思いますが、bashで書かれているので、Golangで作るのに丁度いい題材となりました。
https://github.com/ahmetb/kubectx/blob/master/kubens使い勝手はgifをみていただければと思いますが、シンプルで割りかし良いと思っています。
この手のツールは作っても使わないことが多いので・・・今回のツール作成の際のポイントを少しまとめたいと思います。
KubernetesでGolangに興味を持ったエンジニアのご参考になれば幸いです。client-goでKubernetes API Serverにアクセス
基本的にはclient-goのリポジトリにあるサンプルコードを参考に作りました。
https://github.com/kubernetes/client-go/tree/master/examplesこちらに
in-cluster-client-configuration
とout-of-cluster-client-configuration
があるのですが、今回はコマンドラインツールのためout-of-cluster-client-configuration
を参考にします。基本的にクライアントのkubeconfigの情報を利用してAPI Serverに繋ぎに行きます。
(通常kubeconfigは~/.kube/config
にあります。)namespace名を取得するまでの簡単流れは以下の通りです。
大事なところだけ見ると結構シンプルです。import ( "os" "gopkg.in/yaml.v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" ) // kubeconfigのパスを取得 var kubeconfigPath *string *kubeconfigPath = filepath.Join(os.Getenv("HOME"), ".kube", "config") // kubeconfigを元にclientcmd.BuildConfigFromFlagsでconfigオブジェクトを取得 config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig) // clientsetなるオブジェクトを取得。(こいつで各種API操作が可能です) clientset, err := kubernetes.NewForConfig(config) // 今回はnamespaceのリストを以下のコマンドで取得 nss, err := clientset.CoreV1().Namespaces().List(metav1.ListOptions{}) // namespaceの名前を取得。 ns_name = nss.Items[i].ObjectMeta.Name取得した
ns
の持っている属性はgodocから見れます
https://godoc.org/k8s.io/api/core/v1#Namespace
ただし、Kubernetesのオブジェクトは共通してtypemeta
とobjectmeta
という2つの属性を持っていますので、一つ一つ調べる必要はありません。・Typemeta
https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#TypeMeta・Objectmeta
https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#ObjectMetaKubernetesのマニフェストでお馴染みの属性たちです。今回は名前が欲しかったので、
ObjectMeta.Name
で取得しました。podやserviceなど他のオブジェクトの操作方法はgodocをご参照ください。
(以下はcore/v1のリンクです。)
https://godoc.org/k8s.io/client-go/kubernetes/typed/core/v1Golangでyamlの読み書き
kubeconfigはyamlで書かれているので、yamlを読み書きする必要があります。
client-goのパッケージにkubeconfigを操作するものがあるのですが、今回はGolangの勉強をかねているので、あえてyamlのパース部分を実装しました。
import ( "io/ioutil" "os" "path/filepath" "strconv" "gopkg.in/yaml.v2" ) // Kubeconfigの構造体を定義 type Kubeconfig struct { APIVersion string `yaml:"apiVersion"` Kind string Clusters []interface{} Contexts []Context CurrentContext string `yaml:"current-context"` Preferences interface{} Users interface{} } // Contextの構造体を定義 type Context struct { Context struct { Cluster string Namespace string User string } Name string } // read kubeconfig file func readKubeconfig(kubeconfigPath string) (kConfig Kubeconfig) { buf, err := ioutil.ReadFile(kubeconfigPath) if err != nil { panic(err) } // yaml to struct err = yaml.Unmarshal(buf, &kConfig) if err != nil { panic(err) } return kConfig } // write kubeconfig to file func writeKubeconfig(kubeconfigPath string, kConfig Kubeconfig) (err error) { err = nil // struct to yaml out, err := yaml.Marshal(kConfig) if err != nil { panic(err) } // Write kubeconfig file err = ioutil.WriteFile(kubeconfigPath, out, 0600) if err != nil { panic(err) } return err } func main() { ... // 読み込み kConfig := readKubeconfig(*kubeconfigPath) ... // 構造体の中身を編集 kConfig.Contexts[i].Context.Namespace = namespace ... // 書き込み err := writeKubeconfig(*kubeconfigPath, kConfig) ... }構造体を定義すると
yaml.Unmarshal()
で、構造体の名前だけでうまくyamlをパースしてくれます。反対もyaml.Marshal()
で簡単にファイル出力可能な形式にしてくれます。ただしちょっといくつかの制約があり、少し調べました。
・属性名は大文字から始める。
・構造体で使えないハイフンなどは読み取ってくれないので、その場合はyaml:"current-context"
で対象を指定する。type Kubeconfig struct { APIVersion string `yaml:"apiVersion"` Kind string Clusters []interface{} Contexts []Context CurrentContext string `yaml:"current-context"` Preferences interface{} Users interface{} }interface{}はまだ使いこなせていないのですが、型指定を曖昧にできるものという認識で、今回は使わない属性はinterface{}にしました。(合っていなければご指摘ください。。)
対話式のコマンドライン
最後に対話式コマンドラインについてです。
インタラクティブな操作がしたかったのですが、思いのほか簡単に書けました。こちらの記事を参考に、bufioパッケージのscannerというものを使いました。
https://blog.linkbal.co.jp/1971/import ( "bufio" "errors" "fmt" "os" "strconv" ) // show dialog to ask func askNamespaceNum(max int) (i int, qerr error) { fmt.Print("** Which namespace do you want to switch? (exit: q)\n") fmt.Print("Select[n] => ") i = 99 qerr = nil // scannerの取得 scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { // 入力内容の取得 input := scanner.Text() if atoi, err := strconv.Atoi(input); err == nil { i = atoi if 0 <= i && i < max { break } fmt.Print("Select[n] => ") } else if input == "q" { qerr = errors.New("quit") // breakすると入力待ち終了 break } else { fmt.Print("Select[n] => ") } } if err := scanner.Err(); err != nil { panic(err) } return i, qerr }Podの一覧から
kubectl exec
したり、kubectl logs
したり出来たらいいなと色々思いつきます。最後に
Kubernetesやコンテナ周りはほとんどGolangの世界なので、それらの深い理解のためにGolangの学習は大きいと思います。
Golang自体非常に読みやすいので、少しわかるだけでもさらにソースコードリーディングが捗ります。今後はCustom ControllerやOperatorを作成したいです。
- 投稿日:2019-10-04T00:03:51+09:00
私の知らない go 言語: 構造体の埋め込み
https://github.com/nabetani/marshaljsonf64
を書くに当たって気がついた go と json.Marshal / json.Unmarshal の仕様をいくつか紹介する。組み込み型を埋め込む
package main import "fmt" type t0 struct { int } func main() { v0 := t0{int: 123} // "int:" で初期化する //v0 = 456 // エラー v0.int = 456 // こう書く fmt.Println(v0) //=> "{456}" }
type foo int
とtype foo struct{ int }
は似ている。後者を使うべき理由に心当たりはない。
ポインタを埋め込む
package main type t10 struct { *int } type t11 struct { **int // "unexpected *, expecting name" というエラーになる } type pint *int type t12 struct { pint // "embedded type cannot be a pointer" というエラーになる } func main() { vi := 1 v10 := t10{int: &vi} // "int:" で初期化する *v10.int = 3 // こう書く。「*v10=3」とは書けない }
t10
が ok で、t12
がエラーなのは、かなり思いがけなかった。両方埋め込む
package main type t20 struct { int *int // "duplicate field int" というエラーになる }まあそうだよね。
配列・スライス・map を埋め込む
package main type t30 struct { [1]int // "unexpected [, expecting field name or embedded type" } type t31 struct { []int // "unexpected [, expecting field name or embedded type" } type t32 struct { map[int]int // "syntax error: unexpected map, expecting field name or embedded type" } type a [1]int type s []int type m map[int]int type t30x struct{ a } // ok type t31x struct{ s } // ok type t32x struct{ m } // ok func main() { v30 := t30x{a: [1]int{12}} v30[0] = 123 // "invalid operation: v30[0] (type t30x does not support indexing)" v30.a[0] = 456 v31 := t31x{s: []int{34}} v31[0] = 345 // "invalid operation: v31[0] (type t31x does not support indexing)" v31.s[0] = 678 v32 := t32x{m: map[int]int{56: 78}} v32[0] = 99 // invalid operation: v32[0] (type t32x does not support indexing) v32.m[0] = 99 }そのままでは埋め込めない。アクセスする名前がなくて困るからだと思う。
名前をつけると埋め込める。
indexing はメソッドじゃないのでそのままでは呼べない。何重にも埋め込む
package main type t40 struct{ foo int } type t41 struct{ t40 } // そのまま埋め込む type t42 struct{ *t41 } // ポインタを埋め込む func main() { v40 := t40{1} v41 := t41{v40} t42 := t42{&v41} t42.foo = 3 // 何段階も下れる }ポインタ埋め込みとそのまま埋め込みを混在させることができる。
混在していても一気に一番奥のメンバにアクセスできる自分を埋め込む
ポインタを埋め込めるので、自分自身を埋め込むことができる。
package main type T50 struct { *T50 Foo string } func main() { a := T50{T50: nil, Foo: "a"} b := T50{T50: nil, Foo: "b"} c := T50{T50: nil, Foo: "c"} a.T50 = &a b.T50 = &c c.T50 = &b }有意義な使いみちの心当たりはない。
埋め込みとJSON ― 名前の重複
埋め込み構造体を JSON にすると、そのままフラットに展開される。
じゃあ、同じメンバ名を持つ構造体を埋め込んだらどうなるか。
- 実行時エラーになる(好ましい)
- 最初に見つかった方だけが出る(まあまあ酷い仕様)
- 同名のキーで出てきてしまう(かなり酷い仕様)
のいずれかだと予想したんだけど、予想は外れた。
package main import ( "encoding/json" "fmt" ) type t70 struct { Foo string Bar string } type t71 struct { Foo string Baz string } type t72 struct { t70 t71 } type t70x struct { t70 } type t71x struct { t71 } type t74 struct { t70x t71 } func asjson(i interface{}) { j, e := json.Marshal(i) if e != nil { fmt.Println("err:", e) return } fmt.Println("json:", string(j)) } func main() { v70 := t70{"70.foo", "70.bar"} v71 := t71{"71.foo", "71.baz"} asjson(v70) //=> json: {"Foo":"70.foo","Bar":"70.bar"} asjson(v71) //=> json: {"Foo":"71.foo","Baz":"71.baz"} asjson(t72{v70, v71}) //=> json: {"Bar":"70.bar","Baz":"71.baz"} asjson(t74{t70x{v70}, v71}) //=> json: {"Bar":"70.bar","Foo":"71.foo","Baz":"71.baz"} }正解は、
- 同名のフィールドがある場合はそのフィールドは無視される(かなり酷い仕様)
だった。しかも
t74
の例のとおり
- ただし、深さが違う場合は浅い方優先
だとおもう。
びっくりした。
ちなみに。
- Unmarshal でも無視される。
- 一方に
json:"Foo"
を付与すると、そっちが勝って、ついてないほうが無視される。- 両方に
json:"Foo"
を付与すると、両方無視される。埋め込みとJSON ― 名前がつく場合とつかない場合
package main import ( "encoding/json" "fmt" ) type T80 []int type T81 struct{ Foo int } type T82 struct{ Bar int } type T83 struct{ Baz int } type T84 T83 type T85 struct { T80 T81 T82 `json:"T82"` T84 } func asjson(i interface{}) { j, e := json.Marshal(i) if e != nil { fmt.Println("err:", e) return } fmt.Println("json:", string(j)) } func main() { v := T85{ T80: T80{1, 2}, T81: T81{34}, T82: T82{56}, T84: T84{78}, } asjson(v) //=> json: {"T80":[1,2],"Foo":34,"T82":{"Bar":56},"Baz":78} }int や スライスのような型を埋め込む場合と、構造体を埋め込む場合で動作が違う。
- int や スライスのような型は、型名がキー名になる
json:"hoge"
のようなものがついていれば、そのキー名がつく- それ以外なら、親構造体のメンバにそのまま展開される
となる。
埋め込みとJSON ― ポインタを埋め込む場合
package main import ( "encoding/json" "fmt" "strings" ) type T90 struct { Foo int Bar int } type T91 struct { Baz int Qux int } type T92 struct { *T90 *T91 } func asjson(i interface{}) { j, e := json.Marshal(i) if e != nil { fmt.Println("err:", e) return } fmt.Println("json:", string(j)) } func t92FromJSON(j string) { v := T92{} e := json.Unmarshal([]byte(j), &v) if e != nil { fmt.Println("err:", e) return } fmt.Println(strings.Replace(fmt.Sprintf("val: %_", v), "%!_", "", -1)) } func main() { asjson(T92{T90: &T90{12, 34}, T91: nil}) //=> json: {"Foo":12,"Bar":34} asjson(T92{T90: &T90{12, 34}, T91: &T91{56, 78}}) //=> json: {"Foo":12,"Bar":34,"Baz":56,"Qux":78} t92FromJSON(`{"Foo":12}`) //=> val: {(*main.T90=&{12 0}) (*main.T91=<nil>)} t92FromJSON(`{"Foo":12,"Baz":34}`) //=> val: {(*main.T90=&{12 0}) (*main.T91=&{34 0})} }
- 埋め込まれているポインタが nil だと、そのフィールドは Marshal されない。
- Unmarshal 時には、必要に応じてオブジェクトが作られる。
ということになっているようだ。
埋め込まれているオブジェクトが nil かどうかで JSON のキーの数が変わるのはちょっと意外だった。最後に
まだ書いてないこともちょっとあるんだけど、今日のところはこれぐらいで。