深入理解Moby Buildkit系列 #16 - 神奇llb.State之代码实现

1,242 阅读3分钟

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

为了更好的理解作者的意图,袁小白决定把看过的代码,结合状态图再梳理一遍: State concept.jpg

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: Screen Shot 2021-11-17 at 7.18.43 PM.png 二是新建了Source: Screen Shot 2021-11-17 at 7.20.17 PM.png 这里出现了SourceOp,其中包含了它的output,就像我们最开始的状态图所示: State concept(副本).jpg 三是用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的函数,可灵活处理不同的情况

回到我们的状态图来看,也就来到了下面的状态: State concept(副本) (1).jpg

.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: State concept.jpg

这样我们就走完了这个简单用例的全流程。 理解上好像是清楚了一点,那接着往下看:

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又是什么?

下一篇:深入理解Moby Buildkit系列 #17 - Definition