深入理解Moby Buildkit系列 #31 - ExecOp

510 阅读3分钟

这是我参与11月更文挑战的第31天,活动详情查看:2021最后一次更文挑战

SourceOp执行完毕后,会触发ExecOp同样的操作,一个是CacheMap,另一个则是Exec。

ExecOp CacheMap

func (e *execOp) CacheMap(ctx context.Context, g session.Group, index int) (*solver.CacheMap, bool, error) {
   op := cloneExecOp(e.op)
   ...
   dt, err := json.Marshal(struct {
      Type    string
      Exec    *pb.ExecOp
      OS      string
      Arch    string
      Variant string `json:",omitempty"`
   }{
      Type:    execCacheType,
      Exec:    &op,
      OS:      p.OS,
      Arch:    p.Architecture,
      Variant: p.Variant,
   })
   if err != nil {
      return nil, false, err
   }

   cm := &solver.CacheMap{
      Digest: digest.FromBytes(dt),
      Deps: make([]struct {
         Selector          digest.Digest
         ComputeDigestFunc solver.ResultBasedCacheFunc
         PreprocessFunc    solver.PreprocessFunc
      }, e.numInputs),
   }
   deps, err := e.getMountDeps()
   ...
   return cm, true, nil
}

相对SourceOp的CacheMap,ExecOp的相对简单,需要我们关注的是这里多了一个e.getMountDeps():

func (e *execOp) getMountDeps() ([]dep, error) {
   deps := make([]dep, e.numInputs)
   for _, m := range e.op.Mounts {
      if m.Input == pb.Empty {
         continue
      }
      if int(m.Input) >= len(deps) {
         return nil, errors.Errorf("invalid mountinput %v", m)
      }

      sel := m.Selector
      if sel != "" {
         sel = path.Join("/", sel)
         deps[m.Input].Selectors = append(deps[m.Input].Selectors, sel)
      }

      if (!m.Readonly || m.Dest == pb.RootMount) && m.Output != -1 { // exclude read-only rootfs && read-write mounts
         deps[m.Input].NoContentBasedHash = true
      }
   }
   return deps, nil
}

不一样的地方是e.op.Mounts,对于execOp来说,需要mounts,也就是挂载点,但这个mounts从哪儿来呢? 先不着争,我们带着这样的疑问继续看Exec。

ExecOp Exec

而这正是相对SourceOp较为复杂的地方:

func (e *execOp) Exec(ctx context.Context, g session.Group, inputs []solver.Result) (results []solver.Result, err error) {
   refs := make([]*worker.WorkerRef, len(inputs))
   for i, inp := range inputs {
      var ok bool
      refs[i], ok = inp.Sys().(*worker.WorkerRef)
      if !ok {
         return nil, errors.Errorf("invalid reference for exec %T", inp.Sys())
      }
   }
...
}

通过inputs,也就是solver传入的解析结果,这里我们可以理解为sourceOp的执行结果,将执行好的immutableRef给传过来,为下面的挂载做准备:

p, err := gateway.PrepareMounts(ctx, e.mm, e.cm, g, e.op.Meta.Cwd, e.op.Mounts, refs, func(m *pb.Mount, ref cache.ImmutableRef) (cache.MutableRef, error) {
   desc := fmt.Sprintf("mount %s from exec %s", m.Dest, strings.Join(e.op.Meta.Args, " "))
   return e.cm.New(ctx, ref, g, cache.WithDescription(desc))
})

PrepareMounts重点做的事情就是将ref和将要挂载的下标MountIndex对应上,因为很有可能要挂载多层ref,而在我们在例子中,我们只有一层,那就是SourceOp的immutableRef。 而这些操作都依赖于传入的e.op.Mounts,看来我们有必要再梳理一下op的来源了,以帮助进一步理解ExecOp到底是如何执行的。

op的生命周期

Operation - op lifecycle.jpg

  • 从将Dockerfile转换成llb.State开始,在State中,通过Output将Vertex进行关联,这里首次出现了Op,分别是SourceOp和ExecOp,他们主要是实现了Vertex接口。
  • 对于SourceOp最对应的Dockerfile信息就是id,也就是From Foo,让我们知道哪个是基础镜像。
  • 对于ExecOp较重要的信息就是root和mounts,root用于说明操作执行的根目录,而mounts则是具体说明要挂载的所有挂载点,以及源是什么。在我们的例子中,ExecOp的mount里,source代表的就是SourceOp.Output(),也就是源Vertex。这样我们就能得到Vertex的id,并通过cache manager查询到对应的cacheRecord了。
  • 接着,为了方便传输,我们将所有的state信息,转换成Definition进行存储,和传递。
  • solver接收到信息后,又将Definition转换成了Edge,并用Vertex来通用表示不同的Op,而其中的Index则关联了ops.pb中的Op
  • 接着为了标准化Op,并用pipe关联执行,又转换成了sharedOp
  • 在pipe执行的过程中,通过resolverFunc,通过worker,转换成最后我们现在正在用的SourceOp和execOp

以上就是op的来源和为什么现在长这个样子。 再结合上面CacheMap和Exec中出现的e.op.Mounts,这一下我们可以方便的理解了,ExecOp的mounts只有一个,就是在llb.State中的ExecOp的mounts,目标是根挂载点,源来自于SourceOp,这样就可以获取对应的ref了,这样我们就准备好了PrepareMounts的:

p.OutputRefs = append(p.OutputRefs, MountRef{
   MountIndex: i,
   Ref:        active,
})

这样我们就为Exec的运行,打好了文件系统的基础。

下一篇:深入理解Moby Buildkit系列 #32 - ExecOp的运行时