TypeScript の definite assignment assertion operator の話

TypeScript の class では、プロパティは初期化指定子かコンストラクタで初期化していないとエラーになります。そのため、以下のように記述する必要があります。

class Hoge {
  value1: string = 'a';
  value2: number;
  // value3: boolean; // ERROR: Property 'value3' has no initializer and is not definitely assigned in the constructor.

  constructor() {
    this.value2 = 1;
  }
}

しかし、たとえば Vue.js の Component class の props のように Vue.js 側で初期化するプロパティについては少々困ります。この確実に初期化されるが初期化指定子やコンストラクタで初期化はしないという状況の時に、明確な割り当てアサーション演算子(definite assignment assertion operator) ! 1を使用することでこの問題を回避できます。

@Component
export default class HogeComponent extends Vue {
  @Prop({ type: String, required: true }) value1!: string;
  @Prop({ type: Number, required: true }) value2!: number;
  @Prop({ type: Boolean, required: true }) value3!: boolean;
}

bashで複数の子プロセスを並行で実行して終了を待つ方法(Ctrl+cでの終了に対応)

bashで複数の子プロセスを並行で実行して終了を待つには & でバックグラウンド処理しつつ wait で待つことで実現できるのですが、この方法だと Ctrl+c したときに子プロセスが残ってしまいます。

(
  command1 &
  command2 &
  wait
)

ここで、jobs -p を使えば、子プロセスのPID一覧が取得できるので、trap を使って SIGINT をトラップしたときに、jobs -p で得られたPIDを kill すれば子プロセスも kill することができます。

(
  trap 'kill $(jobs -p)' EXIT;
  command1 &
  command2 &
  wait
)

しかし、jobs -pzsh だとうまく動かないので、この場合は bash -c を使って実行する必要があります。

bash -c "
  trap 'kill \$(jobs -p)' EXIT;
  command1 &
  command2 &
  wait
"

参考

GitHub Actions でローカルの composite を使う場合

GitHub Actions でローカルにある composite を使う場合は、あらかじめチェックアウトしていないと、ファイルが見つからないとエラーが出てしまうので注意する。

NG:

name: hoge
on:
  push:
jobs:
  hoge:
    name: Hoge
    runs-on: ubuntu-latest
    steps:
      - uses: ./.github/composite/fuga

OK:

name: hoge
on:
  push:
jobs:
  hoge:
    name: Hoge
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - uses: ./.github/composite/fuga

CIでGCPのServiceAccountを使って認証する方法

GCPでServiceAccountで認証するときはGOOGLE_APPLICATION_CREDENTIALS環境変数に鍵ファイルのパスを設定するが、CI実行時にはどうするんだろうと考えた結果、鍵ファイルをbase64エンコードしたものをCIの環境変数に設定して、CI実行時にデコードしてGOOGLE_APPLICATION_CREDENTIALS環境変数に設定されているパスに出力とかしたけど、もっといい方法ないのかな。

# 1. 鍵ファイルをbase64でエンコード
cat /path/to/key.json | base64

# 2. 以下をCIの環境変数に設定
#   GOOGLE_APPLICATION_CREDENTIALS_CONTENT 1.でエンコードした文字列
#   GOOGLE_APPLICATION_CREDENTIALS /path/to/key.json

# 3. base64でエンコードされた鍵ファイルをデコードして、`GOOGLE_APPLICATION_CREDENTIALS` のパスに出力
echo $GOOGLE_APPLICATION_CREDENTIALS_CONTENT | base64 -d > $GOOGLE_APPLICATION_CREDENTIALS

Docker で Compose V2 などをキャッチアップした

docker-compose up ではなく docker compose up を使うように推奨されていることは知っていたが、ちゃんとキャッチアップしてなかったのでした。また、その過程で Compose V2 以外にも新たに知ったことがあったのでここに記す。

docker-compose との違い

大雑把には以下が異なる。

  • docker-compose は docker とは別のスタンドアローンバイナリだったが、docker compose は docker cli plugin として提供される
  • docker compose は Compose Spec に準拠するようになった

Compose Spec

Composeファイルの標準仕様で、プラットフォームやクラウドに依存しない仕様として策定されている模様。

Compose Spec V2 では従来から主に以下が変更/追加されている。

  1. Composeファイルは docker-compose.yml から compose.yaml になった
  2. トップレベルの version は非推奨になった
  3. 設定ファイルのマウントを行う configs が追加された
  4. 秘匿情報のマウントを行う secrets が追加された
  5. 設定の拡張を行う extends が追加された

1. 仕様ファイルは docker-compose.yml から compose.yaml になった

そのまま。後方互換性のため docker-compose.yml も使えるが、今後標準は compose.yaml となる。なお、Compose Spec では compose.yml も使えるが、拡張子は yaml のほうが好ましいとのこと。

2. トップレベルの version は非推奨になった

今までは、トップレベルの version でComposeファイルを検証していたが、これが今後非推奨となるので記述しなくなる模様。

3. 設定ファイルのマウントを行う configs が追加された

今までは、volumes で行っていたが、今後は設定ファイルの類は configs を使用する。docker compose を使っている限りだと volumes と特に大差はないようだが、プラットフォームごとに実装方法を変えられるように独立した模様。

compose.yaml:

services:
  db:
    image: mysql:8
    configs:
      - source: mysql-config
        target: /etc/mysql/conf.d/my.cnf
    ...

configs:
  mysql-config:
    file: ./my.cnf

4. 秘匿情報のマウントを行う secrets が追加された

秘匿情報のマウントが行える secrets が追加された。configs とは実装が異なる可能性があるので、独立している模様。

compose.yaml:

services:
  app:
    image: golang:1.20.1
    secrets:
      - source: app-secrets
        target: /app/.env
    ...

secrets:
  app-secrets:
    file: ./app-secrets.env

5. 設定の拡張を行う extends が追加された

他のserviceの設定をマージ/拡張できるようになる extends が追加された。同一ファイルのみならず、別ファイルも参照できる模様。

compose.common.yaml:

services:
  common:
    init: true
    environment:
      TZ: utc

compose.yaml:

services:
  app:
    image: golang:1.20.1
    extends:
      file: compose.common.yaml
      service: common
    ...

  db:
    image: mysql:8
    extends:
      file: compose.common.yaml
      service: common
    ...

Compose V2 以外のアレコレ

mysqladmin ping を使って、DBコンテナのMySQLに接続できる状態になってから、アプリケーションコンテナを起動する

depends_on を使えば、コンテナ間の依存関係を定義できるので、DBコンテナが起動してからアプリケーションコンテナを起動するのができたし、今までもこれを使ってた。

compose.yaml:

services:
  app:
    image: golang:1.20.1
    depends_on:
      - db
    ...

  db:
    image: mysql:8
    ...

しかし、これだとdbコンテナが起動したあとに対象コンテナを起動するという指定にすぎないので、dbコンテナが起動しているがMySQLにはまだ接続できない状態には対処できない。その場合はdbコンテナの healthcheckmysqladmin ping で疎通確認し、依存先コンテナの depends_oncondition: service_healthy を指定すればできる。

compose.yaml:

services:
  app:
    image: golang:1.20.1
    depends_on:
      db:
        condition: service_healthy
    ...

  db:
    image: mysql:8
    healthcheck:
      test: ["CMD", "mysqladmin" ,"ping", "-h", "0.0.0.0"]
      interval: 1s
      timeout: 1s
      retries: 30
      start_period: 1s
    ...

docker compose で作成されるコンテナ名などのプレフィックスを変更する

docker compose で作成されるコンテナ名などのプレフィックスは、Composeファイルのトップレベルの name によって指定できる。また、COMPOSE_PROJECT_NAME環境変数でこの指定はオーバーライドできる。

compose.yaml:

name: docker-compose-v2
services:
  ...
$ docker compose up
[+] Running 4/3
 ⠿ Network docker-compose-v2_default  Created                                                                                                                                     0.0s
 ⠿ Volume "docker-compose-v2_db"      Created                                                                                                                                     0.0s
 ⠿ Container docker-compose-v2-db-1   Created                                                                                                                                     0.1s
 ⠿ Container docker-compose-v2-app-1  Created                                                                                                                                     0.1s
...
$ COMPOSE_PROJECT_NAME=other-name docker compose up
[+] Running 4/3
 ⠿ Network other-name_default  Created                                                                                                                                            0.0s
 ⠿ Volume "other-name_db"      Created                                                                                                                                            0.0s
 ⠿ Container other-name-db-1   Created                                                                                                                                            0.1s
 ⠿ Container other-name-app-1  Created                                                                                                                                            0.1s
...

なお、Composeファイルでは環境変数で設定値の指定ができるが、nameでは使用できない模様。

compose.yaml:

name: ${BASE_NAME}-docker-compose-v2
services:
  ...
$ BASE_NAME=hoge docker compose up
[+] Running 4/4
 ⠿ Network base_name-docker-compose-v2_default  Created                                                                                                                           0.1s
 ⠿ Volume "base_name-docker-compose-v2_db"      Created                                                                                                                           0.0s
 ⠿ Container base_name-docker-compose-v2-db-1   Created                                                                                                                           0.1s
 ⠿ Container base_name-docker-compose-v2-app-1  Created                                                                                                                           0.6s
...

環境

  • OS: macOS Ventura 13.2
  • Docker Desktop: 4.16.2
  • Docker Engine: 20.10.22
  • Docker Compose: 2.15.1

参考

Compose V2

その他

サンプルコード

この記事で使用したサンプルコードは以下にある。

github.com/mrk21/sandbox/docker-compose-v2

gomockのMatcherで構造体を再帰的に比較する

gomockの引数のMatcherでgomock.Eq()を使うときに、構造体がポインタ型だったりするとうまく比較できない。

type Hoge struct {
    ID   string
}

m.EXPECT().Hoge(gomock.Eq(&Hoge{ID: "1"})).Return(nil).AnyTimes() // マッチしない
m.Hoge(&Hoge{ID: "1"})

そのため、go-cmp を使って両者を比較するカスタムMatcherを実装する。

func GomockDeepEq(expected interface{}) gomock.Matcher {
    return &gomockDeepEqMatcher{
        expected: expected,
    }
}

type gomockDeepEqMatcher struct {
    expected interface{}
    diff     string
}

func (m *gomockDeepEqMatcher) Matches(x interface{}) bool {
    m.diff = cmp.Diff(x, m.expected, CmpTransformJSON())
    return m.diff == ""
}

func (m *gomockDeepEqMatcher) String() string {
    return m.diff
}

// interface check
var _ gomock.Matcher = (*gomockDeepEqMatcher)(nil)
type Hoge struct {
    ID   string
}

m.EXPECT().Hoge(GomockDeepEq(&Hoge{ID: "1"})).Return(nil).AnyTimes() // マッチする
m.Hoge(&Hoge{ID: "1"})

ここで、 go-cmp の cmp.Diff() でJSON文字列を比較できるようにする - mrk21::blog {} で述べたように、JSON文字列を比較できるようにしておくとさらに便利になる。

type Hoge struct {
    ID   string
    JSON json.RawMessage
}

m.EXPECT().Hoge(GomockDeepEq(&Hoge{ID: "1", JSON: json.RawMessage(`{"a":1, "b":2}`)})).Return(nil).AnyTimes() // マッチする
m.Hoge(&Hoge{ID: "1", JSON: json.RawMessage(`{"b":2, "a":1}`)})

環境

  • Go: 1.9.3
  • gomock: 1.4.4
  • go-cmp: 0.5.9

go-cmp の cmp.Diff() でJSON文字列を比較できるようにする

go-cmp を使うと2つの構造体のDiffを取ることができるが、プロパティにJSON文字列([]bytejson.RawMessage など)があると、内容は同じでもインデント等が異なるとdiffが出てしまい、うまく比較できない。

type Hoge struct {
    ID   string
    JSON json.RawMessage
}

func main() {
a := Hoge{ID: "1", JSON: json.RawMessage(`{"a":1, "b":2}`)}
b := Hoge{ID: "1", JSON: json.RawMessage(`{"b":2, "a":1}`)}
diff := cmp.Diff(a, b)
fmt.Println(diff) // diff がでる

この場合は、cmp.Transformer()JSON文字列をjson.Unmarshal()して構造体に変換するようにして、cmp.Diff()のオプションに渡すことで比較できるようになる1

func CmpTransformJSON() cmp.Option {
    return cmp.FilterValues(
        func(x, y []byte) bool {
            return json.Valid(x) && json.Valid(y)
        },
        cmp.Transformer("ParseJSON", func(in []byte) (out interface{}) {
            if err := json.Unmarshal(in, &out); err != nil {
                panic(err)
            }
            return out
        }),
    )
}
type Hoge struct {
    ID   string
    JSON json.RawMessage
}

a := Hoge{ID: "1", JSON: json.RawMessage(`{"a":1, "b":2}`)}
b := Hoge{ID: "1", JSON: json.RawMessage(`{"b":2, "a":1}`)}
diff := cmp.Diff(a, b, CmpTransformJSON())
fmt.Println(diff) // diff がでない

まとめると以下のようになる。

package main

import (
    "encoding/json"
    "fmt"

    "github.com/google/go-cmp/cmp"
)

type Hoge struct {
    ID   string
    JSON json.RawMessage
}

func main() {
    a := Hoge{ID: "1", JSON: json.RawMessage(`{"a":1, "b":2}`)}
    b := Hoge{ID: "1", JSON: json.RawMessage(`{"b":2, "a":1}`)}
    {
        diff := cmp.Diff(a, b)
        fmt.Println("diff:", diff) // diff がでる
    }
    {
        diff := cmp.Diff(a, b, CmpTransformJSON())
        fmt.Println("diff:", diff) // diff がでない
    }
}

func CmpTransformJSON() cmp.Option {
    return cmp.FilterValues(
        func(x, y []byte) bool {
            return json.Valid(x) && json.Valid(y)
        },
        cmp.Transformer("ParseJSON", func(in []byte) (out interface{}) {
            if err := json.Unmarshal(in, &out); err != nil {
                panic(err)
            }
            return out
        }),
    )
}

Go Playground - The Go Programming Language

環境

  • Go: 1.9.3
  • go-cmp: 0.5.9