diff --git a/integration_tests/dockerfiles/Dockerfile_test_copy b/integration_tests/dockerfiles/Dockerfile_test_copy index af4dc77d3..99c179c11 100644 --- a/integration_tests/dockerfiles/Dockerfile_test_copy +++ b/integration_tests/dockerfiles/Dockerfile_test_copy @@ -12,3 +12,9 @@ COPY ["context/foo", "/tmp/foo" ] COPY context/b* /baz/ COPY context/foo context/bar/ba? /test/ COPY context/arr[[]0].txt /mydir/ +COPY context/bar/bat . + +ENV contextenv ./context +COPY ${contextenv}/foo /tmp/foo2 +COPY $contextenv/foo /tmp/foo3 +COPY $contextenv/* /tmp/${contextenv}/ diff --git a/integration_tests/dockerfiles/Dockerfile_test_metadata b/integration_tests/dockerfiles/Dockerfile_test_metadata new file mode 100644 index 000000000..261db4d5b --- /dev/null +++ b/integration_tests/dockerfiles/Dockerfile_test_metadata @@ -0,0 +1,7 @@ +FROM gcr.io/distroless/base:latest +CMD ["command", "one"] +CMD ["command", "two"] +CMD echo "hello" + +ENTRYPOINT ["execute", "something"] +ENTRYPOINT ["execute", "entrypoint"] diff --git a/integration_tests/dockerfiles/Dockerfile_test_user_run b/integration_tests/dockerfiles/Dockerfile_test_user_run new file mode 100644 index 000000000..a71fb535e --- /dev/null +++ b/integration_tests/dockerfiles/Dockerfile_test_user_run @@ -0,0 +1,19 @@ +# Copyright 2018 Google, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM gcr.io/google-appengine/debian9 +RUN useradd testuser +RUN groupadd testgroup +USER testuser:testgroup +RUN echo "hey" > /tmp/foo diff --git a/integration_tests/dockerfiles/Dockerfile_test_workdir b/integration_tests/dockerfiles/Dockerfile_test_workdir new file mode 100644 index 000000000..6c89e4c11 --- /dev/null +++ b/integration_tests/dockerfiles/Dockerfile_test_workdir @@ -0,0 +1,13 @@ +FROM gcr.io/google-appengine/debian9:latest +COPY context/foo foo +WORKDIR /test +# Test that this will be appended on to the previous command, to create /test/workdir +WORKDIR workdir +COPY context/foo ./currentfoo +# Test that the RUN command will happen in the correct directory +RUN cp currentfoo newfoo +WORKDIR /new/dir +ENV dir /another/new/dir +WORKDIR $dir/newdir +WORKDIR $dir/$doesntexist +WORKDIR / diff --git a/integration_tests/dockerfiles/config_test_workdir.json b/integration_tests/dockerfiles/config_test_workdir.json new file mode 100644 index 000000000..c87503403 --- /dev/null +++ b/integration_tests/dockerfiles/config_test_workdir.json @@ -0,0 +1,12 @@ +[ + { + "Image1": "gcr.io/kbuild-test/docker-test-workdir:latest", + "Image2": "gcr.io/kbuild-test/kbuild-test-workdir:latest", + "DiffType": "File", + "Diff": { + "Adds": null, + "Dels": null, + "Mods": null + } + } +] \ No newline at end of file diff --git a/integration_tests/dockerfiles/test_metadata.yaml b/integration_tests/dockerfiles/test_metadata.yaml new file mode 100644 index 000000000..cb0024311 --- /dev/null +++ b/integration_tests/dockerfiles/test_metadata.yaml @@ -0,0 +1,4 @@ +schemaVersion: '2.0.0' +metadataTest: + cmd: ["/bin/sh", "-c", "echo \"hello\""] + entrypoint: ["execute", "entrypoint"] diff --git a/integration_tests/dockerfiles/test_user.yaml b/integration_tests/dockerfiles/test_user.yaml new file mode 100644 index 000000000..9a4bed1dc --- /dev/null +++ b/integration_tests/dockerfiles/test_user.yaml @@ -0,0 +1,15 @@ +schemaVersion: '2.0.0' +commandTests: +- name: 'whoami' + command: 'whoami' + expectedOutput: ['testuser'] + excludedOutput: ['root'] +- name: 'file owner' + command: 'ls' + args: ['-l', '/tmp/foo'] + expectedOutput: ['.*testuser.*', '.*testgroup.*'] + excludedOutput: ['.*root.*'] +fileContentTests: +- name: "/tmp/foo" + path: "/tmp/foo" + expectedContent: ["hey"] diff --git a/integration_tests/integration_test_yaml.go b/integration_tests/integration_test_yaml.go index a550deeee..72a045dda 100644 --- a/integration_tests/integration_test_yaml.go +++ b/integration_tests/integration_test_yaml.go @@ -87,6 +87,14 @@ var fileTests = []struct { kbuildContextBucket: true, repo: "test-bucket-buildcontext", }, + { + description: "test workdir", + dockerfilePath: "/workspace/integration_tests/dockerfiles/Dockerfile_test_workdir", + configPath: "/workspace/integration_tests/dockerfiles/config_test_workdir.json", + dockerContext: buildcontextPath, + kbuildContext: buildcontextPath, + repo: "test-workdir", + }, } var structureTests = []struct { @@ -105,6 +113,22 @@ var structureTests = []struct { kbuildContext: dockerfilesPath, structureTestYamlPath: "/workspace/integration_tests/dockerfiles/test_env.yaml", }, + { + description: "test metadata", + dockerfilePath: "/workspace/integration_tests/dockerfiles/Dockerfile_test_metadata", + repo: "test-metadata", + dockerBuildContext: dockerfilesPath, + kbuildContext: dockerfilesPath, + structureTestYamlPath: "/workspace/integration_tests/dockerfiles/test_metadata.yaml", + }, + { + description: "test user command", + dockerfilePath: "/workspace/integration_tests/dockerfiles/Dockerfile_test_user_run", + repo: "test-user", + dockerBuildContext: dockerfilesPath, + kbuildContext: dockerfilesPath, + structureTestYamlPath: "/workspace/integration_tests/dockerfiles/test_user.yaml", + }, } type step struct { diff --git a/pkg/commands/cmd.go b/pkg/commands/cmd.go new file mode 100644 index 000000000..4d49234b2 --- /dev/null +++ b/pkg/commands/cmd.go @@ -0,0 +1,65 @@ +/* +Copyright 2018 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "github.com/containers/image/manifest" + "github.com/docker/docker/builder/dockerfile/instructions" + "github.com/sirupsen/logrus" + "strings" +) + +type CmdCommand struct { + cmd *instructions.CmdCommand +} + +// ExecuteCommand executes the CMD command +// Argument handling is the same as RUN. +func (c *CmdCommand) ExecuteCommand(config *manifest.Schema2Config) error { + logrus.Info("cmd: CMD") + var newCommand []string + if c.cmd.PrependShell { + // This is the default shell on Linux + // TODO: Support shell command here + shell := []string{"/bin/sh", "-c"} + newCommand = append(shell, strings.Join(c.cmd.CmdLine, " ")) + } else { + newCommand = c.cmd.CmdLine + } + + logrus.Infof("Replacing CMD in config with %v", newCommand) + config.Cmd = newCommand + return nil +} + +// FilesToSnapshot returns an empty array since this is a metadata command +func (c *CmdCommand) FilesToSnapshot() []string { + return []string{} +} + +// CreatedBy returns some information about the command for the image config history +func (c *CmdCommand) CreatedBy() string { + cmd := []string{"CMD"} + cmdLine := strings.Join(c.cmd.CmdLine, " ") + if c.cmd.PrependShell { + // TODO: Support shell command here + shell := []string{"/bin/sh", "-c"} + appendedShell := append(cmd, shell...) + return strings.Join(append(appendedShell, cmdLine), " ") + } + return strings.Join(append(cmd, cmdLine), " ") +} diff --git a/pkg/commands/cmd_test.go b/pkg/commands/cmd_test.go new file mode 100644 index 000000000..3a966da11 --- /dev/null +++ b/pkg/commands/cmd_test.go @@ -0,0 +1,61 @@ +/* +Copyright 2018 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package commands + +import ( + "github.com/GoogleCloudPlatform/k8s-container-builder/testutil" + "github.com/containers/image/manifest" + "github.com/containers/image/pkg/strslice" + "github.com/docker/docker/builder/dockerfile/instructions" + "testing" +) + +var cmdTests = []struct { + prependShell bool + cmdLine []string + expectedCmd strslice.StrSlice +}{ + { + prependShell: true, + cmdLine: []string{"echo", "cmd1"}, + expectedCmd: strslice.StrSlice{"/bin/sh", "-c", "echo cmd1"}, + }, + { + prependShell: false, + cmdLine: []string{"echo", "cmd2"}, + expectedCmd: strslice.StrSlice{"echo", "cmd2"}, + }, +} + +func TestExecuteCmd(t *testing.T) { + + cfg := &manifest.Schema2Config{ + Cmd: nil, + } + + for _, test := range cmdTests { + cmd := CmdCommand{ + &instructions.CmdCommand{ + ShellDependantCmdLine: instructions.ShellDependantCmdLine{ + PrependShell: test.prependShell, + CmdLine: test.cmdLine, + }, + }, + } + err := cmd.ExecuteCommand(cfg) + testutil.CheckErrorAndDeepEqual(t, false, err, test.expectedCmd, cfg.Cmd) + } +} diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index 8f6b6a8cc..0376d225a 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -44,6 +44,16 @@ func GetCommand(cmd instructions.Command, buildcontext string) (DockerCommand, e return &ExposeCommand{cmd: c}, nil case *instructions.EnvCommand: return &EnvCommand{cmd: c}, nil + case *instructions.WorkdirCommand: + return &WorkdirCommand{cmd: c}, nil + case *instructions.CmdCommand: + return &CmdCommand{cmd: c}, nil + case *instructions.EntrypointCommand: + return &EntrypointCommand{cmd: c}, nil + case *instructions.LabelCommand: + return &LabelCommand{cmd: c}, nil + case *instructions.UserCommand: + return &UserCommand{cmd: c}, nil } return nil, errors.Errorf("%s is not a supported command", cmd.Name()) } diff --git a/pkg/commands/copy.go b/pkg/commands/copy.go index 96fded65d..e9f8cab59 100644 --- a/pkg/commands/copy.go +++ b/pkg/commands/copy.go @@ -39,8 +39,14 @@ func (c *CopyCommand) ExecuteCommand(config *manifest.Schema2Config) error { logrus.Infof("cmd: copy %s", srcs) logrus.Infof("dest: %s", dest) + // First, resolve any environment replacement + resolvedEnvs, err := util.ResolveEnvironmentReplacementList(c.cmd.SourcesAndDest, config.Env, true) + if err != nil { + return err + } + dest = resolvedEnvs[len(resolvedEnvs)-1] // Get a map of [src]:[files rooted at src] - srcMap, err := util.ResolveSources(c.cmd.SourcesAndDest, c.buildcontext) + srcMap, err := util.ResolveSources(resolvedEnvs, c.buildcontext) if err != nil { return err } diff --git a/pkg/commands/entrypoint.go b/pkg/commands/entrypoint.go new file mode 100644 index 000000000..a7fd5f3ed --- /dev/null +++ b/pkg/commands/entrypoint.go @@ -0,0 +1,64 @@ +/* +Copyright 2018 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "github.com/containers/image/manifest" + "github.com/docker/docker/builder/dockerfile/instructions" + "github.com/sirupsen/logrus" + "strings" +) + +type EntrypointCommand struct { + cmd *instructions.EntrypointCommand +} + +// ExecuteCommand handles command processing similar to CMD and RUN, +func (e *EntrypointCommand) ExecuteCommand(config *manifest.Schema2Config) error { + logrus.Info("cmd: ENTRYPOINT") + var newCommand []string + if e.cmd.PrependShell { + // This is the default shell on Linux + // TODO: Support shell command here + shell := []string{"/bin/sh", "-c"} + newCommand = append(shell, strings.Join(e.cmd.CmdLine, " ")) + } else { + newCommand = e.cmd.CmdLine + } + + logrus.Infof("Replacing Entrypoint in config with %v", newCommand) + config.Entrypoint = newCommand + return nil +} + +// FilesToSnapshot returns an empty array since this is a metadata command +func (e *EntrypointCommand) FilesToSnapshot() []string { + return []string{} +} + +// CreatedBy returns some information about the command for the image config history +func (e *EntrypointCommand) CreatedBy() string { + entrypoint := []string{"ENTRYPOINT"} + cmdLine := strings.Join(e.cmd.CmdLine, " ") + if e.cmd.PrependShell { + // TODO: Support shell command here + shell := []string{"/bin/sh", "-c"} + appendedShell := append(entrypoint, shell...) + return strings.Join(append(appendedShell, cmdLine), " ") + } + return strings.Join(append(entrypoint, cmdLine), " ") +} diff --git a/pkg/commands/entrypoint_test.go b/pkg/commands/entrypoint_test.go new file mode 100644 index 000000000..0835de07e --- /dev/null +++ b/pkg/commands/entrypoint_test.go @@ -0,0 +1,61 @@ +/* +Copyright 2018 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package commands + +import ( + "github.com/GoogleCloudPlatform/k8s-container-builder/testutil" + "github.com/containers/image/manifest" + "github.com/containers/image/pkg/strslice" + "github.com/docker/docker/builder/dockerfile/instructions" + "testing" +) + +var entrypointTests = []struct { + prependShell bool + cmdLine []string + expectedCmd strslice.StrSlice +}{ + { + prependShell: true, + cmdLine: []string{"echo", "cmd1"}, + expectedCmd: strslice.StrSlice{"/bin/sh", "-c", "echo cmd1"}, + }, + { + prependShell: false, + cmdLine: []string{"echo", "cmd2"}, + expectedCmd: strslice.StrSlice{"echo", "cmd2"}, + }, +} + +func TestEntrypointExecuteCmd(t *testing.T) { + + cfg := &manifest.Schema2Config{ + Cmd: nil, + } + + for _, test := range entrypointTests { + cmd := EntrypointCommand{ + &instructions.EntrypointCommand{ + ShellDependantCmdLine: instructions.ShellDependantCmdLine{ + PrependShell: test.prependShell, + CmdLine: test.cmdLine, + }, + }, + } + err := cmd.ExecuteCommand(cfg) + testutil.CheckErrorAndDeepEqual(t, false, err, test.expectedCmd, cfg.Entrypoint) + } +} diff --git a/pkg/commands/env.go b/pkg/commands/env.go index f7f004940..94e1edabe 100644 --- a/pkg/commands/env.go +++ b/pkg/commands/env.go @@ -17,11 +17,9 @@ limitations under the License. package commands import ( - "bytes" + "github.com/GoogleCloudPlatform/k8s-container-builder/pkg/util" "github.com/containers/image/manifest" "github.com/docker/docker/builder/dockerfile/instructions" - "github.com/docker/docker/builder/dockerfile/parser" - "github.com/docker/docker/builder/dockerfile/shell" "github.com/sirupsen/logrus" "os" "strings" @@ -33,26 +31,18 @@ type EnvCommand struct { func (e *EnvCommand) ExecuteCommand(config *manifest.Schema2Config) error { logrus.Info("cmd: ENV") - // The dockerfile/shell package handles processing env values - // It handles escape characters and supports expansion from the config.Env array - // Shlex handles some of the following use cases (these and more are tested in integration tests) - // ""a'b'c"" -> "a'b'c" - // "Rex\ The\ Dog \" -> "Rex The Dog" - // "a\"b" -> "a"b" - envString := envToString(e.cmd) - p, err := parser.Parse(bytes.NewReader([]byte(envString))) - if err != nil { - return err - } - shlex := shell.NewLex(p.EscapeToken) newEnvs := e.cmd.Env for index, pair := range newEnvs { - expandedValue, err := shlex.ProcessWord(pair.Value, config.Env) + expandedKey, err := util.ResolveEnvironmentReplacement(pair.Key, config.Env, false) + if err != nil { + return err + } + expandedValue, err := util.ResolveEnvironmentReplacement(pair.Value, config.Env, false) if err != nil { return err } newEnvs[index] = instructions.KeyValuePair{ - Key: pair.Key, + Key: expandedKey, Value: expandedValue, } logrus.Infof("Setting environment variable %s=%s", pair.Key, expandedValue) @@ -98,14 +88,6 @@ Loop: return nil } -func envToString(cmd *instructions.EnvCommand) string { - env := []string{"ENV"} - for _, kvp := range cmd.Env { - env = append(env, kvp.Key+"="+kvp.Value) - } - return strings.Join(env, " ") -} - // We know that no files have changed, so return an empty array func (e *EnvCommand) FilesToSnapshot() []string { return []string{} diff --git a/pkg/commands/env_test.go b/pkg/commands/env_test.go index f0cccdaa4..5aedcff02 100644 --- a/pkg/commands/env_test.go +++ b/pkg/commands/env_test.go @@ -53,21 +53,39 @@ func TestUpdateEnvConfig(t *testing.T) { updateConfigEnv(newEnvs, cfg) testutil.CheckErrorAndDeepEqual(t, false, nil, expectedEnvArray, cfg.Env) } +func Test_EnvExecute(t *testing.T) { + cfg := &manifest.Schema2Config{ + Env: []string{ + "path=/usr/", + "home=/root", + }, + } -func TestEnvToString(t *testing.T) { - envCmd := &instructions.EnvCommand{ - Env: []instructions.KeyValuePair{ - { - Key: "PATH", - Value: "/some/path", - }, - { - Key: "HOME", - Value: "/root", + envCmd := &EnvCommand{ + &instructions.EnvCommand{ + Env: []instructions.KeyValuePair{ + { + Key: "path", + Value: "/some/path", + }, + { + Key: "HOME", + Value: "$home", + }, + { + Key: "$path", + Value: "$home/", + }, }, }, } - expectedString := "ENV PATH=/some/path HOME=/root" - actualString := envToString(envCmd) - testutil.CheckErrorAndDeepEqual(t, false, nil, expectedString, actualString) + + expectedEnvs := []string{ + "path=/some/path", + "home=/root", + "HOME=/root", + "/usr/=/root/", + } + err := envCmd.ExecuteCommand(cfg) + testutil.CheckErrorAndDeepEqual(t, false, err, expectedEnvs, cfg.Env) } diff --git a/pkg/commands/expose.go b/pkg/commands/expose.go index daf4dc781..fc9d6fe75 100644 --- a/pkg/commands/expose.go +++ b/pkg/commands/expose.go @@ -18,6 +18,7 @@ package commands import ( "fmt" + "github.com/GoogleCloudPlatform/k8s-container-builder/pkg/util" "github.com/containers/image/manifest" "github.com/docker/docker/builder/dockerfile/instructions" "github.com/sirupsen/logrus" @@ -29,25 +30,16 @@ type ExposeCommand struct { } func (r *ExposeCommand) ExecuteCommand(config *manifest.Schema2Config) error { - return updateExposedPorts(r.cmd.Ports, config) -} - -func validProtocol(protocol string) bool { - validProtocols := [2]string{"tcp", "udp"} - for _, p := range validProtocols { - if protocol == p { - return true - } - } - return false -} - -func updateExposedPorts(ports []string, config *manifest.Schema2Config) error { + logrus.Info("cmd: EXPOSE") // Grab the currently exposed ports existingPorts := config.ExposedPorts - // Add any new ones in - for _, p := range ports { + for _, p := range r.cmd.Ports { + // Resolve any environment variables + p, err := util.ResolveEnvironmentReplacement(p, config.Env, false) + if err != nil { + return err + } // Add the default protocol if one isn't specified if !strings.Contains(p, "/") { p = p + "/tcp" @@ -64,11 +56,21 @@ func updateExposedPorts(ports []string, config *manifest.Schema2Config) error { return nil } +func validProtocol(protocol string) bool { + validProtocols := [2]string{"tcp", "udp"} + for _, p := range validProtocols { + if protocol == p { + return true + } + } + return false +} + func (r *ExposeCommand) FilesToSnapshot() []string { return []string{} } func (r *ExposeCommand) CreatedBy() string { - s := []string{"/bin/sh", "-c"} + s := []string{r.cmd.Name()} return strings.Join(append(s, r.cmd.Ports...), " ") } diff --git a/pkg/commands/expose_test.go b/pkg/commands/expose_test.go index fc7fb2c0e..ad23cca25 100644 --- a/pkg/commands/expose_test.go +++ b/pkg/commands/expose_test.go @@ -19,6 +19,7 @@ package commands import ( "github.com/GoogleCloudPlatform/k8s-container-builder/testutil" "github.com/containers/image/manifest" + "github.com/docker/docker/builder/dockerfile/instructions" "testing" ) @@ -27,6 +28,10 @@ func TestUpdateExposedPorts(t *testing.T) { ExposedPorts: manifest.Schema2PortSet{ "8080/tcp": {}, }, + Env: []string{ + "port=udp", + "num=8085", + }, } ports := []string{ @@ -34,6 +39,15 @@ func TestUpdateExposedPorts(t *testing.T) { "8081/tcp", "8082", "8083/udp", + "8084/$port", + "$num", + "$num/$port", + } + + exposeCmd := &ExposeCommand{ + &instructions.ExposeCommand{ + Ports: ports, + }, } expectedPorts := manifest.Schema2PortSet{ @@ -41,9 +55,12 @@ func TestUpdateExposedPorts(t *testing.T) { "8081/tcp": {}, "8082/tcp": {}, "8083/udp": {}, + "8084/udp": {}, + "8085/tcp": {}, + "8085/udp": {}, } - err := updateExposedPorts(ports, cfg) + err := exposeCmd.ExecuteCommand(cfg) testutil.CheckErrorAndDeepEqual(t, false, err, expectedPorts, cfg.ExposedPorts) } @@ -56,6 +73,12 @@ func TestInvalidProtocol(t *testing.T) { "80/garbage", } - err := updateExposedPorts(ports, cfg) + exposeCmd := &ExposeCommand{ + &instructions.ExposeCommand{ + Ports: ports, + }, + } + + err := exposeCmd.ExecuteCommand(cfg) testutil.CheckErrorAndDeepEqual(t, true, err, nil, nil) } diff --git a/pkg/commands/label.go b/pkg/commands/label.go new file mode 100644 index 000000000..81b9bab56 --- /dev/null +++ b/pkg/commands/label.go @@ -0,0 +1,72 @@ +/* +Copyright 2018 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "github.com/GoogleCloudPlatform/k8s-container-builder/pkg/util" + "github.com/containers/image/manifest" + "github.com/docker/docker/builder/dockerfile/instructions" + "github.com/sirupsen/logrus" + "strings" +) + +type LabelCommand struct { + cmd *instructions.LabelCommand +} + +func (r *LabelCommand) ExecuteCommand(config *manifest.Schema2Config) error { + logrus.Info("cmd: LABEL") + return updateLabels(r.cmd.Labels, config) +} + +func updateLabels(labels []instructions.KeyValuePair, config *manifest.Schema2Config) error { + existingLabels := config.Labels + + // Let's unescape values before setting the label + for index, kvp := range labels { + unescaped, err := util.ResolveEnvironmentReplacement(kvp.Value, []string{}, false) + if err != nil { + return err + } + labels[index] = instructions.KeyValuePair{ + Key: kvp.Key, + Value: unescaped, + } + } + for _, kvp := range labels { + logrus.Infof("Applying label %s=%s", kvp.Key, kvp.Value) + existingLabels[kvp.Key] = kvp.Value + } + + config.Labels = existingLabels + return nil + +} + +// No files have changed, this command only touches metadata. +func (r *LabelCommand) FilesToSnapshot() []string { + return []string{} +} + +// CreatedBy returns some information about the command for the image config history +func (r *LabelCommand) CreatedBy() string { + l := []string{r.cmd.Name()} + for _, kvp := range r.cmd.Labels { + l = append(l, kvp.String()) + } + return strings.Join(l, " ") +} diff --git a/pkg/commands/label_test.go b/pkg/commands/label_test.go new file mode 100644 index 000000000..f997611c3 --- /dev/null +++ b/pkg/commands/label_test.go @@ -0,0 +1,60 @@ +/* +Copyright 2018 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "github.com/GoogleCloudPlatform/k8s-container-builder/testutil" + "github.com/containers/image/manifest" + "github.com/docker/docker/builder/dockerfile/instructions" + "testing" +) + +func TestUpdateLabels(t *testing.T) { + cfg := &manifest.Schema2Config{ + Labels: map[string]string{ + "foo": "bar", + }, + } + + labels := []instructions.KeyValuePair{ + { + Key: "foo", + Value: "override", + }, + { + Key: "bar", + Value: "baz", + }, + { + Key: "multiword", + Value: "lots\\ of\\ words", + }, + { + Key: "backslashes", + Value: "lots\\\\ of\\\\ words", + }, + } + + expectedLabels := map[string]string{ + "foo": "override", + "bar": "baz", + "multiword": "lots of words", + "backslashes": "lots\\ of\\ words", + } + updateLabels(labels, cfg) + testutil.CheckErrorAndDeepEqual(t, false, nil, expectedLabels, cfg.Labels) +} diff --git a/pkg/commands/run.go b/pkg/commands/run.go index b08cf8800..6be8f33fe 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -22,7 +22,9 @@ import ( "github.com/sirupsen/logrus" "os" "os/exec" + "strconv" "strings" + "syscall" ) type RunCommand struct { @@ -44,7 +46,28 @@ func (r *RunCommand) ExecuteCommand(config *manifest.Schema2Config) error { logrus.Infof("args: %s", newCommand[1:]) cmd := exec.Command(newCommand[0], newCommand[1:]...) + cmd.Dir = config.WorkingDir cmd.Stdout = os.Stdout + // If specified, run the command as a specific user + if config.User != "" { + userAndGroup := strings.Split(config.User, ":") + // uid and gid need to be uint32 + uid64, err := strconv.ParseUint(userAndGroup[0], 10, 32) + if err != nil { + return err + } + uid := uint32(uid64) + var gid uint32 + if len(userAndGroup) > 1 { + gid64, err := strconv.ParseUint(userAndGroup[1], 10, 32) + if err != nil { + return err + } + gid = uint32(gid64) + } + cmd.SysProcAttr = &syscall.SysProcAttr{} + cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid} + } return cmd.Run() } diff --git a/pkg/commands/user.go b/pkg/commands/user.go new file mode 100644 index 000000000..d2e4cff61 --- /dev/null +++ b/pkg/commands/user.go @@ -0,0 +1,95 @@ +/* +Copyright 2018 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "github.com/GoogleCloudPlatform/k8s-container-builder/pkg/util" + "github.com/containers/image/manifest" + "github.com/docker/docker/builder/dockerfile/instructions" + "github.com/sirupsen/logrus" + "os/user" + "strings" +) + +type UserCommand struct { + cmd *instructions.UserCommand +} + +func (r *UserCommand) ExecuteCommand(config *manifest.Schema2Config) error { + logrus.Info("cmd: USER") + u := r.cmd.User + userAndGroup := strings.Split(u, ":") + userStr, err := util.ResolveEnvironmentReplacement(userAndGroup[0], config.Env, false) + if err != nil { + return err + } + var groupStr string + if len(userAndGroup) > 1 { + groupStr, err = util.ResolveEnvironmentReplacement(userAndGroup[1], config.Env, false) + if err != nil { + return err + } + } + + // Lookup by username + userObj, err := user.Lookup(userStr) + if err != nil { + if _, ok := err.(user.UnknownUserError); ok { + // Lookup by id + userObj, err = user.LookupId(userStr) + if err != nil { + return err + } + } else { + return err + } + } + + // Same dance with groups + var group *user.Group + if groupStr != "" { + group, err = user.LookupGroup(groupStr) + if err != nil { + if _, ok := err.(user.UnknownGroupError); ok { + group, err = user.LookupGroupId(groupStr) + if err != nil { + return err + } + } else { + return err + } + } + } + + uid := userObj.Uid + if group != nil { + uid = uid + ":" + group.Gid + } + + logrus.Infof("Setting user to %s", uid) + config.User = uid + return nil +} + +func (r *UserCommand) FilesToSnapshot() []string { + return []string{} +} + +func (r *UserCommand) CreatedBy() string { + s := []string{r.cmd.Name(), r.cmd.User} + return strings.Join(s, " ") +} diff --git a/pkg/commands/user_test.go b/pkg/commands/user_test.go new file mode 100644 index 000000000..c1ebe0ab2 --- /dev/null +++ b/pkg/commands/user_test.go @@ -0,0 +1,98 @@ +/* +Copyright 2018 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package commands + +import ( + "github.com/GoogleCloudPlatform/k8s-container-builder/testutil" + "github.com/containers/image/manifest" + "github.com/docker/docker/builder/dockerfile/instructions" + "testing" +) + +var userTests = []struct { + user string + expectedUid string + shouldError bool +}{ + { + user: "root", + expectedUid: "0", + shouldError: false, + }, + { + user: "0", + expectedUid: "0", + shouldError: false, + }, + { + user: "fakeUser", + expectedUid: "", + shouldError: true, + }, + { + user: "root:root", + expectedUid: "0:0", + shouldError: false, + }, + { + user: "0:root", + expectedUid: "0:0", + shouldError: false, + }, + { + user: "root:0", + expectedUid: "0:0", + shouldError: false, + }, + { + user: "0:0", + expectedUid: "0:0", + shouldError: false, + }, + { + user: "root:fakeGroup", + expectedUid: "", + shouldError: true, + }, + { + user: "$envuser", + expectedUid: "0", + shouldError: false, + }, + { + user: "root:$envgroup", + expectedUid: "0:0", + shouldError: false, + }, +} + +func TestUpdateUser(t *testing.T) { + for _, test := range userTests { + cfg := &manifest.Schema2Config{ + Env: []string{ + "envuser=root", + "envgroup=root", + }, + } + cmd := UserCommand{ + &instructions.UserCommand{ + User: test.user, + }, + } + err := cmd.ExecuteCommand(cfg) + testutil.CheckErrorAndDeepEqual(t, test.shouldError, err, test.expectedUid, cfg.User) + } +} diff --git a/pkg/commands/workdir.go b/pkg/commands/workdir.go new file mode 100644 index 000000000..f249608ac --- /dev/null +++ b/pkg/commands/workdir.go @@ -0,0 +1,58 @@ +/* +Copyright 2018 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "github.com/GoogleCloudPlatform/k8s-container-builder/pkg/util" + "github.com/containers/image/manifest" + "github.com/docker/docker/builder/dockerfile/instructions" + "github.com/sirupsen/logrus" + "os" + "path/filepath" +) + +type WorkdirCommand struct { + cmd *instructions.WorkdirCommand + snapshotFiles []string +} + +func (w *WorkdirCommand) ExecuteCommand(config *manifest.Schema2Config) error { + logrus.Info("cmd: workdir") + workdirPath := w.cmd.Path + resolvedWorkingDir, err := util.ResolveEnvironmentReplacement(workdirPath, config.Env, true) + if err != nil { + return err + } + if filepath.IsAbs(resolvedWorkingDir) { + config.WorkingDir = resolvedWorkingDir + } else { + config.WorkingDir = filepath.Join(config.WorkingDir, resolvedWorkingDir) + } + logrus.Infof("Changed working directory to %s", config.WorkingDir) + w.snapshotFiles = []string{config.WorkingDir} + return os.MkdirAll(config.WorkingDir, 0755) +} + +// FilesToSnapshot returns the workingdir, which should have been created if it didn't already exist +func (w *WorkdirCommand) FilesToSnapshot() []string { + return w.snapshotFiles +} + +// CreatedBy returns some information about the command for the image config history +func (w *WorkdirCommand) CreatedBy() string { + return w.cmd.Name() + " " + w.cmd.Path +} diff --git a/pkg/commands/workdir_test.go b/pkg/commands/workdir_test.go new file mode 100644 index 000000000..439d77fd5 --- /dev/null +++ b/pkg/commands/workdir_test.go @@ -0,0 +1,83 @@ +/* +Copyright 2018 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package commands + +import ( + "github.com/GoogleCloudPlatform/k8s-container-builder/testutil" + "github.com/containers/image/manifest" + "github.com/docker/docker/builder/dockerfile/instructions" + "testing" +) + +// Each test here changes the same WorkingDir field in the config +// So, some of the tests build off of each other +// This is needed to make sure WorkingDir handles paths correctly +// For example, if WORKDIR specifies a non-absolute path, it should be appended to the current WORKDIR +var workdirTests = []struct { + path string + expectedPath string +}{ + { + path: "/a", + expectedPath: "/a", + }, + { + path: "b", + expectedPath: "/a/b", + }, + { + path: "c", + expectedPath: "/a/b/c", + }, + { + path: "/d", + expectedPath: "/d", + }, + { + path: "$path", + expectedPath: "/d/usr", + }, + { + path: "$home", + expectedPath: "/root", + }, + { + path: "$path/$home", + expectedPath: "/root/usr/root", + }, +} + +func TestWorkdirCommand(t *testing.T) { + + cfg := &manifest.Schema2Config{ + WorkingDir: "/", + Env: []string{ + "path=usr/", + "home=/root", + }, + } + + for _, test := range workdirTests { + cmd := WorkdirCommand{ + cmd: &instructions.WorkdirCommand{ + Path: test.path, + }, + snapshotFiles: []string{}, + } + cmd.ExecuteCommand(cfg) + testutil.CheckErrorAndDeepEqual(t, false, nil, test.expectedPath, cfg.WorkingDir) + } +} diff --git a/pkg/util/command_util.go b/pkg/util/command_util.go index 38521231a..0f6472f79 100644 --- a/pkg/util/command_util.go +++ b/pkg/util/command_util.go @@ -18,6 +18,8 @@ package util import ( "github.com/docker/docker/builder/dockerfile/instructions" + "github.com/docker/docker/builder/dockerfile/parser" + "github.com/docker/docker/builder/dockerfile/shell" "github.com/pkg/errors" "github.com/sirupsen/logrus" "os" @@ -25,6 +27,45 @@ import ( "strings" ) +// ResolveEnvironmentReplacement resolves a list of values by calling resolveEnvironmentReplacement +func ResolveEnvironmentReplacementList(values, envs []string, isFilepath bool) ([]string, error) { + var resolvedValues []string + for _, value := range values { + resolved, err := ResolveEnvironmentReplacement(value, envs, isFilepath) + logrus.Debugf("Resolved %s to %s", value, resolved) + if err != nil { + return nil, err + } + resolvedValues = append(resolvedValues, resolved) + } + return resolvedValues, nil +} + +// ResolveEnvironmentReplacement resolves replacing env variables in some text from envs +// It takes in a string representation of the command, the value to be resolved, and a list of envs (config.Env) +// Ex: fp = $foo/newdir, envs = [foo=/foodir], then this should return /foodir/newdir +// The dockerfile/shell package handles processing env values +// It handles escape characters and supports expansion from the config.Env array +// Shlex handles some of the following use cases (these and more are tested in integration tests) +// ""a'b'c"" -> "a'b'c" +// "Rex\ The\ Dog \" -> "Rex The Dog" +// "a\"b" -> "a"b" +func ResolveEnvironmentReplacement(value string, envs []string, isFilepath bool) (string, error) { + shlex := shell.NewLex(parser.DefaultEscapeToken) + fp, err := shlex.ProcessWord(value, envs) + if !isFilepath { + return fp, err + } + if err != nil { + return "", err + } + fp = filepath.Clean(fp) + if IsDestDir(value) { + fp = fp + "/" + } + return fp, nil +} + // ContainsWildcards returns true if any entry in paths contains wildcards func ContainsWildcards(paths []string) bool { for _, path := range paths { diff --git a/pkg/util/command_util_test.go b/pkg/util/command_util_test.go index 2d96f8d2a..611ad7b81 100644 --- a/pkg/util/command_util_test.go +++ b/pkg/util/command_util_test.go @@ -22,6 +22,88 @@ import ( "testing" ) +var testEnvReplacement = []struct { + path string + command string + envs []string + isFilepath bool + expectedPath string +}{ + { + path: "/simple/path", + command: "WORKDIR /simple/path", + envs: []string{ + "simple=/path/", + }, + isFilepath: true, + expectedPath: "/simple/path", + }, + { + path: "/simple/path/", + command: "WORKDIR /simple/path/", + envs: []string{ + "simple=/path/", + }, + isFilepath: true, + expectedPath: "/simple/path/", + }, + { + path: "${a}/b", + command: "WORKDIR ${a}/b", + envs: []string{ + "a=/path/", + "b=/path2/", + }, + isFilepath: true, + expectedPath: "/path/b", + }, + { + path: "/$a/b", + command: "COPY ${a}/b /c/", + envs: []string{ + "a=/path/", + "b=/path2/", + }, + isFilepath: true, + expectedPath: "/path/b", + }, + { + path: "/$a/b/", + command: "COPY /${a}/b /c/", + envs: []string{ + "a=/path/", + "b=/path2/", + }, + isFilepath: true, + expectedPath: "/path/b/", + }, + { + path: "\\$foo", + command: "COPY \\$foo /quux", + envs: []string{ + "foo=/path/", + }, + isFilepath: true, + expectedPath: "$foo", + }, + { + path: "8080/$protocol", + command: "EXPOSE 8080/$protocol", + envs: []string{ + "protocol=udp", + }, + expectedPath: "8080/udp", + }, +} + +func Test_EnvReplacement(t *testing.T) { + for _, test := range testEnvReplacement { + actualPath, err := ResolveEnvironmentReplacement(test.path, test.envs, test.isFilepath) + testutil.CheckErrorAndDeepEqual(t, false, err, test.expectedPath, actualPath) + + } +} + var buildContextPath = "../../integration_tests/" var destinationFilepathTests = []struct {