diff options
author | Dan Duvall <dduvall@wikimedia.org> | 2018-02-21 16:18:47 -0800 |
---|---|---|
committer | Dan Duvall <dduvall@wikimedia.org> | 2018-03-05 13:22:10 -0800 |
commit | 47526283fea7df1734ef5b9a5da5c810bf76a29a (patch) | |
tree | 7d6ed2530acdaded093f410939aa9df250130c0a /config | |
parent | f606d212fd94769294b1ebdaa6ec224458281d22 (diff) | |
download | blubber-47526283fea7df1734ef5b9a5da5c810bf76a29a.tar.gz |
Fix application files/runtime permissions scheme
Summary:
Introduces new `lives` configuration that provides the name/UID/GID of
the user that will own application files and installed dependencies.
This new configuration is distinct from `runs` in that the former
determines application file location ownership and the latter now only
determines runtime process ownership. Default configuration has also
been introduced for both config sections.
In addition to the new configuration, a new `build.CopyAs` instruction
has been introduced that ensures correct UID/GID ownership of files
copied into the container image, and all unqualified `build.Copy`
instructions are wrapped by the new `build.CopyAs` instruction using the
UID/GID appropriate for the current build phase. A new `build.User`
instruction is also introduced and injected into the build at the start
of certain phases to enforce ownership of `build.Run` processes.
This effective process/file ownership model is:
PhasePrivileged - "root"
PhasePrivilegedDropped - lives.as
PhasePreInstall - lives.as
PhaseInstall - lives.as
PhasePostInstall - runs.as
Fixes T187372
Test Plan: Run `go test ./...`.
Reviewers: thcipriani, hashar, demon, #release-engineering-team
Reviewed By: thcipriani, #release-engineering-team
Subscribers: mmodell
Tags: #release-engineering-team
Maniphest Tasks: T187372
Differential Revision: https://phabricator.wikimedia.org/D984
Diffstat (limited to 'config')
-rw-r--r-- | config/common.go | 16 | ||||
-rw-r--r-- | config/lives.go | 54 | ||||
-rw-r--r-- | config/lives_test.go | 194 | ||||
-rw-r--r-- | config/reader.go | 18 | ||||
-rw-r--r-- | config/runs.go | 84 | ||||
-rw-r--r-- | config/runs_test.go | 88 | ||||
-rw-r--r-- | config/user.go | 25 | ||||
-rw-r--r-- | config/variant.go | 33 | ||||
-rw-r--r-- | config/variant_test.go | 10 |
9 files changed, 352 insertions, 170 deletions
diff --git a/config/common.go b/config/common.go index 668b543..fa100b8 100644 --- a/config/common.go +++ b/config/common.go @@ -8,12 +8,13 @@ import ( // and each configured variant. // type CommonConfig struct { - Base string `yaml:"base" validate:"omitempty,baseimage"` // name/path to base image - Apt AptConfig `yaml:"apt"` // APT related configuration - Node NodeConfig `yaml:"node"` // Node related configuration - Runs RunsConfig `yaml:"runs"` // runtime environment configuration - SharedVolume Flag `yaml:"sharedvolume"` // define a volume for application - EntryPoint []string `yaml:"entrypoint"` // entry-point executable + Base string `yaml:"base" validate:"omitempty,baseimage"` // name/path to base image + Apt AptConfig `yaml:"apt"` // APT related configuration + Node NodeConfig `yaml:"node"` // Node related configuration + Lives LivesConfig `yaml:"lives"` // application owner/dir configuration + Runs RunsConfig `yaml:"runs"` // runtime environment configuration + SharedVolume Flag `yaml:"sharedvolume"` // define a volume for application + EntryPoint []string `yaml:"entrypoint"` // entry-point executable } // Merge takes another CommonConfig and merges its fields this one's. @@ -25,6 +26,7 @@ func (cc *CommonConfig) Merge(cc2 CommonConfig) { cc.Apt.Merge(cc2.Apt) cc.Node.Merge(cc2.Node) + cc.Lives.Merge(cc2.Lives) cc.Runs.Merge(cc2.Runs) cc.SharedVolume.Merge(cc2.SharedVolume) @@ -38,7 +40,7 @@ func (cc *CommonConfig) Merge(cc2 CommonConfig) { // injected. // func (cc *CommonConfig) PhaseCompileableConfig() []build.PhaseCompileable { - return []build.PhaseCompileable{cc.Apt, cc.Node, cc.Runs} + return []build.PhaseCompileable{cc.Apt, cc.Node, cc.Lives, cc.Runs} } // InstructionsForPhase injects instructions into the given build phase for diff --git a/config/lives.go b/config/lives.go new file mode 100644 index 0000000..8d705e7 --- /dev/null +++ b/config/lives.go @@ -0,0 +1,54 @@ +package config + +import ( + "phabricator.wikimedia.org/source/blubber/build" +) + +// LocalLibPrefix declares the shared directory into which application level +// dependencies will be installed. +// +const LocalLibPrefix = "/opt/lib" + +// LivesConfig holds configuration fields related to the livesship of +// installed dependencies and application files. +// +type LivesConfig struct { + In string `yaml:"in" validate:"omitempty,abspath"` // application directory + UserConfig `yaml:",inline"` +} + +// Merge takes another LivesConfig and overwrites this struct's fields. +// +func (lives *LivesConfig) Merge(lives2 LivesConfig) { + if lives2.In != "" { + lives.In = lives2.In + } + + lives.UserConfig.Merge(lives2.UserConfig) +} + +// InstructionsForPhase injects build instructions related to creation of the +// application lives. +// +// PhasePrivileged +// +// Creates LocalLibPrefix directory and application lives's user home +// directory, creates the lives user and its group, and sets up directory +// permissions. +// +func (lives LivesConfig) InstructionsForPhase(phase build.Phase) []build.Instruction { + switch phase { + case build.PhasePrivileged: + return []build.Instruction{build.RunAll{ + append( + build.CreateUser(lives.As, lives.UID, lives.GID), + build.CreateDirectory(lives.In), + build.Chown(lives.UID, lives.GID, lives.In), + build.CreateDirectory(LocalLibPrefix), + build.Chown(lives.UID, lives.GID, LocalLibPrefix), + ), + }} + } + + return []build.Instruction{} +} diff --git a/config/lives_test.go b/config/lives_test.go new file mode 100644 index 0000000..eeb91ef --- /dev/null +++ b/config/lives_test.go @@ -0,0 +1,194 @@ +package config_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "phabricator.wikimedia.org/source/blubber/build" + "phabricator.wikimedia.org/source/blubber/config" +) + +func TestLivesConfig(t *testing.T) { + cfg, err := config.ReadConfig([]byte(`--- + base: foo + lives: + in: /some/directory + as: foouser + uid: 123 + gid: 223 + variants: + development: {}`)) + + if assert.NoError(t, err) { + assert.Equal(t, "/some/directory", cfg.Lives.In) + assert.Equal(t, "foouser", cfg.Lives.As) + assert.Equal(t, uint(123), cfg.Lives.UID) + assert.Equal(t, uint(223), cfg.Lives.GID) + + variant, err := config.ExpandVariant(cfg, "development") + + if assert.NoError(t, err) { + assert.Equal(t, "/some/directory", variant.Lives.In) + assert.Equal(t, "foouser", variant.Lives.As) + assert.Equal(t, uint(123), variant.Lives.UID) + assert.Equal(t, uint(223), variant.Lives.GID) + } + } +} + +func TestLivesConfigDefaults(t *testing.T) { + cfg, err := config.ReadConfig([]byte(`--- + base: foo`)) + + if assert.NoError(t, err) { + assert.Equal(t, "somebody", cfg.Lives.As) + assert.Equal(t, uint(65533), cfg.Lives.UID) + assert.Equal(t, uint(65533), cfg.Lives.GID) + } +} + +func TestLivesConfigInstructions(t *testing.T) { + cfg := config.LivesConfig{ + In: "/some/directory", + UserConfig: config.UserConfig{ + As: "foouser", + UID: 123, + GID: 223, + }, + } + + t.Run("PhasePrivileged", func(t *testing.T) { + assert.Equal(t, + []build.Instruction{build.RunAll{[]build.Run{ + {"groupadd -o -g %s -r", []string{"223", "foouser"}}, + {"useradd -o -m -d %s -r -g %s -u %s", []string{"/home/foouser", "foouser", "123", "foouser"}}, + {"mkdir -p", []string{"/some/directory"}}, + {"chown %s:%s", []string{"123", "223", "/some/directory"}}, + {"mkdir -p", []string{"/opt/lib"}}, + {"chown %s:%s", []string{"123", "223", "/opt/lib"}}, + }}}, + cfg.InstructionsForPhase(build.PhasePrivileged), + ) + }) + + t.Run("PhasePrivilegeDropped", func(t *testing.T) { + assert.Empty(t, cfg.InstructionsForPhase(build.PhasePreInstall)) + }) + + t.Run("PhasePreInstall", func(t *testing.T) { + assert.Empty(t, cfg.InstructionsForPhase(build.PhasePreInstall)) + }) + + t.Run("PhasePostInstall", func(t *testing.T) { + assert.Empty(t, cfg.InstructionsForPhase(build.PhasePostInstall)) + }) +} + +func TestLivesConfigValidation(t *testing.T) { + t.Run("in", func(t *testing.T) { + t.Run("ok", func(t *testing.T) { + _, err := config.ReadConfig([]byte(`--- + lives: + in: /foo`)) + + assert.False(t, config.IsValidationError(err)) + }) + + t.Run("optional", func(t *testing.T) { + _, err := config.ReadConfig([]byte(`--- + lives: {}`)) + + assert.False(t, config.IsValidationError(err)) + }) + + t.Run("non-root", func(t *testing.T) { + _, err := config.ReadConfig([]byte(`--- + lives: + in: /`)) + + if assert.True(t, config.IsValidationError(err)) { + msg := config.HumanizeValidationError(err) + + assert.Equal(t, `in: "/" is not a valid absolute non-root path`, msg) + } + }) + + t.Run("non-root tricky", func(t *testing.T) { + _, err := config.ReadConfig([]byte(`--- + lives: + in: /foo/..`)) + + if assert.True(t, config.IsValidationError(err)) { + msg := config.HumanizeValidationError(err) + + assert.Equal(t, `in: "/foo/.." is not a valid absolute non-root path`, msg) + } + }) + + t.Run("absolute", func(t *testing.T) { + _, err := config.ReadConfig([]byte(`--- + lives: + in: foo/bar`)) + + if assert.True(t, config.IsValidationError(err)) { + msg := config.HumanizeValidationError(err) + + assert.Equal(t, `in: "foo/bar" is not a valid absolute non-root path`, msg) + } + }) + }) + + t.Run("as", func(t *testing.T) { + t.Run("ok", func(t *testing.T) { + _, err := config.ReadConfig([]byte(`--- + lives: + as: foo-bar.baz`)) + + assert.False(t, config.IsValidationError(err)) + }) + + t.Run("optional", func(t *testing.T) { + _, err := config.ReadConfig([]byte(`--- + lives: {}`)) + + assert.False(t, config.IsValidationError(err)) + }) + + t.Run("no spaces", func(t *testing.T) { + _, err := config.ReadConfig([]byte(`--- + lives: + as: foo bar`)) + + if assert.True(t, config.IsValidationError(err)) { + msg := config.HumanizeValidationError(err) + + assert.Equal(t, `as: "foo bar" is not a valid user name`, msg) + } + }) + + t.Run("long enough", func(t *testing.T) { + _, err := config.ReadConfig([]byte(`--- + lives: + as: fo`)) + + if assert.True(t, config.IsValidationError(err)) { + msg := config.HumanizeValidationError(err) + + assert.Equal(t, `as: "fo" is not a valid user name`, msg) + } + }) + + t.Run("not root", func(t *testing.T) { + _, err := config.ReadConfig([]byte(`--- + lives: + as: root`)) + + if assert.True(t, config.IsValidationError(err)) { + msg := config.HumanizeValidationError(err) + + assert.Equal(t, `as: "root" is not a valid user name`, msg) + } + }) + }) +} diff --git a/config/reader.go b/config/reader.go index bb28603..378d523 100644 --- a/config/reader.go +++ b/config/reader.go @@ -8,6 +8,20 @@ import ( "gopkg.in/yaml.v2" ) +// DefaultConfig contains YAML that is applied before the user's +// configuration. +// +const DefaultConfig = `--- +lives: + in: /srv/app + as: somebody + uid: 65533 + gid: 65533 +runs: + as: runuser + uid: 900 + gid: 900` + // ResolveIncludes iterates over and recurses through a given variant's // includes to build a flat slice of variant names in the correct order by // which they should be expanded/merged. It checks for both the existence of @@ -74,11 +88,13 @@ func ExpandVariant(config *Config, name string) (*VariantConfig, error) { return expanded, nil } -// ReadConfig unmarshals the given YAML bytes into a Config struct. +// ReadConfig unmarshals the given YAML bytes into a new Config struct. // func ReadConfig(data []byte) (*Config, error) { var config Config + yaml.Unmarshal([]byte(DefaultConfig), &config) + err := yaml.Unmarshal(data, &config) if err != nil { diff --git a/config/runs.go b/config/runs.go index 6769de2..a4147e5 100644 --- a/config/runs.go +++ b/config/runs.go @@ -1,25 +1,15 @@ package config import ( - "fmt" - "phabricator.wikimedia.org/source/blubber/build" ) -// LocalLibPrefix declares the shared directory into which application level -// dependencies will be installed. -// -const LocalLibPrefix = "/opt/lib" - // RunsConfig holds configuration fields related to the application's // runtime environment. // type RunsConfig struct { - In string `yaml:"in" validate:"omitempty,abspath"` // working directory - As string `yaml:"as" validate:"omitempty,username"` // unprivileged user - UID uint `yaml:"uid"` // unprivileged user ID - GID uint `yaml:"gid"` // unprivileged group ID - Environment map[string]string `yaml:"environment" validate:"envvars"` // environment variables + UserConfig `yaml:",inline"` + Environment map[string]string `yaml:"environment" validate:"envvars"` // environment variables } // Merge takes another RunsConfig and overwrites this struct's fields. All @@ -27,18 +17,7 @@ type RunsConfig struct { // merge. // func (run *RunsConfig) Merge(run2 RunsConfig) { - if run2.In != "" { - run.In = run2.In - } - if run2.As != "" { - run.As = run2.As - } - if run2.UID != 0 { - run.UID = run2.UID - } - if run2.GID != 0 { - run.GID = run2.GID - } + run.UserConfig.Merge(run2.UserConfig) if run.Environment == nil { run.Environment = make(map[string]string) @@ -49,17 +28,6 @@ func (run *RunsConfig) Merge(run2 RunsConfig) { } } -// Home returns the home directory for the configured user, or /root if no -// user is set. -// -func (run RunsConfig) Home() string { - if run.As == "" { - return "/root" - } - - return "/home/" + run.As -} - // InstructionsForPhase injects build instructions related to the runtime // configuration. // @@ -71,52 +39,22 @@ func (run RunsConfig) Home() string { // // PhasePrivilegeDropped // -// Injects build.Env instructions for the user home directory and all -// names/values defined by RunsConfig.Environment. +// Injects build.Env instructions for all names/values defined by +// RunsConfig.Environment. // func (run RunsConfig) InstructionsForPhase(phase build.Phase) []build.Instruction { - ins := []build.Instruction{} - switch phase { case build.PhasePrivileged: - runAll := build.RunAll{[]build.Run{ - {"mkdir -p", []string{LocalLibPrefix}}, + return []build.Instruction{build.RunAll{ + build.CreateUser(run.As, run.UID, run.GID), }} - - if run.In != "" { - runAll.Runs = append(runAll.Runs, - build.Run{"mkdir -p", []string{run.In}}, - ) - } - - if run.As != "" { - runAll.Runs = append(runAll.Runs, - build.Run{"groupadd -o -g %s -r", - []string{fmt.Sprint(run.GID), run.As}}, - build.Run{"useradd -o -m -d %s -r -g %s -u %s", - []string{run.Home(), run.As, fmt.Sprint(run.UID), run.As}}, - build.Run{"chown %s:%s", - []string{run.As, run.As, LocalLibPrefix}}, - ) - - if run.In != "" { - runAll.Runs = append(runAll.Runs, - build.Run{"chown %s:%s", - []string{run.As, run.As, run.In}}, - ) - } - } - - if len(runAll.Runs) > 0 { - ins = append(ins, runAll) - } case build.PhasePrivilegeDropped: - ins = append(ins, build.Env{map[string]string{"HOME": run.Home()}}) - if len(run.Environment) > 0 { - ins = append(ins, build.Env{run.Environment}) + return []build.Instruction{ + build.Env{run.Environment}, + } } } - return ins + return []build.Instruction{} } diff --git a/config/runs_test.go b/config/runs_test.go index 0b7d26a..4d46eb4 100644 --- a/config/runs_test.go +++ b/config/runs_test.go @@ -14,7 +14,6 @@ func TestRunsConfig(t *testing.T) { base: foo runs: as: someuser - in: /some/directory uid: 666 gid: 777 environment: { FOO: bar } @@ -28,30 +27,18 @@ func TestRunsConfig(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "someuser", variant.Runs.As) - assert.Equal(t, "/some/directory", variant.Runs.In) assert.Equal(t, uint(666), variant.Runs.UID) assert.Equal(t, uint(777), variant.Runs.GID) assert.Equal(t, map[string]string{"FOO": "bar"}, variant.Runs.Environment) } -func TestRunsHomeWithUser(t *testing.T) { - runs := config.RunsConfig{As: "someuser"} - - assert.Equal(t, "/home/someuser", runs.Home()) -} - -func TestRunsHomeWithoutUser(t *testing.T) { - runs := config.RunsConfig{} - - assert.Equal(t, "/root", runs.Home()) -} - func TestRunsConfigInstructions(t *testing.T) { cfg := config.RunsConfig{ - As: "someuser", - In: "/some/directory", - UID: 666, - GID: 777, + UserConfig: config.UserConfig{ + As: "someuser", + UID: 666, + GID: 777, + }, Environment: map[string]string{ "fooname": "foovalue", "barname": "barvalue", @@ -61,12 +48,8 @@ func TestRunsConfigInstructions(t *testing.T) { t.Run("PhasePrivileged", func(t *testing.T) { assert.Equal(t, []build.Instruction{build.RunAll{[]build.Run{ - {"mkdir -p", []string{"/opt/lib"}}, - {"mkdir -p", []string{"/some/directory"}}, {"groupadd -o -g %s -r", []string{"777", "someuser"}}, {"useradd -o -m -d %s -r -g %s -u %s", []string{"/home/someuser", "someuser", "666", "someuser"}}, - {"chown %s:%s", []string{"someuser", "someuser", "/opt/lib"}}, - {"chown %s:%s", []string{"someuser", "someuser", "/some/directory"}}, }}}, cfg.InstructionsForPhase(build.PhasePrivileged), ) @@ -75,7 +58,6 @@ func TestRunsConfigInstructions(t *testing.T) { t.Run("PhasePrivilegeDropped", func(t *testing.T) { assert.Equal(t, []build.Instruction{ - build.Env{map[string]string{"HOME": "/home/someuser"}}, build.Env{map[string]string{"barname": "barvalue", "fooname": "foovalue"}}, }, cfg.InstructionsForPhase(build.PhasePrivilegeDropped), @@ -84,12 +66,7 @@ func TestRunsConfigInstructions(t *testing.T) { t.Run("with empty Environment", func(t *testing.T) { cfg.Environment = map[string]string{} - assert.Equal(t, - []build.Instruction{ - build.Env{map[string]string{"HOME": "/home/someuser"}}, - }, - cfg.InstructionsForPhase(build.PhasePrivilegeDropped), - ) + assert.Empty(t, cfg.InstructionsForPhase(build.PhasePrivilegeDropped)) }) }) @@ -103,59 +80,6 @@ func TestRunsConfigInstructions(t *testing.T) { } func TestRunsConfigValidation(t *testing.T) { - t.Run("in", func(t *testing.T) { - t.Run("ok", func(t *testing.T) { - _, err := config.ReadConfig([]byte(`--- - runs: - in: /foo`)) - - assert.False(t, config.IsValidationError(err)) - }) - - t.Run("optional", func(t *testing.T) { - _, err := config.ReadConfig([]byte(`--- - runs: {}`)) - - assert.False(t, config.IsValidationError(err)) - }) - - t.Run("non-root", func(t *testing.T) { - _, err := config.ReadConfig([]byte(`--- - runs: - in: /`)) - - if assert.True(t, config.IsValidationError(err)) { - msg := config.HumanizeValidationError(err) - - assert.Equal(t, `in: "/" is not a valid absolute non-root path`, msg) - } - }) - - t.Run("non-root tricky", func(t *testing.T) { - _, err := config.ReadConfig([]byte(`--- - runs: - in: /foo/..`)) - - if assert.True(t, config.IsValidationError(err)) { - msg := config.HumanizeValidationError(err) - - assert.Equal(t, `in: "/foo/.." is not a valid absolute non-root path`, msg) - } - }) - - t.Run("absolute", func(t *testing.T) { - _, err := config.ReadConfig([]byte(`--- - runs: - in: foo/bar`)) - - if assert.True(t, config.IsValidationError(err)) { - msg := config.HumanizeValidationError(err) - - assert.Equal(t, `in: "foo/bar" is not a valid absolute non-root path`, msg) - } - }) - }) - t.Run("as", func(t *testing.T) { t.Run("ok", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- diff --git a/config/user.go b/config/user.go new file mode 100644 index 0000000..f42fbf7 --- /dev/null +++ b/config/user.go @@ -0,0 +1,25 @@ +package config + +// UserConfig holds configuration fields related to a user account. +// +type UserConfig struct { + As string `yaml:"as" validate:"omitempty,username"` // user name + UID uint `yaml:"uid"` // user ID + GID uint `yaml:"gid"` // group ID +} + +// Merge takes another UserConfig and overwrites this struct's fields. +// +func (user *UserConfig) Merge(user2 UserConfig) { + if user2.As != "" { + user.As = user2.As + } + + if user2.UID != 0 { + user.UID = user2.UID + } + + if user2.GID != 0 { + user.GID = user2.GID + } +} diff --git a/config/variant.go b/config/variant.go index 2f7be21..0cb3f09 100644 --- a/config/variant.go +++ b/config/variant.go @@ -41,16 +41,43 @@ func (vc *VariantConfig) InstructionsForPhase(phase build.Phase) []build.Instruc } instructions = append(ainstructions, instructions...) + var switchUser string switch phase { + case build.PhasePrivileged: + switchUser = "root" + + case build.PhasePrivilegeDropped: + switchUser = vc.Lives.As + instructions = build.ApplyUser(vc.Lives.UID, vc.Lives.GID, instructions) + + case build.PhasePreInstall: + instructions = build.ApplyUser(vc.Lives.UID, vc.Lives.GID, instructions) + case build.PhaseInstall: if vc.Copies == "" { if vc.SharedVolume.True { - instructions = append(instructions, build.Volume{vc.Runs.In}) + instructions = append(instructions, build.Volume{vc.Lives.In}) } else { instructions = append(instructions, build.Copy{[]string{"."}, "."}) } } + + instructions = build.ApplyUser(vc.Lives.UID, vc.Lives.GID, instructions) + + case build.PhasePostInstall: + switchUser = vc.Runs.As + instructions = build.ApplyUser(vc.Runs.UID, vc.Runs.GID, instructions) + } + + if switchUser != "" { + instructions = append( + []build.Instruction{ + build.User{switchUser}, + build.Home(switchUser), + }, + instructions..., + ) } return instructions @@ -83,8 +110,8 @@ func (vc *VariantConfig) defaultArtifacts() []ArtifactsConfig { return []ArtifactsConfig{ { From: vc.Copies, - Source: vc.Runs.In, - Destination: vc.Runs.In, + Source: vc.Lives.In, + Destination: vc.Lives.In, }, { From: vc.Copies, diff --git a/config/variant_test.go b/config/variant_test.go index 3f58c39..6890f6d 100644 --- a/config/variant_test.go +++ b/config/variant_test.go @@ -75,7 +75,7 @@ func TestVariantConfigInstructions(t *testing.T) { t.Run("shared volume", func(t *testing.T) { cfg := config.VariantConfig{} - cfg.Runs.In = "/srv/service" + cfg.Lives.In = "/srv/service" cfg.SharedVolume.True = true assert.Equal(t, @@ -88,10 +88,12 @@ func TestVariantConfigInstructions(t *testing.T) { t.Run("standard source copy", func(t *testing.T) { cfg := config.VariantConfig{} + cfg.Lives.UID = 123 + cfg.Lives.GID = 223 assert.Equal(t, []build.Instruction{ - build.Copy{[]string{"."}, "."}, + build.CopyAs{123, 223, build.Copy{[]string{"."}, "."}}, }, cfg.InstructionsForPhase(build.PhaseInstall), ) @@ -105,7 +107,7 @@ func TestVariantConfigInstructions(t *testing.T) { Artifacts: []config.ArtifactsConfig{ {From: "build", Source: "/foo/src", Destination: "/foo/dst"}, }, - CommonConfig: config.CommonConfig{Runs: config.RunsConfig{In: "/srv/service"}}, + CommonConfig: config.CommonConfig{Lives: config.LivesConfig{In: "/srv/service"}}, } assert.Equal(t, @@ -123,7 +125,7 @@ func TestVariantConfigInstructions(t *testing.T) { Artifacts: []config.ArtifactsConfig{ {From: "build", Source: "/foo/src", Destination: "/foo/dst"}, }, - CommonConfig: config.CommonConfig{Runs: config.RunsConfig{In: "/srv/service"}}, + CommonConfig: config.CommonConfig{Lives: config.LivesConfig{In: "/srv/service"}}, } assert.Equal(t, |