KDOC 20: docker build のログ出力を読む

[python 2/4]RUN pip3 install
[build  2/13] COPY . .

みたいな表示はどうやっているのかを調べる。

ミニマムに実装しようとしたが、あまりよくわからなかった…。contextの知識が足りなそう。

memo

classDiagram
class oneOffProgress {
  (ctx context.Context, id string) func(err error) error
  ログ文字列送信
}
class NewFromContext {
  (ctx context.Context, opts ...WriterOption) (Writer, bool, context.Context)
  FromContext関数にctxを渡して実行
}
class FromContext {
  (ctx context.Context, opts ...WriterOption) WriterFactory
  writerを返す関数生成
}
class newWriter {
  (pw *progressWriter) *progressWriter
  writerを上書き
}
class append {
  (pr *progressReader) append(pw *progressWriter)
  追加
}

oneOffProgress "1" --> "1" NewFromContext
NewFromContext "1" --> "1" FromContext
FromContext    "1" --> "1" newWriter
newWriter      "1" --> "1" append

20230219200923-GI4NyKiVWY.png

ログ送信

よく見るログ文から、どこで実行してるかを調べる。

layersDone := oneOffProgress(ctx, “exporting layers”)

vendorにもある。

layersDone := progress.OneOff(ctx, “exporting layers”)

どっちも変更して、docker本体をビルドして動かしてみたが、反映されない。読んで調べる。

oneOffProgressを調べる。

func oneOffProgress(ctx context.Context, id string) func(err error) error { pw, _, _ := progress.NewFromContext(ctx) now := time.Now() st := progress.Status{ Started: &now, } _ = pw.Write(id, st) return func(err error) error { // TODO: set error on status now := time.Now() st.Completed = &now _ = pw.Write(id, st) _ = pw.Close() return err } }

progress.NewFromContext(ctx)を調べる。

/ NewFromContext creates a new Writer based on a Writer previously stored / in the Context and returns a new Context with the new Writer stored. It is // the callers responsibility to Close the returned Writer to avoid resource leaks. func NewFromContext(ctx context.Context, opts …WriterOption) (Writer, bool, context.Context) { return FromContext(ctx, opts…)(ctx) }

/ FromContext returns a WriterFactory to generate new progress writers based / on a Writer previously stored in the Context. func FromContext(ctx context.Context, opts …WriterOption) WriterFactory { v := ctx.Value(contextKey) return func(ctx context.Context) (Writer, bool, context.Context) { pw, ok := v.(*progressWriter) if !ok { if pw, ok := v.(*MultiWriter); ok { return pw, true, ctx } return &noOpWriter{}, false, ctx } pw = newWriter(pw) for _, o := range opts { o(pw) } ctx = context.WithValue(ctx, contextKey, pw) return pw, true, ctx } }

  • contextを使っていそう
    • FromContext
      • 古いcontextに、新しいcontextを書き込んで返す
      • contextから、key “buildkit/util/progress” を読み出す
      • progressWriterであることを型チェック
      • newWriterを実行
      • 生成したwriterを返す関数を返す
      • ↑をctxを渡して評価することで、writerが取り出せる
      • 取り出したwriterに、id, statusを書き込む

type progressWriter struct { done bool reader *progressReader meta map[string]interface{} }

  • 実行結果を表示させているのをprogressという。まあわかる

func newWriter(pw *progressWriter) *progressWriter { meta := make(map[string]interface{}) for k, v := range pw.meta { meta[k] = v } pw = &progressWriter{ reader: pw.reader, meta: meta, } pw.reader.append(pw) return pw }

  • progressWriterを取ってprogressWriterを返す
    • ログの情報を付け加えてるのだろう
  • metaはstring keyのmap。引数のprogressWriterから値をセットして、metaを複製
  • 前のmetaの値を引き継いだ構造体progressWriterを作成
  • pw.readerに作ったwriterをappend
    • appendはmutex lockがされていて、同時編集されないようになっている
  • どうしてreaderにappendするのか
    • このappendは、追加された関数
    • readerの中に、writersがあって、そこに追加する

type progressReader struct { ctx context.Context cond *sync.Cond mu sync.Mutex writers map[*progressWriter]struct{} dirty map[string]*Progress }

func (pr *progressReader) append(pw *progressWriter) { pr.mu.Lock() defer pr.mu.Unlock()

select { case <-pr.ctx.Done(): return default: pr.writers[pw] = struct{}{} } }

  • appendは、progressReaderの中のwritersに追加するという関数
  • 試してみたが、いまいち使い方がわからない
    • 単独実行で試せない
    • 初期化はどうするんだ
- reader
  - writers
    - writer
      - reader
    - writer
      - reader
  • そもそもcontextは、goルーチンに渡すことで、異なるgoルーチンと値をシェアできるようになる、というもの

送信したログをどこで表示してるか

ログの送信とprintは分離されている。どこでprintしているのだろうか。

contextの使い方

WithValue(parent Context, key, val interface{}) Context
Value(key interface{}) interface{}
  • WithValueで値をセットし、Valueで値を取り出す

progressWriter

  • ProgressWriterというのがあるな。関係ありそう

type ProgressWriter struct { Output io.Writer StdoutFormatter io.Writer StderrFormatter io.Writer AuxFormatter *streamformatter.AuxFormatter ProgressReaderFunc func(io.ReadCloser) io.ReadCloser }

builder

  • 気づいた
  • stdoutとoutputが別になってる
    • これは、コードを書いてる途中で直面したことだ。stdoutだと一時的にためておいてあとで整形して出す、ということができないために、一時的な保存として別のbufferを使った。このコードでもそうなのかは知らない

type Builder struct { options *types.ImageBuildOptions

Stdout io.Writer Stderr io.Writer Aux *streamformatter.AuxFormatter Output io.Writer

docker builder.Backend clientCtx context.Context

idMapping idtools.IdentityMapping disableCommit bool imageSources *imageSources pathCache pathCache containerManager *containerManager imageProber ImageProber platform *specs.Platform }

openAPI定義

docker engineのAPI定義を発見した。

処理はどこにあるか

  • Dockerはエンジンと、クライアントに分かれている。Docker EngineとDocker CLI
  • CLIはただのラッパーにすぎない。本質的な処理はエンジンのほうがやっている、はず

APIサーバ

  • APIサーバはgorillaであることがわかる。

“github.com/gorilla/mux”

いっぽうでGRPCを使っていそうな箇所もある。