diff options
author | Dan Duvall <dduvall@wikimedia.org> | 2017-10-16 11:58:00 -0700 |
---|---|---|
committer | Dan Duvall <dduvall@wikimedia.org> | 2017-10-19 09:39:49 -0700 |
commit | 818a0fcc0ffcda1c80a33c4f1ff6e1cc4d52abe3 (patch) | |
tree | 9ee6e4067414506e6bd422888ea0d08dc7eefef7 | |
parent | d53a06a138106c1652fff0b8576ae907a29a727c (diff) | |
download | blubber-818a0fcc0ffcda1c80a33c4f1ff6e1cc4d52abe3.tar.gz |
Include meta data as labels in Dockerfile output
Summary:
Certain meta data including the Blubber version and variant used at
invocation may be useful in downstream tracking.
Implemented abstract and Docker-specific instructions for labels and
modified the Docker compiler to include these instructions at the end of
the output for the final stage.
Depends on D816
Fixes T178022
Test Plan:
Run unit tests. Run `make && bin/blubber blubber.example.yaml production` and
verify that both `blubber.version` and `blubber.variant` labels are present
and accurate.
Reviewers: thcipriani, hashar, #release-engineering-team
Reviewed By: thcipriani, #release-engineering-team
Tags: #release-engineering-team
Maniphest Tasks: T178022
Differential Revision: https://phabricator.wikimedia.org/D818
-rw-r--r-- | build/instructions.go | 39 | ||||
-rw-r--r-- | build/instructions_test.go | 14 | ||||
-rw-r--r-- | docker/compiler.go | 49 | ||||
-rw-r--r-- | docker/compiler_test.go | 19 | ||||
-rw-r--r-- | docker/instructions.go | 14 | ||||
-rw-r--r-- | docker/instructions_test.go | 12 |
6 files changed, 116 insertions, 31 deletions
diff --git a/build/instructions.go b/build/instructions.go index 38ea322..41c959d 100644 --- a/build/instructions.go +++ b/build/instructions.go @@ -64,20 +64,18 @@ type Env struct { } func (env Env) Compile() []string { - defs := make([]string, 0, len(env.Definitions)) - names := make([]string, 0, len(env.Definitions)) - - for name := range env.Definitions { - names = append(names, name) - } - - sort.Strings(names) + return compileSortedKeyValues(env.Definitions) +} - for _, name := range names { - defs = append(defs, name+"="+quote(env.Definitions[name])) - } +// Label represents a number of meta-data key/value pairs +type Label struct { + Definitions map[string]string +} - return defs +// Compile returns the label key/value pairs as a number of `key="value"` +// strings where the values are properly quoted +func (label Label) Compile() []string { + return compileSortedKeyValues(label.Definitions) } type Volume struct { @@ -88,6 +86,23 @@ func (vol Volume) Compile() []string { return []string{quote(vol.Path)} } +func compileSortedKeyValues(keyValues map[string]string) []string { + defs := make([]string, 0, len(keyValues)) + names := make([]string, 0, len(keyValues)) + + for name := range keyValues { + names = append(names, name) + } + + sort.Strings(names) + + for _, name := range names { + defs = append(defs, name+"="+quote(keyValues[name])) + } + + return defs +} + func quote(arg string) string { return strconv.Quote(arg) } diff --git a/build/instructions_test.go b/build/instructions_test.go index acbdb7c..4098711 100644 --- a/build/instructions_test.go +++ b/build/instructions_test.go @@ -56,6 +56,20 @@ func TestEnv(t *testing.T) { }, i.Compile()) } +func TestLabel(t *testing.T) { + i := build.Label{map[string]string{ + "fooname": "foovalue", + "barname": "barvalue", + "quxname": "quxvalue", + }} + + assert.Equal(t, []string{ + `barname="barvalue"`, + `fooname="foovalue"`, + `quxname="quxvalue"`, + }, i.Compile()) +} + func TestVolume(t *testing.T) { i := build.Volume{"/foo/dir"} diff --git a/docker/compiler.go b/docker/compiler.go index 79f179e..0a2120b 100644 --- a/docker/compiler.go +++ b/docker/compiler.go @@ -6,6 +6,7 @@ import ( "phabricator.wikimedia.org/source/blubber/build" "phabricator.wikimedia.org/source/blubber/config" + "phabricator.wikimedia.org/source/blubber/meta" ) // Compile blubber yaml file into Dockerfile @@ -28,63 +29,73 @@ func Compile(cfg *config.Config, variant string) (*bytes.Buffer, error) { if err != nil { return nil, err } - CompileStage(buffer, stage, dependency) + compileStage(buffer, stage, dependency) mainStage = variant } - CompileStage(buffer, mainStage, vcfg) + compileStage(buffer, mainStage, vcfg) + + // add meta-data labels to the final stage + compileInstructions(buffer, build.Label{map[string]string{ + "blubber.variant": variant, + "blubber.version": meta.FullVersion(), + }}) return buffer, nil } -func CompileStage(buffer *bytes.Buffer, stage string, vcfg *config.VariantConfig) { +func compileStage(buffer *bytes.Buffer, stage string, vcfg *config.VariantConfig) { baseAndStage := vcfg.Base if stage != "" { baseAndStage += " AS " + stage } - Writeln(buffer, "FROM ", baseAndStage) + writeln(buffer, "FROM ", baseAndStage) - Writeln(buffer, "USER root") + writeln(buffer, "USER root") - CompilePhase(buffer, vcfg, build.PhasePrivileged) + compilePhase(buffer, vcfg, build.PhasePrivileged) if vcfg.Runs.As != "" { - Writeln(buffer, "USER ", vcfg.Runs.As) + writeln(buffer, "USER ", vcfg.Runs.As) } - CompilePhase(buffer, vcfg, build.PhasePrivilegeDropped) + compilePhase(buffer, vcfg, build.PhasePrivilegeDropped) if vcfg.Runs.In != "" { - Writeln(buffer, "WORKDIR ", vcfg.Runs.In) + writeln(buffer, "WORKDIR ", vcfg.Runs.In) } - CompilePhase(buffer, vcfg, build.PhasePreInstall) + compilePhase(buffer, vcfg, build.PhasePreInstall) - CompilePhase(buffer, vcfg, build.PhaseInstall) + compilePhase(buffer, vcfg, build.PhaseInstall) - CompilePhase(buffer, vcfg, build.PhasePostInstall) + compilePhase(buffer, vcfg, build.PhasePostInstall) if len(vcfg.EntryPoint) > 0 { - Writeln(buffer, "ENTRYPOINT [\"", strings.Join(vcfg.EntryPoint, "\", \""), "\"]") + writeln(buffer, "ENTRYPOINT [\"", strings.Join(vcfg.EntryPoint, "\", \""), "\"]") } } -func CompilePhase(buffer *bytes.Buffer, vcfg *config.VariantConfig, phase build.Phase) { - for _, instruction := range vcfg.InstructionsForPhase(phase) { +func compileInstructions(buffer *bytes.Buffer, instructions ...build.Instruction) { + for _, instruction := range instructions { dockerInstruction, _ := NewDockerInstruction(instruction) - Write(buffer, dockerInstruction.Compile()) + write(buffer, dockerInstruction.Compile()) } } -func Write(buffer *bytes.Buffer, strings ...string) { +func compilePhase(buffer *bytes.Buffer, vcfg *config.VariantConfig, phase build.Phase) { + compileInstructions(buffer, vcfg.InstructionsForPhase(phase)...) +} + +func write(buffer *bytes.Buffer, strings ...string) { for _, str := range strings { buffer.WriteString(str) } } -func Writeln(buffer *bytes.Buffer, strings ...string) { - Write(buffer, strings...) +func writeln(buffer *bytes.Buffer, strings ...string) { + write(buffer, strings...) buffer.WriteString("\n") } diff --git a/docker/compiler_test.go b/docker/compiler_test.go index 04454b4..846304b 100644 --- a/docker/compiler_test.go +++ b/docker/compiler_test.go @@ -8,6 +8,7 @@ import ( "phabricator.wikimedia.org/source/blubber/config" "phabricator.wikimedia.org/source/blubber/docker" + "phabricator.wikimedia.org/source/blubber/meta" ) func TestSingleStageHasNoName(t *testing.T) { @@ -72,3 +73,21 @@ func TestMultipleArtifactsFromSameStage(t *testing.T) { assert.Equal(t, 1, strings.Count(dockerfile, "FROM foo/bar AS build\n")) assert.Equal(t, 1, strings.Count(dockerfile, "FROM foo/bar AS production\n")) } + +func TestMetaDataLabels(t *testing.T) { + cfg, err := config.ReadConfig([]byte(`--- + base: foo/bar + variants: + development: {}`)) + + assert.Nil(t, err) + + dockerOut, _ := docker.Compile(cfg, "development") + dockerfile := dockerOut.String() + + version := meta.FullVersion() + + assert.Contains(t, dockerfile, + "LABEL blubber.variant=\"development\" blubber.version=\""+version+"\"\n", + ) +} diff --git a/docker/instructions.go b/docker/instructions.go index fa566cb..eba6731 100644 --- a/docker/instructions.go +++ b/docker/instructions.go @@ -26,6 +26,10 @@ func NewDockerInstruction(instruction build.Instruction) (DockerInstruction, err var dockerInstruction DockerEnv dockerInstruction.arguments = instruction.Compile() return dockerInstruction, nil + case build.Label: + var dockerInstruction DockerLabel + dockerInstruction.arguments = instruction.Compile() + return dockerInstruction, nil case build.Volume: var dockerInstruction DockerVolume dockerInstruction.arguments = instruction.Compile() @@ -81,6 +85,16 @@ func (de DockerEnv) Compile() string { join(de.arguments, " ")) } +// DockerLabel represents a concrete LABEL instruction +type DockerLabel struct{ abstractDockerInstruction } + +// Compile returns multiple key="value" arguments as a single LABEL string +func (dl DockerLabel) Compile() string { + return fmt.Sprintf( + "LABEL %s\n", + join(dl.arguments, " ")) +} + type DockerVolume struct{ abstractDockerInstruction } func (dv DockerVolume) Compile() string { diff --git a/docker/instructions_test.go b/docker/instructions_test.go index 87dfada..a4f648b 100644 --- a/docker/instructions_test.go +++ b/docker/instructions_test.go @@ -71,6 +71,18 @@ func TestEnv(t *testing.T) { assert.Equal(t, "ENV bar=\"foo\" foo=\"bar\"\n", di.Compile()) } +func TestLabel(t *testing.T) { + i := build.Label{map[string]string{"foo": "bar", "bar": "foo"}} + + di, err := docker.NewDockerInstruction(i) + + var dockerLabel docker.DockerLabel + + assert.Nil(t, err) + assert.IsType(t, dockerLabel, di) + assert.Equal(t, "LABEL bar=\"foo\" foo=\"bar\"\n", di.Compile()) +} + func TestVolume(t *testing.T) { i := build.Volume{"/foo/dir"} |