diff --git a/debian/postinst.sh b/debian/postinst.sh index b387b9f22..f9dbb3ee3 100755 --- a/debian/postinst.sh +++ b/debian/postinst.sh @@ -20,7 +20,7 @@ fi # Update docker configuration. if [ -f /etc/docker/daemon.json ]; then - runsc install + runsc install --clobber=false if systemctl is-active -q docker; then systemctl reload docker || echo "unable to reload docker; you must do so manually." >&2 fi diff --git a/runsc/cmd/BUILD b/runsc/cmd/BUILD index aedf1947c..c55ffb9bc 100644 --- a/runsc/cmd/BUILD +++ b/runsc/cmd/BUILD @@ -82,6 +82,7 @@ go_test( "delete_test.go", "exec_test.go", "gofer_test.go", + "install_test.go", "mitigate_test.go", ], data = [ diff --git a/runsc/cmd/install.go b/runsc/cmd/install.go index 757b829d8..0856776a8 100644 --- a/runsc/cmd/install.go +++ b/runsc/cmd/install.go @@ -22,6 +22,7 @@ import ( "log" "os" "path" + "regexp" "github.com/google/subcommands" "gvisor.dev/gvisor/pkg/sentry/platform" @@ -31,10 +32,13 @@ import ( // Install implements subcommands.Command. type Install struct { - ConfigFile string - Runtime string - Experimental bool - CgroupDriver string + ConfigFile string + Runtime string + Experimental bool + Clobber bool + CgroupDriver string + executablePath string + runtimeArgs []string } // Name implements subcommands.Command.Name. @@ -57,17 +61,18 @@ func (*Install) Usage() string { func (i *Install) SetFlags(fs *flag.FlagSet) { fs.StringVar(&i.ConfigFile, "config_file", "/etc/docker/daemon.json", "path to Docker daemon config file") fs.StringVar(&i.Runtime, "runtime", "runsc", "runtime name") - fs.BoolVar(&i.Experimental, "experimental", false, "enable experimental features") + fs.BoolVar(&i.Experimental, "experimental", false, "enable/disable experimental features") + fs.BoolVar(&i.Clobber, "clobber", true, "clobber existing runtime configuration") fs.StringVar(&i.CgroupDriver, "cgroupdriver", "", "docker cgroup driver") } // Execute implements subcommands.Command.Execute. func (i *Install) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { // Grab the name and arguments. - runtimeArgs := f.Args() + i.runtimeArgs = f.Args() testFlags := flag.NewFlagSet("test", flag.ContinueOnError) config.RegisterFlags(testFlags) - testFlags.Parse(runtimeArgs) + testFlags.Parse(i.runtimeArgs) conf, err := config.NewFromFlags(testFlags) if err != nil { log.Fatalf("invalid runtime arguments: %v", err) @@ -92,10 +97,34 @@ func (i *Install) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) log.Fatalf("Error reading current exectuable: %v", err) } + i.executablePath = path + + installRW := configReaderWriter{ + read: defaultReadConfig, + write: defaultWriteConfig, + } + + if err := doInstallConfig(i, installRW); err != nil { + log.Fatalf("Install failed: %v", err) + } + + // Success. + log.Print("Successfully updated config.") + return subcommands.ExitSuccess +} + +func doInstallConfig(i *Install, rw configReaderWriter) error { // Load the configuration file. - c, err := readConfig(i.ConfigFile) + configBytes, err := rw.read(i.ConfigFile) if err != nil { - log.Fatalf("Error reading config file %q: %v", i.ConfigFile, err) + return fmt.Errorf("error reading config file %q: %v", i.ConfigFile, err) + } + // Unmarshal the configuration. + c := make(map[string]interface{}) + if len(configBytes) > 0 { + if err := json.Unmarshal(configBytes, &c); err != nil { + return err + } } // Add the given runtime. @@ -106,12 +135,25 @@ func (i *Install) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) rts = make(map[string]interface{}) c["runtimes"] = rts } - rts[i.Runtime] = struct { - Path string `json:"path,omitempty"` - RuntimeArgs []string `json:"runtimeArgs,omitempty"` - }{ - Path: path, - RuntimeArgs: runtimeArgs, + updateRuntime := func() { + rts[i.Runtime] = struct { + Path string `json:"path,omitempty"` + RuntimeArgs []string `json:"runtimeArgs,omitempty"` + }{ + Path: i.executablePath, + RuntimeArgs: i.runtimeArgs, + } + } + _, ok := rts[i.Runtime] + switch { + case !ok: + log.Printf("Runtime %s not found: adding\n", i.Runtime) + updateRuntime() + case i.Clobber: + log.Printf("Clobber is set. Overwriting runtime %s not found: adding\n", i.Runtime) + updateRuntime() + default: + log.Printf("Not overwriting runtime %s\n", i.Runtime) } // Set experimental if required. @@ -119,24 +161,38 @@ func (i *Install) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) c["experimental"] = true } + re := regexp.MustCompile(`^native.cgroupdriver=`) + // Set the cgroupdriver if required. if i.CgroupDriver != "" { v, ok := c["exec-opts"] - if ok { - opts := v.([]interface{}) - c["exec-opts"] = append(opts, fmt.Sprintf("native.cgroupdriver=%s", i.CgroupDriver)) - } else { + if !ok { c["exec-opts"] = []string{fmt.Sprintf("native.cgroupdriver=%s", i.CgroupDriver)} + } else { + opts := v.([]interface{}) + newOpts := []interface{}{} + for _, opt := range opts { + if !i.Clobber { + newOpts = opts + break + } + o, ok := opt.(string) + if !ok { + continue + } + + if !re.MatchString(o) { + newOpts = append(newOpts, o) + } + } + c["exec-opts"] = append(newOpts, fmt.Sprintf("native.cgroupdriver=%s", i.CgroupDriver)) } } // Write out the runtime. - if err := writeConfig(c, i.ConfigFile); err != nil { - log.Fatalf("Error writing config file %q: %v", i.ConfigFile, err) + if err := rw.write(c, i.ConfigFile); err != nil { + return fmt.Errorf("error writing config file %q: %v", i.ConfigFile, err) } - - // Success. - log.Printf("Added runtime %q with arguments %v to %q.", i.Runtime, runtimeArgs, i.ConfigFile) - return subcommands.ExitSuccess + return nil } // Uninstall implements subcommands.Command. @@ -170,48 +226,61 @@ func (u *Uninstall) SetFlags(fs *flag.FlagSet) { // Execute implements subcommands.Command.Execute. func (u *Uninstall) Execute(context.Context, *flag.FlagSet, ...interface{}) subcommands.ExitStatus { log.Printf("Removing runtime %q from %q.", u.Runtime, u.ConfigFile) - - c, err := readConfig(u.ConfigFile) - if err != nil { - log.Fatalf("Error reading config file %q: %v", u.ConfigFile, err) - } - - var rts map[string]interface{} - if i, ok := c["runtimes"]; ok { - rts = i.(map[string]interface{}) - } else { - log.Fatalf("runtime %q not found", u.Runtime) - } - if _, ok := rts[u.Runtime]; !ok { - log.Fatalf("runtime %q not found", u.Runtime) - } - delete(rts, u.Runtime) - - if err := writeConfig(c, u.ConfigFile); err != nil { - log.Fatalf("Error writing config file %q: %v", u.ConfigFile, err) + if err := doUninstallConfig(u, configReaderWriter{ + read: defaultReadConfig, + write: defaultWriteConfig, + }); err != nil { + log.Fatalf("Uninstall failed: %v", err) } return subcommands.ExitSuccess } -func readConfig(path string) (map[string]interface{}, error) { - // Read the configuration data. - configBytes, err := ioutil.ReadFile(path) - if err != nil && !os.IsNotExist(err) { - return nil, err +func doUninstallConfig(u *Uninstall, rw configReaderWriter) error { + configBytes, err := rw.read(u.ConfigFile) + if err != nil { + return fmt.Errorf("error reading config file %q: %v", u.ConfigFile, err) } // Unmarshal the configuration. c := make(map[string]interface{}) if len(configBytes) > 0 { if err := json.Unmarshal(configBytes, &c); err != nil { - return nil, err + return err } } - return c, nil + var rts map[string]interface{} + if i, ok := c["runtimes"]; ok { + rts = i.(map[string]interface{}) + } else { + return fmt.Errorf("runtime %q not found", u.Runtime) + } + if _, ok := rts[u.Runtime]; !ok { + return fmt.Errorf("runtime %q not found", u.Runtime) + } + delete(rts, u.Runtime) + + if err := rw.write(c, u.ConfigFile); err != nil { + return fmt.Errorf("error writing config file %q: %v", u.ConfigFile, err) + } + return nil } -func writeConfig(c map[string]interface{}, filename string) error { +type configReaderWriter struct { + read func(string) ([]byte, error) + write func(map[string]interface{}, string) error +} + +func defaultReadConfig(path string) ([]byte, error) { + // Read the configuration data. + configBytes, err := ioutil.ReadFile(path) + if err != nil && !os.IsNotExist(err) { + return nil, err + } + return configBytes, nil +} + +func defaultWriteConfig(c map[string]interface{}, filename string) error { // Marshal the configuration. b, err := json.MarshalIndent(c, "", " ") if err != nil { diff --git a/runsc/cmd/install_test.go b/runsc/cmd/install_test.go new file mode 100644 index 000000000..f50e2a5b6 --- /dev/null +++ b/runsc/cmd/install_test.go @@ -0,0 +1,286 @@ +// Copyright 2022 The gVisor Authors. +// +// 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 cmd + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" +) + +type runtimeDef struct { + path string + runtimeArgs []string +} + +func (r *runtimeDef) MarshalJSON() ([]byte, error) { + args, err := json.Marshal(r.runtimeArgs) + if err != nil { + return nil, err + } + str := fmt.Sprintf(`{"path": "%s", "runtimeArgs":%s}`, r.path, args) + return []byte(str), nil +} + +func (r *runtimeDef) UnmarshalJSON(data []byte) error { + var dat map[string]interface{} + if err := json.Unmarshal(data, &dat); err != nil { + return err + } + if p, ok := dat["path"]; ok { + r.path = p.(string) + } + if p, ok := dat["runtimeArgs"]; ok { + r.runtimeArgs = p.([]string) + } + return nil +} + +var defaultInput = map[string]interface{}{ + "runtimes": map[string]*runtimeDef{ + "runtime1": &runtimeDef{ + path: "runtime1_path", + runtimeArgs: []string{"some", "args"}, + }, + "other runtime": &runtimeDef{ + path: "other_runtime_path", + runtimeArgs: []string{"some", "other", "args"}, + }, + "myRuntime": &runtimeDef{ + path: "myRuntimePath", + runtimeArgs: []string{"super", "cool", "args"}, + }, + }, + "exec-opts": []string{"some-cgroup-driver=something", "native.cgroupdriver=init_driver"}, +} + +func TestInstall(t *testing.T) { + + for _, tc := range []struct { + name string + i *Install + input map[string]interface{} + output map[string]interface{} + }{ + { + name: "clobber", + i: &Install{ + Runtime: "myRuntime", + Experimental: true, + Clobber: true, + CgroupDriver: "my_driver", + executablePath: "some_runsc_path", + runtimeArgs: []string{"new", "cool", "args"}, + }, + input: defaultInput, + output: map[string]interface{}{ + "runtimes": map[string]*runtimeDef{ + "runtime1": &runtimeDef{ + path: "runtime1_path", + runtimeArgs: []string{"some", "args"}, + }, + "other runtime": &runtimeDef{ + path: "other_runtime_path", + runtimeArgs: []string{"some", "other", "args"}, + }, + "myRuntime": &runtimeDef{ + path: "some_runsc_path", + runtimeArgs: []string{"new", "cool", "args"}, + }, + }, + "exec-opts": []string{"some-cgroup-driver=something", "native.cgroupdriver=my_driver"}, + "experimental": true, + }, + }, + { + name: "no clobber", + i: &Install{ + Runtime: "myRuntime", + Experimental: true, + Clobber: false, + CgroupDriver: "my_driver", + executablePath: "some_runsc_path", + runtimeArgs: []string{"new", "cool", "args"}, + }, + input: defaultInput, + output: map[string]interface{}{ + "runtimes": map[string]*runtimeDef{ + "runtime1": &runtimeDef{ + path: "runtime1_path", + runtimeArgs: []string{"some", "args"}, + }, + "other runtime": &runtimeDef{ + path: "other_runtime_path", + runtimeArgs: []string{"some", "other", "args"}, + }, + "myRuntime": &runtimeDef{ + path: "myRuntimePath", + runtimeArgs: []string{"super", "cool", "args"}, + }, + }, + "exec-opts": []string{"some-cgroup-driver=something", "native.cgroupdriver=init_driver", "native.cgroupdriver=my_driver"}, + "experimental": true, + }, + }, + { + name: "new runtime", + i: &Install{ + Runtime: "newRuntime", + Experimental: true, + executablePath: "newPath", + runtimeArgs: []string{"new", "cool", "args"}, + }, + input: defaultInput, + output: map[string]interface{}{ + "runtimes": map[string]*runtimeDef{ + "runtime1": &runtimeDef{ + path: "runtime1_path", + runtimeArgs: []string{"some", "args"}, + }, + "newRuntime": &runtimeDef{ + path: "newPath", + runtimeArgs: []string{"new", "cool", "args"}, + }, + "other runtime": &runtimeDef{ + path: "other_runtime_path", + runtimeArgs: []string{"some", "other", "args"}, + }, + "myRuntime": &runtimeDef{ + path: "myRuntimePath", + runtimeArgs: []string{"super", "cool", "args"}, + }, + }, + "exec-opts": []string{"some-cgroup-driver=something", "native.cgroupdriver=init_driver"}, + "experimental": true, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + + mockRead := func(_ string) ([]byte, error) { + return json.MarshalIndent(tc.input, "", " ") + } + + got := []byte{} + mockWrite := func(c map[string]interface{}, _ string) error { + res, err := json.MarshalIndent(c, "", " ") + if err != nil { + return err + } + got = res + return nil + } + + rw := configReaderWriter{ + read: mockRead, + write: mockWrite, + } + + if err := doInstallConfig(tc.i, rw); err != nil { + t.Fatalf("Error updating config: %v", err) + } + + want, err := json.MarshalIndent(tc.output, "", " ") + if err != nil { + t.Fatalf("Failed to marshal output: %v", err) + } + + if res := cmp.Diff(string(want), string(got)); res != "" { + t.Fatalf("Mismatch output (-want +got): %s", res) + } + }) + } +} + +func TestUninstall(t *testing.T) { + for _, tc := range []struct { + name string + u *Uninstall + input map[string]interface{} + output map[string]interface{} + wantErr bool + }{ + { + name: "runtime found", + u: &Uninstall{ + Runtime: "other runtime", + }, + input: defaultInput, + output: map[string]interface{}{ + "runtimes": map[string]*runtimeDef{ + "runtime1": &runtimeDef{ + path: "runtime1_path", + runtimeArgs: []string{"some", "args"}, + }, + "myRuntime": &runtimeDef{ + path: "myRuntimePath", + runtimeArgs: []string{"super", "cool", "args"}, + }, + }, + "exec-opts": []string{"some-cgroup-driver=something", "native.cgroupdriver=init_driver"}, + }, + }, + { + name: "runtime not found", + u: &Uninstall{ + Runtime: "not found runtime", + }, + input: defaultInput, + wantErr: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + mockRead := func(_ string) ([]byte, error) { + return json.MarshalIndent(tc.input, "", " ") + } + + got := []byte{} + mockWrite := func(c map[string]interface{}, _ string) error { + res, err := json.MarshalIndent(c, "", " ") + if err != nil { + return err + } + got = res + return nil + } + + rw := configReaderWriter{ + read: mockRead, + write: mockWrite, + } + + err := doUninstallConfig(tc.u, rw) + if tc.wantErr { + if err == nil { + t.Fatalf("Did not get an error when expected.") + } + return + } + if err != nil { + t.Fatalf("Error updating config: %v", err) + } + + want, err := json.MarshalIndent(tc.output, "", " ") + if err != nil { + t.Fatalf("Failed to marshal output: %v", err) + } + if res := cmp.Diff(string(want), string(got)); res != "" { + t.Fatalf("Mismatch output (-want +got-): %s", res) + } + }) + } +}