GolangでDockerの公開ポート設定のように数値のペアをフラグとして受け取る
自分用スニペットです。 表題の「Dockerの公開ポート設定」とは以下のようなものです。
-p 8080:80
-p 192.168.1.100:8080:80
-p 8080:80/udp
Container networking | Docker Documentation
このように、-p 1:2
として「1-->2」という対応、また-p "1:2, 3,4"
として「1-->2」「3-->4」という対応を解釈するようなものを実装しました。
実装と解説
実装
- main.go
package main import ( "flag" "fmt" "strconv" "strings" "github.com/k0kubun/pp" ) func main() { p := flag.String("p", "1:2", "") flag.Parse() res, err := parsePair(*p) if err != nil { fmt.Println(err.Error()) return } pp.Println(res) } type Pair struct { In int64 Out int64 } func parsePair(expr string) ([]Pair, error) { res := []Pair{} for _, v1 := range strings.Split(expr, ",") { if v1 == "" { continue } n := strings.SplitN(v1, ":", 2) for i, v := range n { n[i] = strings.TrimSpace(v) } tmp := Pair{} if in, err := strconv.ParseInt(n[0], 10, 32); err == nil { tmp.In = in } else { return nil, err } if out, err := strconv.ParseInt(n[1], 10, 32); err == nil { tmp.Out = out } else { return nil, err } res = append(res, tmp) } return res, nil }
- 実行
$ go build -o out main.go $ ./out # デフォルト値 []main.Pair{ main.Pair{ In: 1, Out: 2, }, } $ ./out -p "0:0, 1:3, 120:5000" []main.Pair{ main.Pair{ In: 0, Out: 0, }, main.Pair{ In: 1, Out: 3, }, main.Pair{ In: 120, Out: 5000, }, }
解説
- 任意個の「2つの数値のペア」を受け取る。
- フラグとして渡される文字列は、「数値:数値」という形でペアを表している。ペアは「,」で区切られて複数渡されることもある。
- 構造体Pairは、In・OutというInt64型の要素を持つ。
- 関数parsePairは、Pairのスライスにフラグの解析結果を詰めて返す。
Dockerではどう実装しているか
ザっとコードリーディングしてみた。
-p
で受け取った値のゆくえ
-p
フラグの設定は以下でされている。
-p
で受け取った値は、関数parseの中で変数copts.publish
に格納される。- 変数
copts.publish
はcontainerOptions
型で、同じファイル内で定義されている。 containerOptions
型のpublish
要素は、opts.ListOpts
型。- 以下のように、変数
copts.publish
のGetAll()
メソッドが呼ばれ、変数publishOpts
が生成される。
publishOpts := copts.publish.GetAll()
- 以下のように、変数
publishOpts
が関数convertToStandardNotation
に渡され、変数convertedOpts
が生成されている。
convertedOpts, err = convertToStandardNotation(publishOpts)
- 関数
convertToStandardNotation
は以下で定義されている。
- 変数
convertedOpts,
は[]string型。 - 以下のように、変数
convertedOpts
は関数nat.ParsePortSpecs
に渡され、ports, portBindingsを生成される。
ports, portBindings, err = nat.ParsePortSpecs(convertedOpts)
- 変数
ports
はmap[nat.Port]struct{}
型で、後に*container.Config
型の変数config
の要素ExposedPorts
に格納される。 - 変数
portBindings
はmap[nat.Port][]nat.PortBinding
型の変数で、後に*container.HostConfig
型の変数hostConfig
の要素PortBindings
に格納される。
map[nat.Port][]nat.PortBinding
型
ここまでの処理で、結局はmap[nat.Port][]nat.PortBinding
型の変数で「ポートとポートの対応関係」を表しているらしいことがわかった。この型はつまり、「
『nat.Port
』と『nat.PortBinding
のスライス』の対応」を表している。
ここで、nat.Port
は以下のように定義されている。
// Port is a string containing port number and protocol in the format "80/tcp" type Port string
また、nat.PortBinding
は以下のように定義されている
// PortBinding represents a binding between a Host IP address and a Host Port type PortBinding struct { // HostIP is the host IP Address HostIP string `json:"HostIp"` // HostPort is the host port number HostPort string }
つまり、map[nat.Port][]nat.PortBinding
型は「『コンテナ側のポート』と『「ホストのIPとポート」のスライス』の対応」を表している。
ここで、どうしてnat.PortBinding
をスライスにしているのかと不思議に思いました。試しにやってみたところ、「コンテナのポート」に対して複数の「ホストのポート」を対応付けられるんですね。docker run -p 8080:8080 -p 8081:8080 -it alpine
とすれば、コンテナ内の8080番・8081番両方の通信を、ホストの8080番にバインドできました。
nat.Port
とnat.PortBinding
の生成
前述の処理の流れの中で、以下のようにnat.Port
型・nat.PortBinding
型の変数をそれぞれ生成する処理がありました。
ports, portBindings, err = nat.ParsePortSpecs(convertedOpts)
natパッケージ内にはParsePortSpecs
という名前の関数が2つあり、それらは引数と戻り値が異なります。ここで呼ばれている関数はfunc ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding, error)
です。
こちらのParsePortSpecs
は以下のように定義されています。
// ParsePortSpecs receives port specs in the format of ip:public:private/proto and parses // these in to the internal types func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding, error) { var ( exposedPorts = make(map[Port]struct{}, len(ports)) bindings = make(map[Port][]PortBinding) ) for _, rawPort := range ports { portMappings, err := ParsePortSpec(rawPort) if err != nil { return nil, nil, err } for _, portMapping := range portMappings { port := portMapping.Port if _, exists := exposedPorts[port]; !exists { exposedPorts[port] = struct{}{} } bslice, exists := bindings[port] if !exists { bslice = []PortBinding{} } bindings[port] = append(bslice, portMapping.Binding) } } return exposedPorts, bindings, nil }
もう一方のParsePortSpec
を呼び出していることがわかります。もう一方のParsePortSpec
は以下のように定義されています。
// ParsePortSpec parses a port specification string into a slice of PortMappings func ParsePortSpec(rawPort string) ([]PortMapping, error) { var proto string rawIP, hostPort, containerPort := splitParts(rawPort) proto, containerPort = SplitProtoPort(containerPort) // Strip [] from IPV6 addresses ip, _, err := net.SplitHostPort(rawIP + ":") if err != nil { return nil, fmt.Errorf("Invalid ip address %v: %s", rawIP, err) } if ip != "" && net.ParseIP(ip) == nil { return nil, fmt.Errorf("Invalid ip address: %s", ip) } if containerPort == "" { return nil, fmt.Errorf("No port specified: %s<empty>", rawPort) } startPort, endPort, err := ParsePortRange(containerPort) if err != nil { return nil, fmt.Errorf("Invalid containerPort: %s", containerPort) } var startHostPort, endHostPort uint64 = 0, 0 if len(hostPort) > 0 { startHostPort, endHostPort, err = ParsePortRange(hostPort) if err != nil { return nil, fmt.Errorf("Invalid hostPort: %s", hostPort) } } if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) { // Allow host port range iff containerPort is not a range. // In this case, use the host port range as the dynamic // host port range to allocate into. if endPort != startPort { return nil, fmt.Errorf("Invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort) } } if !validateProto(strings.ToLower(proto)) { return nil, fmt.Errorf("Invalid proto: %s", proto) } ports := []PortMapping{} for i := uint64(0); i <= (endPort - startPort); i++ { containerPort = strconv.FormatUint(startPort+i, 10) if len(hostPort) > 0 { hostPort = strconv.FormatUint(startHostPort+i, 10) } // Set hostPort to a range only if there is a single container port // and a dynamic host port. if startPort == endPort && startHostPort != endHostPort { hostPort = fmt.Sprintf("%s-%s", hostPort, strconv.FormatUint(endHostPort, 10)) } port, err := NewPort(strings.ToLower(proto), containerPort) if err != nil { return nil, err } binding := PortBinding{ HostIP: ip, HostPort: hostPort, } ports = append(ports, PortMapping{Port: port, Binding: binding}) } return ports, nil }
こちらのParsePortSpec
は[]PortMapping
型の変数を返します。PortMapping
型は以下のように定義されています。
// PortMapping is a data object mapping a Port to a PortBinding type PortMapping struct { Port Port Binding PortBinding }
まさにnat.Port
とnat.PortBinding
の対応を表す型ですね。
ここで、ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding, error)
の処理をまとめると、以下のようになります。
ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding, error)
にポートバインディングの設定を表すstring型のスライスが渡される。- 引数のstring型のスライスの各要素(変数
rawPort
)について、以下の処理をする。func ParsePortSpec(rawPort string) ([]PortMapping, error)
にrawPort
を渡す。rawPort
を解析し、PortMapping
のスライスを返す。
- 受け取った
PortMapping
型のデータをすべて分解して、map[Port]struct{}
・map[Port][]PortBinding
の形にまとめなおす。
おわりに
おまけでDockerの実装を追ってみましたが、思ったより分量があって遭難しかけました。