这是我参与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的生命周期
- 从将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的运行,打好了文件系统的基础。