Skip to content

Commit

Permalink
fix(staged-dockerfile): do not store non-target Dockerfile stages in …
Browse files Browse the repository at this point in the history
…the final-repo

* Do not store non-target Dockerfile stages in the final-repo, nor set custom tags on non-target stages.
* Changed staged-dockerfile builder digest calculation algorithm: do not use instruction number in digest input calculation, only instruction name.

refs #2215

Signed-off-by: Timofey Kirillov <timofey.kirillov@flant.com>
  • Loading branch information
distorhead committed Dec 12, 2022
1 parent c394c01 commit a0d7838
Show file tree
Hide file tree
Showing 25 changed files with 123 additions and 117 deletions.
3 changes: 3 additions & 0 deletions pkg/build/build_phase.go
Expand Up @@ -272,6 +272,9 @@ func (phase *BuildPhase) AfterImageStages(ctx context.Context, img *image.Image)
if img.IsArtifact {
return nil
}
if img.IsDockerfileImage && !img.IsDockerfileTargetStage {
return nil
}

if phase.Conveyor.StorageManager.GetFinalStagesStorage() != nil {
if err := phase.Conveyor.StorageManager.CopyStageIntoFinalStorage(ctx, img.GetLastNonEmptyStage(), phase.Conveyor.ContainerBackend, manager.CopyStageIntoFinalStorageOptions{ShouldBeBuiltMode: phase.ShouldBeBuiltMode}); err != nil {
Expand Down
49 changes: 28 additions & 21 deletions pkg/build/image/dockerfile.go
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"fmt"
"path/filepath"
"strings"

"github.com/docker/docker/builder/dockerignore"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
Expand Down Expand Up @@ -68,15 +69,17 @@ func mapDockerfileToImagesSets(ctx context.Context, cfg *dockerfile.Dockerfile,
WerfImageName string
Stage *dockerfile.DockerfileStage
Level int
IsTargetStage bool
}{
{WerfImageName: dockerfileImageConfig.Name, Stage: targetStage, Level: 0},
{WerfImageName: dockerfileImageConfig.Name, Stage: targetStage, Level: 0, IsTargetStage: true},
}

appendQueue := func(werfImageName string, stage *dockerfile.DockerfileStage, level int) {
queue = append(queue, struct {
WerfImageName string
Stage *dockerfile.DockerfileStage
Level int
IsTargetStage bool
}{WerfImageName: werfImageName, Stage: stage, Level: level})
}

Expand All @@ -103,6 +106,7 @@ func mapDockerfileToImagesSets(ctx context.Context, cfg *dockerfile.Dockerfile,
if baseStg := cfg.FindStage(stg.BaseName); baseStg != nil {
img, err = NewImage(ctx, item.WerfImageName, StageAsBaseImage, ImageOptions{
IsDockerfileImage: true,
IsDockerfileTargetStage: item.IsTargetStage,
DockerfileImageConfig: dockerfileImageConfig,
CommonImageOptions: opts,
BaseImageName: baseStg.WerfImageName(),
Expand All @@ -116,6 +120,7 @@ func mapDockerfileToImagesSets(ctx context.Context, cfg *dockerfile.Dockerfile,
} else {
img, err = NewImage(ctx, item.WerfImageName, ImageFromRegistryAsBaseImage, ImageOptions{
IsDockerfileImage: true,
IsDockerfileTargetStage: item.IsTargetStage,
DockerfileImageConfig: dockerfileImageConfig,
CommonImageOptions: opts,
BaseImageReference: stg.BaseName,
Expand All @@ -127,10 +132,11 @@ func mapDockerfileToImagesSets(ctx context.Context, cfg *dockerfile.Dockerfile,
}

for ind, instr := range stg.Instructions {
stageName := stage.StageName(fmt.Sprintf("%s%d", instr.GetInstructionData().Name(), ind))
stageLogName := fmt.Sprintf("%s%d", strings.ToUpper(instr.GetInstructionData().Name()), ind+1)
isFirstStage := (len(img.stages) == 0)
baseStageOptions := &stage.BaseStageOptions{
ImageName: img.Name,
LogName: stageLogName,
ImageTmpDir: img.TmpDir,
ContainerWerfDir: img.ContainerWerfDir,
ProjectName: opts.ProjectName,
Expand All @@ -141,37 +147,37 @@ func mapDockerfileToImagesSets(ctx context.Context, cfg *dockerfile.Dockerfile,
case *dockerfile.DockerfileStageInstruction[*instructions.ArgCommand]:
continue
case *dockerfile.DockerfileStageInstruction[*instructions.AddCommand]:
stg = stage_instruction.NewAdd(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewAdd(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.CmdCommand]:
stg = stage_instruction.NewCmd(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewCmd(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.CopyCommand]:
stg = stage_instruction.NewCopy(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewCopy(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.EntrypointCommand]:
stg = stage_instruction.NewEntrypoint(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewEntrypoint(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.EnvCommand]:
stg = stage_instruction.NewEnv(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewEnv(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.ExposeCommand]:
stg = stage_instruction.NewExpose(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewExpose(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.HealthCheckCommand]:
stg = stage_instruction.NewHealthcheck(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewHealthcheck(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.LabelCommand]:
stg = stage_instruction.NewLabel(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewLabel(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.MaintainerCommand]:
stg = stage_instruction.NewMaintainer(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewMaintainer(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.OnbuildCommand]:
stg = stage_instruction.NewOnBuild(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewOnBuild(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.RunCommand]:
stg = stage_instruction.NewRun(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewRun(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.ShellCommand]:
stg = stage_instruction.NewShell(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewShell(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.StopSignalCommand]:
stg = stage_instruction.NewStopSignal(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewStopSignal(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.UserCommand]:
stg = stage_instruction.NewUser(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewUser(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.VolumeCommand]:
stg = stage_instruction.NewVolume(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewVolume(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
case *dockerfile.DockerfileStageInstruction[*instructions.WorkdirCommand]:
stg = stage_instruction.NewWorkdir(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
stg = stage_instruction.NewWorkdir(typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions)
default:
panic(fmt.Sprintf("unsupported instruction type %#v", instr))
}
Expand All @@ -191,9 +197,10 @@ func mapDockerfileToImagesSets(ctx context.Context, cfg *dockerfile.Dockerfile,

func mapLegacyDockerfileToImage(ctx context.Context, dockerfileImageConfig *config.ImageFromDockerfile, opts CommonImageOptions) (*Image, error) {
img, err := NewImage(ctx, dockerfileImageConfig.Name, NoBaseImage, ImageOptions{
CommonImageOptions: opts,
IsDockerfileImage: true,
DockerfileImageConfig: dockerfileImageConfig,
CommonImageOptions: opts,
IsDockerfileImage: true,
IsDockerfileTargetStage: true,
DockerfileImageConfig: dockerfileImageConfig,
})
if err != nil {
return nil, fmt.Errorf("unable to create image %q: %w", dockerfileImageConfig.Name, err)
Expand Down
24 changes: 13 additions & 11 deletions pkg/build/image/image.go
Expand Up @@ -41,8 +41,8 @@ type CommonImageOptions struct {

type ImageOptions struct {
CommonImageOptions
IsArtifact, IsDockerfileImage bool
DockerfileImageConfig *config.ImageFromDockerfile
IsArtifact, IsDockerfileImage, IsDockerfileTargetStage bool
DockerfileImageConfig *config.ImageFromDockerfile

BaseImageReference string
BaseImageName string
Expand All @@ -58,11 +58,12 @@ func NewImage(ctx context.Context, name string, baseImageType BaseImageType, opt
}

i := &Image{
Name: name,
CommonImageOptions: opts.CommonImageOptions,
IsArtifact: opts.IsArtifact,
IsDockerfileImage: opts.IsDockerfileImage,
DockerfileImageConfig: opts.DockerfileImageConfig,
Name: name,
CommonImageOptions: opts.CommonImageOptions,
IsArtifact: opts.IsArtifact,
IsDockerfileImage: opts.IsDockerfileImage,
IsDockerfileTargetStage: opts.IsDockerfileTargetStage,
DockerfileImageConfig: opts.DockerfileImageConfig,

baseImageType: baseImageType,
baseImageReference: opts.BaseImageReference,
Expand All @@ -82,10 +83,11 @@ func NewImage(ctx context.Context, name string, baseImageType BaseImageType, opt
type Image struct {
CommonImageOptions

IsArtifact bool
IsDockerfileImage bool
Name string
DockerfileImageConfig *config.ImageFromDockerfile
IsArtifact bool
IsDockerfileImage bool
IsDockerfileTargetStage bool
Name string
DockerfileImageConfig *config.ImageFromDockerfile

stages []stage.Interface
lastNonEmptyStage stage.Interface
Expand Down
14 changes: 12 additions & 2 deletions pkg/build/stage/base.go
Expand Up @@ -74,6 +74,7 @@ var AllStages = []StageName{
}

type BaseStageOptions struct {
LogName string
ImageName string
ConfigMounts []*config.Mount
ImageTmpDir string
Expand All @@ -84,6 +85,7 @@ type BaseStageOptions struct {
func NewBaseStage(name StageName, options *BaseStageOptions) *BaseStage {
s := &BaseStage{}
s.name = name
s.logName = options.LogName
s.imageName = options.ImageName
s.configMounts = options.ConfigMounts
s.imageTmpDir = options.ImageTmpDir
Expand All @@ -94,6 +96,7 @@ func NewBaseStage(name StageName, options *BaseStageOptions) *BaseStage {

type BaseStage struct {
name StageName
logName string
imageName string
digest string
contentDigest string
Expand All @@ -119,13 +122,20 @@ func (s *BaseStage) LogDetailedName() string {
imageName = "~"
}

return fmt.Sprintf("%s/%s", imageName, s.Name())
return fmt.Sprintf("%s/%s", imageName, s.LogName())
}

func (s *BaseStage) ImageName() string {
return s.imageName
}

func (s *BaseStage) LogName() string {
if s.logName != "" {
return s.logName
}
return string(s.Name())
}

func (s *BaseStage) Name() StageName {
if s.name != "" {
return s.name
Expand Down Expand Up @@ -164,7 +174,7 @@ func (s *BaseStage) getNextStageGitDependencies(ctx context.Context, c Conveyor)
}
}

logboek.Context(ctx).Debug().LogF("Stage %q next stage dependencies: %#v\n", s.Name(), args)
logboek.Context(ctx).Debug().LogF("Stage %q next stage dependencies: %#v\n", s.LogName(), args)
sort.Strings(args)

return util.Sha256Hash(args...), nil
Expand Down
5 changes: 2 additions & 3 deletions pkg/build/stage/instruction/add.go
Expand Up @@ -19,8 +19,8 @@ type Add struct {
*Base[*instructions.AddCommand, *backend_instruction.Add]
}

func NewAdd(name stage.StageName, i *dockerfile.DockerfileStageInstruction[*instructions.AddCommand], dependencies []*config.Dependency, hasPrevStage bool, opts *stage.BaseStageOptions) *Add {
return &Add{Base: NewBase(name, i, backend_instruction.NewAdd(i.Data), dependencies, hasPrevStage, opts)}
func NewAdd(i *dockerfile.DockerfileStageInstruction[*instructions.AddCommand], dependencies []*config.Dependency, hasPrevStage bool, opts *stage.BaseStageOptions) *Add {
return &Add{Base: NewBase(i, backend_instruction.NewAdd(i.Data), dependencies, hasPrevStage, opts)}
}

func (stg *Add) GetDependencies(ctx context.Context, c stage.Conveyor, cb container_backend.ContainerBackend, prevImage, prevBuiltImage *stage.StageImage, buildContextArchive container_backend.BuildContextArchiver) (string, error) {
Expand All @@ -29,7 +29,6 @@ func (stg *Add) GetDependencies(ctx context.Context, c stage.Conveyor, cb contai
return "", err
}

args = append(args, "Instruction", stg.instruction.Data.Name())
args = append(args, append([]string{"Sources"}, stg.instruction.Data.Sources()...)...)
args = append(args, "Dest", stg.instruction.Data.Dest())
args = append(args, "Chown", stg.instruction.Data.Chown)
Expand Down
24 changes: 12 additions & 12 deletions pkg/build/stage/instruction/add_test.go
Expand Up @@ -25,7 +25,7 @@ var _ = DescribeTable("ADD digest",
},

Entry("ADD basic", NewTestData(
NewAdd("ADD",
NewAdd(
dockerfile.NewDockerfileStageInstruction(
&instructions.AddCommand{SourcesAndDest: []string{"src", "/app"}, Chown: "1000:1000", Chmod: ""},
dockerfile.DockerfileStageInstructionOptions{},
Expand All @@ -36,7 +36,7 @@ var _ = DescribeTable("ADD digest",
ProjectName: "example-project",
},
),
"88c31da85ac26ae35d29462c6dc309c2a02997c0de92b4ccee7db2e41be17187",
"79d3642e997030deb225e0414f0c2d0e3c6681cc036899aaba62895f7e2ac4e3",
TestDataOptions{
Files: []*FileData{
{Name: "src/main/java/worker/Worker.java", Data: []byte(`package worker;`)},
Expand All @@ -46,7 +46,7 @@ var _ = DescribeTable("ADD digest",
)),

Entry("ADD with changed chown", NewTestData(
NewAdd("ADD",
NewAdd(
dockerfile.NewDockerfileStageInstruction(
&instructions.AddCommand{SourcesAndDest: []string{"src", "/app"}, Chown: "1000:1001", Chmod: ""},
dockerfile.DockerfileStageInstructionOptions{},
Expand All @@ -57,7 +57,7 @@ var _ = DescribeTable("ADD digest",
ProjectName: "example-project",
},
),
"846ef29e994224dd84bf0a5de47b0b3255c8681b8178e8da5611b21547cd182b",
"ccfda65ec4fa8667abe7a54b047a98158d122b39e224929dbb7c33ea466e7f5a",
TestDataOptions{
Files: []*FileData{
{Name: "src/main/java/worker/Worker.java", Data: []byte(`package worker;`)},
Expand All @@ -68,7 +68,7 @@ var _ = DescribeTable("ADD digest",
)),

Entry("ADD with changed chmod", NewTestData(
NewAdd("ADD",
NewAdd(
dockerfile.NewDockerfileStageInstruction(
&instructions.AddCommand{SourcesAndDest: []string{"src", "/app"}, Chown: "1000:1001", Chmod: "0777"},
dockerfile.DockerfileStageInstructionOptions{},
Expand All @@ -79,7 +79,7 @@ var _ = DescribeTable("ADD digest",
ProjectName: "example-project",
},
),
"cef21e87710631a08edeb176a9487f81ae20171c22ec4537a3dc8fbc67aca868",
"19e86e1145aecd554d7d492add3c6c6ed51b022279f059f6c8c54a4eac9d07f0",
TestDataOptions{
Files: []*FileData{
{Name: "src/main/java/worker/Worker.java", Data: []byte(`package worker;`)},
Expand All @@ -90,7 +90,7 @@ var _ = DescribeTable("ADD digest",
)),

Entry("ADD with changed sources paths", NewTestData(
NewAdd("ADD",
NewAdd(
dockerfile.NewDockerfileStageInstruction(
&instructions.AddCommand{SourcesAndDest: []string{"src", "pom.xml", "/app"}, Chown: "1000:1001", Chmod: "0777"},
dockerfile.DockerfileStageInstructionOptions{},
Expand All @@ -101,7 +101,7 @@ var _ = DescribeTable("ADD digest",
ProjectName: "example-project",
},
),
"97f3f8a240902d73ec9a209f6c8368047b56d9247bdf9da88a40ac5dba925209",
"4d17db1e6926bbe9e4f7b70d18bb055b7735f6bfb6db35452524bde561e8b95f",
TestDataOptions{
Files: []*FileData{
{Name: "src/main/java/worker/Worker.java", Data: []byte(`package worker;`)},
Expand All @@ -112,7 +112,7 @@ var _ = DescribeTable("ADD digest",
)),

Entry("ADD with changed source files", NewTestData(
NewAdd("ADD",
NewAdd(
dockerfile.NewDockerfileStageInstruction(
&instructions.AddCommand{SourcesAndDest: []string{"src", "pom.xml", "/app"}, Chown: "1000:1001", Chmod: "0777"},
dockerfile.DockerfileStageInstructionOptions{},
Expand All @@ -123,7 +123,7 @@ var _ = DescribeTable("ADD digest",
ProjectName: "example-project",
},
),
"60178e0b174bd1bce1cd29f8132ea84cc7212773b6fce9fad3ddff842d5cf2e0",
"372ed0cb6fff0a58e087fa8bf19e1f62a146d3983ca4510c496c452db8a7080e",
TestDataOptions{
Files: []*FileData{
{Name: "src/main/java/worker/Worker.java", Data: []byte(`package worker2;`)},
Expand All @@ -134,7 +134,7 @@ var _ = DescribeTable("ADD digest",
)),

Entry("ADD with changed destination path", NewTestData(
NewAdd("ADD",
NewAdd(
dockerfile.NewDockerfileStageInstruction(
&instructions.AddCommand{SourcesAndDest: []string{"src", "pom.xml", "/app2"}, Chown: "1000:1001", Chmod: "0777"},
dockerfile.DockerfileStageInstructionOptions{},
Expand All @@ -145,7 +145,7 @@ var _ = DescribeTable("ADD digest",
ProjectName: "example-project",
},
),
"c1f03d5701951fe9c5836957c753c9486f22e14b2d9291780ae70f288e531e1c",
"825c66ecb926ed7897fc99f7686ed4fc2a7f8133d6a66860c8755772764d0293",
TestDataOptions{
Files: []*FileData{
{Name: "src/main/java/worker/Worker.java", Data: []byte(`package worker2;`)},
Expand Down
4 changes: 2 additions & 2 deletions pkg/build/stage/instruction/base.go
Expand Up @@ -19,9 +19,9 @@ type Base[T dockerfile.InstructionDataInterface, BT container_backend.Instructio
hasPrevStage bool
}

func NewBase[T dockerfile.InstructionDataInterface, BT container_backend.InstructionInterface](name stage.StageName, instruction *dockerfile.DockerfileStageInstruction[T], backendInstruction BT, dependencies []*config.Dependency, hasPrevStage bool, opts *stage.BaseStageOptions) *Base[T, BT] {
func NewBase[T dockerfile.InstructionDataInterface, BT container_backend.InstructionInterface](instruction *dockerfile.DockerfileStageInstruction[T], backendInstruction BT, dependencies []*config.Dependency, hasPrevStage bool, opts *stage.BaseStageOptions) *Base[T, BT] {
return &Base[T, BT]{
BaseStage: stage.NewBaseStage(name, opts),
BaseStage: stage.NewBaseStage(stage.StageName(instruction.Data.Name()), opts),
instruction: instruction,
backendInstruction: backendInstruction,
dependencies: dependencies,
Expand Down
5 changes: 2 additions & 3 deletions pkg/build/stage/instruction/cmd.go
Expand Up @@ -18,8 +18,8 @@ type Cmd struct {
*Base[*instructions.CmdCommand, *backend_instruction.Cmd]
}

func NewCmd(name stage.StageName, i *dockerfile.DockerfileStageInstruction[*instructions.CmdCommand], dependencies []*config.Dependency, hasPrevStage bool, opts *stage.BaseStageOptions) *Cmd {
return &Cmd{Base: NewBase(name, i, backend_instruction.NewCmd(i.Data), dependencies, hasPrevStage, opts)}
func NewCmd(i *dockerfile.DockerfileStageInstruction[*instructions.CmdCommand], dependencies []*config.Dependency, hasPrevStage bool, opts *stage.BaseStageOptions) *Cmd {
return &Cmd{Base: NewBase(i, backend_instruction.NewCmd(i.Data), dependencies, hasPrevStage, opts)}
}

func (stg *Cmd) GetDependencies(ctx context.Context, c stage.Conveyor, cb container_backend.ContainerBackend, prevImage, prevBuiltImage *stage.StageImage, buildContextArchive container_backend.BuildContextArchiver) (string, error) {
Expand All @@ -28,7 +28,6 @@ func (stg *Cmd) GetDependencies(ctx context.Context, c stage.Conveyor, cb contai
return "", err
}

args = append(args, "Instruction", stg.instruction.Data.Name())
args = append(args, append([]string{"Cmd"}, stg.instruction.Data.CmdLine...)...)
args = append(args, "PrependShell", fmt.Sprintf("%v", stg.instruction.Data.PrependShell))

Expand Down

0 comments on commit a0d7838

Please sign in to comment.