20191004のGoに関する記事は4件です。

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証明書なので IsCABasicConstraintsValidtrue に設定

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. コード

コードはこちら
https://github.com/tardevnull/gopkicookbook2

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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を指定。
ホットリロード環境を作りたいのでfreshgo getしています。

myapp/docker/go/Dockerfile
FROM golang:1.13.1-alpine

ENV GO111MODULE=on

RUN apk update && \
    apk add emacs curl git && \
    go get github.com/pilu/fresh

Vue.js

nodeのバージョンは、現時点(2019年10月)で最新の8.16
vue-cli3を利用するので@vue/clinpm installしています。

mysqpp/docker/vue/Dockerfile
FROM node:8.16-alpine

RUN apk update && \
    npm install -g npm && \
    npm install -g @vue/cli

docker-compose.ymlの中身(初期)

nginxの設定は後で足します。
まずは、GoとVue.jsのコンテナを作るところから始めていきたいので。

docker-compose.yml
 version: '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

ちなみに、govue共に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.go
package 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 tmp

go.modができましたね。
 
Ctr + dとかでコンテナの外に出て、go.mogファイルの中身を確認すると以下のうようになっています。

go.mod
module app

go 1.13

 
では、Echoを導入していきます。
コンテナの外からで良いので、以下のようにmain.goを編集します。
なんてことはない、Echoのクイックスタート(ほぼ)そのままです。

main.go
package 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にアクセスすると、以下のような画面が表示されます。
スクリーンショット 2019-10-03 12.21.43.png

見事に世界に挨拶ができましたね。

ちなみに、go.modはこんな感じになっています。

go.mod
module 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の設定に入っていきます。
お好みで設定すれば良いと思いますが、今回は以下のようにします。

スクリーンショット 2019-10-03 12.41.11.png
スクリーンショット 2019-10-03 12.45.38.png

Enterを無心で押していくと、以下のような感じで設定が始まります。
スクリーンショット 2019-10-03 12.45.54.png

サクセスすると、以下のような表示になるかと思います。
スクリーンショット 2019-10-03 18.10.01.png
 
ここまできたら、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です。
スクリーンショット 2019-10-03 18.17.43.png
 
上記画面が出た状態で、localhost:8080にアクセスすると、以下のような画面が表示されます。
スクリーンショット 2019-10-03 18.20.06.png
Vue.js Appの世界に無事、迎え入れられてもらうことができました。

nginxのコンテナを作る

一旦、docker-compose downで、コンテナを落としておきます。
そして、docker-compose.ymlに追記&nginx.confを作ります。

docker-compose.yml
version: '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.conf
nginx.conf
worker_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 でアクセスしてみましょう。
スクリーンショット 2019-10-03 18.20.06.png
この画面になったらOKです。

Vue側からGo側へリクエストを送り、値を得る。

axiosを使って、GoのAPIを叩き値を取ってくることを想定して、もう少しファイルを編集していきます。

axiosを使えるようにする。

axiosを使えるようにするため、package.json(assetsディレクトリ内にあるはずです)を編集します。
dependenciesの部分にaxiosを追加します(バージョンは、2019年10月最新のものにしてます)。
併せて、console.log()が入っているときにWaringが出ないようにするため、esLintConfgrulesに、
"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.js
import 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を叩く。

今回は超簡易的に、

  1. ブラウザで http://localhost にアクセス。
  2. APIが叩かれる(GET)。
  3. Go側からレスポンスを返す。
  4. 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 にアクセスしてみましょう。
(もしくは、リロードしてみましょう。)

スクリーンショット 2019-10-04 12.51.09.png

画像のように、レスポンスが返ってきているのが確認できたらOKです。
あとは、返ってきた値を上手く調理するなどして、画面にレンダリングするとかしてみてみるといいんじゃないでしょうか。
(その辺は、余裕あったら続きを書こうと思います。記録としても残しておく意味で。)

以上。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

client-goを利用してKubernetesのnamespaceを切り替える対話式CLIツールを作成した。

今回作ったツール - kns

kns.gif

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-configurationout-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のオブジェクトは共通してtypemetaobjectmetaという2つの属性を持っていますので、一つ一つ調べる必要はありません。

・Typemeta
https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#TypeMeta

・Objectmeta
https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#ObjectMeta

Kubernetesのマニフェストでお馴染みの属性たちです。今回は名前が欲しかったので、ObjectMeta.Nameで取得しました。

podやserviceなど他のオブジェクトの操作方法はgodocをご参照ください。
(以下はcore/v1のリンクです。)
https://godoc.org/k8s.io/client-go/kubernetes/typed/core/v1

Golangで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を作成したいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

私の知らない 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 inttype 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 のキーの数が変わるのはちょっと意外だった。

最後に

まだ書いてないこともちょっとあるんだけど、今日のところはこれぐらいで。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む