这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战
为了更好的理解作者的意图,袁小白决定把看过的代码,结合状态图再梳理一遍:
llb.Image("foo")
因为我们只是传入了一个image ref,并没有其它的任何参数,代码可以简化为:
func Image(ref string, opts ...ImageOption) State {
r, err := reference.ParseNormalizedNamed(ref)
...
src := NewSource("docker-image://"+ref, attrs, info.Constraints)
...
return NewState(src.Output())
从代码中,可以看出主流程做了三件事,一是将传入的ref进行解析,这里的reference.ParseNormalizedNamed方法,来自于docker:
二是新建了Source:
这里出现了SourceOp,其中包含了它的output,就像我们最开始的状态图所示:
三是用src.Output() - s.output创建了新状态并返回,也就是说这里才是State被创建的地方。
func NewState(o Output) State {
s := State{
out: o,
}.Dir("/")
s = s.ensurePlatform()
return s
}
NewState代码很简单,就是将output包成了一个State,但值得回味的是这里的Dir("/"),代码如下:
func (s State) Dir(str string) State {
return Dir(str)(s)
}
func Dir(str string) StateOption {
return dirf(str, false)
}
func dirf(value string, replace bool, v ...interface{}) StateOption {
if replace {
value = fmt.Sprintf(value, v...)
}
return func(s State) State {
return s.withValue(keyDir, func(ctx context.Context, c *Constraints) (interface{}, error) {
if !path.IsAbs(value) {
prev, err := getDir(s)(ctx, c)
if err != nil {
return nil, err
}
if prev == "" {
prev = "/"
}
value = path.Join(prev, value)
}
return value, nil
})
}
}
可以看出,进行了一连串的调用!
如果从结果来看,最后dirf函数返回的是一个func,这个方法入参是一个State,返回一个新的State,也就是传入了我们将我们SourceOp.output包成的第一个state传入,经过s.withValue处理,返回另一个新的State,为什么要这样设计呢?
从函数名dirf-dir format可以看出,这里需要处理的场景是动态目录,也就是Dir("/%s")等这样的format。
再看s.withValue:
func (s State) withValue(k interface{}, v func(context.Context, *Constraints) (interface{}, error)) State {
return State{
out: s.Output(),
prev: &s, // doesn't need to be original pointer
key: k,
value: v,
}
}
创建了一个新的State,并将前一个的output设置为自己的output,将前一个State设置为自己的pref结点。 而value: v中的v是一个函数:
func(ctx context.Context, c *Constraints) (interface{}, error) {
if !path.IsAbs(value) {
prev, err := getDir(s)(ctx, c)
if err != nil {
return nil, err
}
if prev == "" {
prev = "/"
}
value = path.Join(prev, value)
}
return value, nil
}
这里可以看出这个方法会接收一些Constraints,也就是说在最后执行的时候,还会有机会根据不同的限制得出不同的结果,做到动态可配置。
而咱们的情况比较简单Dir("/"),是一个绝对路径,根据代码可看出,直接返回return value, nil。
总的来看,State有以下几个特点:
- State是一个单向链表
- State并不是用来一一对应Op的
- 每个State都有output
- 每个State都是一个结点,有自己的key, value,并且value是可以接收Constraints的函数,可灵活处理不同的情况
回到我们的状态图来看,也就来到了下面的状态:
.Run(llb.shlex("bar"))
func (s State) Run(ro ...RunOption) ExecState {
ei := &ExecInfo{State: s}
...
exec := NewExecOp(ei.State, ei.ProxyEnv, ei.ReadonlyRootFS, ei.Constraints)
...
return ExecState{
State: s.WithOutput(exec.Output()),
exec: exec,
}
}
func NewExecOp(base State, proxyEnv *ProxyEnv, readOnly bool, c Constraints) *ExecOp {
e := &ExecOp{base: base, constraints: c, proxyEnv: proxyEnv}
root := base.Output()
...
e.mounts = append(e.mounts, rootMount)
if readOnly {
e.root = root
} else {
o := &output{vertex: e, getIndex: e.getMountIndexFn(rootMount)}
...
e.root = o
}
rootMount.output = e.root
return e
}
func (s State) WithOutput(o Output) State {
prev := s
s = State{
out: o,
prev: &prev,
}
s = s.ensurePlatform()
return s
}
从Run函数可以看出,先创建出了ExecInfo,这里将会收集创建NewExecOp所需的参数信息,其中ReadonlyRootFS将永定ExecOp的output,也就是e.root的值,如果readOnly为true,就和上一个State一样,但这里我们走的流程是else,也就新创建了一个output,最后返回的是ExecState:
这样我们就走完了这个简单用例的全流程。 理解上好像是清楚了一点,那接着往下看:
func TestDefaultPlatform(t *testing.T) {
t.Parallel()
s := llb.Image("foo").Run(llb.Shlex("bar"))
def, err := s.Marshal(context.TODO())
require.NoError(t, err)
e, err := llbsolver.Load(def.ToPB())
require.NoError(t, err)
require.Equal(t, depth(e), 2)
// needs extra normalize for default spec
// https://github.com/moby/buildkit/pull/2427#issuecomment-952301867
expected := platforms.Normalize(platforms.DefaultSpec())
require.Equal(t, expected, platform(e))
require.Equal(t, []string{"bar"}, args(e))
e = parent(e, 0)
require.Equal(t, expected, platform(e))
require.Equal(t, "docker-image://docker.io/library/foo:latest", id(e))
}
其中def, err := s.Marshal(context.TODO()),又带来了另一个问题, def - Definition又是什么?