diff --git a/simulator/container.go b/simulator/container.go index 6f626d311..16294b8e8 100644 --- a/simulator/container.go +++ b/simulator/container.go @@ -288,10 +288,10 @@ func getBridge(bridgeName string) (string, error) { // if the underlay bridge already exists, return that // we don't check for a specific label or similar so that it's possible to use a bridge created by other frameworks for composite testing var bridge bridgeNet - cmd := exec.Command("docker", "network", "ls", "--format", "json", "-f", fmt.Sprintf("name=%s$", bridgeName)) + cmd := exec.Command("docker", "network", "ls", "--format={{json .}}", "-f", fmt.Sprintf("name=%s$", bridgeName)) out, err := cmd.Output() if err != nil { - log.Printf("vcsim %s: %s", cmd.Args, err) + log.Printf("vcsim %s: %s, %s", cmd.Args, err, out) return "", err } @@ -306,7 +306,7 @@ func getBridge(bridgeName string) (string, error) { err = json.Unmarshal([]byte(str), &bridge) if err != nil { - log.Printf("vcsim %s: %s", cmd.Args, err) + log.Printf("vcsim %s: %s, %s", cmd.Args, err, str) return "", err } @@ -586,15 +586,16 @@ func (c *container) remove(ctx *Context) error { if lsverr != nil { log.Printf("%s %s: %s", c.name, cmd.Args, lsverr) } + log.Printf("%s volumes: %s", c.name, volumesToReap) var rmverr error if len(volumesToReap) > 0 { run := []string{"volume", "rm", "-f"} run = append(run, strings.Split(string(volumesToReap), "\n")...) cmd = exec.Command("docker", run...) - rmverr = cmd.Run() + out, rmverr := cmd.Output() if rmverr != nil { - log.Printf("%s %s: %s", c.name, cmd.Args, rmverr) + log.Printf("%s %s: %s, %s", c.name, cmd.Args, rmverr, out) } } diff --git a/simulator/container_virtual_machine.go b/simulator/container_virtual_machine.go index cc67d063f..c12a1e1eb 100644 --- a/simulator/container_virtual_machine.go +++ b/simulator/container_virtual_machine.go @@ -34,6 +34,8 @@ import ( "github.com/vmware/govmomi/vim25/types" ) +const ContainerBackingOptionKey = "RUN.container" + var ( toolsRunning = []types.PropertyChange{ {Name: "guest.toolsStatus", Val: types.VirtualMachineToolsStatusToolsOk}, @@ -63,7 +65,7 @@ func createSimulationVM(vm *VirtualMachine) *simVM { for _, opt := range vm.Config.ExtraConfig { val := opt.GetOptionValue() - if val.Key == "RUN.container" { + if val.Key == ContainerBackingOptionKey { return svm } } @@ -203,7 +205,7 @@ func (svm *simVM) start(ctx *Context) error { for _, opt := range svm.vm.Config.ExtraConfig { val := opt.GetOptionValue() - if val.Key == "RUN.container" { + if val.Key == ContainerBackingOptionKey { run := val.Value.(string) err := json.Unmarshal([]byte(run), &args) if err != nil { diff --git a/simulator/container_virtual_machine_test.go b/simulator/container_virtual_machine_test.go new file mode 100644 index 000000000..e36782a66 --- /dev/null +++ b/simulator/container_virtual_machine_test.go @@ -0,0 +1,249 @@ +/* +Copyright (c) 2023 VMware, 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. +*/ + +package simulator + +import ( + "bytes" + "context" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/vmware/govmomi/find" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25" + "github.com/vmware/govmomi/vim25/types" +) + +// takes a content string to serve from the container and returns ExtraConfig options +// to construct container +// content - the contents of index.html +// port - the port to forward to the container port 80 +func constructNginxBacking(t *testing.T, content string, port int) []types.BaseOptionValue { + dir := t.TempDir() + for dirpart := dir; dirpart != "/"; dirpart = filepath.Dir(dirpart) { + os.Chmod(dirpart, 0755) + } + + fpath := filepath.Join(dir, "index.html") + os.WriteFile(fpath, []byte(content), 0644) + // just in case umask gets in the way + os.Chmod(fpath, 0644) + + args := fmt.Sprintf("-v '%s:/usr/share/nginx/html:ro' nginx", dir) + + return []types.BaseOptionValue{ + &types.OptionValue{Key: ContainerBackingOptionKey, Value: args}, // run nginx + &types.OptionValue{Key: "RUN.port.80", Value: "8888"}, // test port remap + } +} + +// validates the VM is serving the expected content on the expected ports +// pairs with constructNginxBacking +func validateNginxContainer(t *testing.T, vm *object.VirtualMachine, expected string, port int) error { + ip, _ := vm.WaitForIP(context.Background(), true) // Returns the docker container's IP + + // Count the number of bytes in feature_test.go via nginx going direct to the container + cmd := exec.Command("docker", "run", "--rm", "curlimages/curl", "curl", "-f", fmt.Sprintf("http://%s:80", ip)) + var buf bytes.Buffer + cmd.Stdout = &buf + err := cmd.Run() + res := buf.String() + + if err != nil || strings.TrimSpace(res) != expected { + // we use Fail not Fatal because we want to clean up + t.Fail() + t.Log(err, buf.String()) + fmt.Printf("%d diff", buf.Len()-len(expected)) + } + + // Count the number of bytes in feature_test.go via nginx going via port remap on host + cmd = exec.Command("curl", "-f", fmt.Sprintf("http://localhost:%d", port)) + buf.Reset() + cmd.Stdout = &buf + err = cmd.Run() + res = buf.String() + if err != nil || strings.TrimSpace(res) != expected { + t.Fail() + t.Log(err, buf.String()) + fmt.Printf("%d diff", buf.Len()-len(expected)) + } + + return nil +} + +// 1. Construct ExtraConfig args for container backing +// 2. Create VM using that ExtraConfig +// 3. Confirm docker container present that matches expectations +func TestCreateVMWithContainerBacking(t *testing.T) { + Test(func(ctx context.Context, c *vim25.Client) { + if _, err := exec.LookPath("docker"); err != nil { + fmt.Println("0 diff") + t.Skip("docker client binary not on PATH") + return + } + + finder := find.NewFinder(c) + pool, _ := finder.ResourcePool(ctx, "DC0_H0/Resources") + dc, err := finder.Datacenter(ctx, "DC0") + if err != nil { + log.Fatal(err) + } + + content := "foo" + port := 8888 + + spec := types.VirtualMachineConfigSpec{ + Name: "nginx-container-backed-from-creation", + Files: &types.VirtualMachineFileInfo{ + VmPathName: "[LocalDS_0] nginx", + }, + ExtraConfig: constructNginxBacking(t, content, port), + } + + f, _ := dc.Folders(ctx) + // Create a new VM + task, err := f.VmFolder.CreateVM(ctx, spec, pool, nil) + if err != nil { + log.Fatal(err) + } + + info, err := task.WaitForResult(ctx, nil) + if err != nil { + log.Fatal(err) + } + + vm := object.NewVirtualMachine(c, info.Result.(types.ManagedObjectReference)) + // PowerOn VM starts the nginx container + task, _ = vm.PowerOn(ctx) + err = task.Wait(ctx) + if err != nil { + log.Fatal(err) + } + + err = validateNginxContainer(t, vm, content, port) + if err != nil { + log.Fatal(err) + } + + spec2 := types.VirtualMachineConfigSpec{ + ExtraConfig: []types.BaseOptionValue{ + &types.OptionValue{Key: ContainerBackingOptionKey, Value: ""}, + }, + } + + task, err = vm.Reconfigure(ctx, spec2) + if err != nil { + log.Fatal(err) + } + + info, err = task.WaitForResult(ctx, nil) + if err != nil { + log.Fatal(info, err) + } + + // PowerOff stops the container + task, _ = vm.PowerOff(ctx) + _ = task.Wait(ctx) + // Destroy deletes the container + task, _ = vm.Destroy(ctx) + _ = task.Wait(ctx) + }) + // Output: 0 diff +} + +// 1. Create VM without ExtraConfig args for container backing +// 2. Construct ExtraConfig args for container backing +// 3. Update VM with ExtraConfig +// 4. Confirm docker container present that matches expectations +func TestUpdateVMAddContainerBacking(t *testing.T) { + Test(func(ctx context.Context, c *vim25.Client) { + if _, err := exec.LookPath("docker"); err != nil { + fmt.Println("0 diff") + t.Skip("docker client binary not on PATH") + return + } + + finder := find.NewFinder(c) + pool, _ := finder.ResourcePool(ctx, "DC0_H0/Resources") + dc, err := finder.Datacenter(ctx, "DC0") + if err != nil { + log.Fatal(err) + } + + content := "foo" + port := 8888 + + spec := types.VirtualMachineConfigSpec{ + Name: "nginx-container-after-reconfig", + Files: &types.VirtualMachineFileInfo{ + VmPathName: "[LocalDS_0] nginx", + }, + } + + f, _ := dc.Folders(ctx) + // Create a new VM + task, err := f.VmFolder.CreateVM(ctx, spec, pool, nil) + if err != nil { + log.Fatal(err) + } + + info, err := task.WaitForResult(ctx, nil) + if err != nil { + log.Fatal(err) + } + + vm := object.NewVirtualMachine(c, info.Result.(types.ManagedObjectReference)) + // PowerOn VM starts the nginx container + task, _ = vm.PowerOn(ctx) + err = task.Wait(ctx) + if err != nil { + log.Fatal(err) + } + + spec2 := types.VirtualMachineConfigSpec{ + ExtraConfig: constructNginxBacking(t, content, port), + } + + task, err = vm.Reconfigure(ctx, spec2) + if err != nil { + log.Fatal(err) + } + + info, err = task.WaitForResult(ctx, nil) + if err != nil { + log.Fatal(info, err) + } + + err = validateNginxContainer(t, vm, content, port) + if err != nil { + log.Fatal(err) + } + + // PowerOff stops the container + task, _ = vm.PowerOff(ctx) + _ = task.Wait(ctx) + // Destroy deletes the container + task, _ = vm.Destroy(ctx) + _ = task.Wait(ctx) + }) + // Output: 0 diff +} diff --git a/simulator/feature_test.go b/simulator/feature_test.go index 8ff91b116..ea817e052 100644 --- a/simulator/feature_test.go +++ b/simulator/feature_test.go @@ -159,7 +159,7 @@ func Example_runContainer() { } // Count the number of bytes in feature_test.go via nginx going via port remap on host - cmd = exec.Command("docker", "run", "--rm", "--network=host", "curlimages/curl", "curl", "-f", fmt.Sprintf("http://%s", ip)) + cmd = exec.Command("curl", "-f", "http://localhost:8888") buf.Reset() cmd.Stdout = &buf err = cmd.Run() diff --git a/simulator/virtual_machine.go b/simulator/virtual_machine.go index d65937c85..ff43a26c5 100644 --- a/simulator/virtual_machine.go +++ b/simulator/virtual_machine.go @@ -394,7 +394,8 @@ func extraConfigKey(key string) string { return key } -func (vm *VirtualMachine) applyExtraConfig(spec *types.VirtualMachineConfigSpec) { +func (vm *VirtualMachine) applyExtraConfig(ctx *Context, spec *types.VirtualMachineConfigSpec) types.BaseMethodFault { + var removedContainerBacking bool var changes []types.PropertyChange for _, c := range spec.ExtraConfig { val := c.GetOptionValue() @@ -419,6 +420,9 @@ func (vm *VirtualMachine) applyExtraConfig(spec *types.VirtualMachineConfigSpec) vm.Config.ExtraConfig = append(vm.Config.ExtraConfig, c) } else { if s, ok := val.Value.(string); ok && s == "" { + if key == ContainerBackingOptionKey { + removedContainerBacking = true + } // Remove existing element l := len(vm.Config.ExtraConfig) vm.Config.ExtraConfig[keyIndex] = vm.Config.ExtraConfig[l-1] @@ -450,13 +454,48 @@ func (vm *VirtualMachine) applyExtraConfig(spec *types.VirtualMachineConfigSpec) ) } } - if len(changes) != 0 { - Map.Update(vm, changes) - } + // create the container backing before we publish the updates so the simVM is available before handlers + // get triggered + var fault types.BaseMethodFault if vm.svm == nil { vm.svm = createSimulationVM(vm) + + // check to see if the VM is already powered on - if so we need to retroactively hit that path here + if vm.Runtime.PowerState == types.VirtualMachinePowerStatePoweredOn { + err := vm.svm.start(ctx) + if err != nil { + // don't attempt to undo the changes already made - just return an error + // we'll retry the svm.start operation on pause/restart calls + fault = &types.VAppConfigFault{ + VimFault: types.VimFault{ + MethodFault: types.MethodFault{ + FaultCause: &types.LocalizedMethodFault{ + Fault: &types.SystemErrorFault{Reason: err.Error()}, + LocalizedMessage: err.Error()}}}} + } + } + } else if removedContainerBacking { + err := vm.svm.remove(ctx) + if err == nil { + vm.svm = nil + } else { + // don't attempt to undo the changes already made - just return an error + // we'll retry the svm.start operation on pause/restart calls + fault = &types.VAppConfigFault{ + VimFault: types.VimFault{ + MethodFault: types.MethodFault{ + FaultCause: &types.LocalizedMethodFault{ + Fault: &types.SystemErrorFault{Reason: err.Error()}, + LocalizedMessage: err.Error()}}}} + } + } + + if len(changes) != 0 { + Map.Update(vm, changes) } + + return fault } func validateGuestID(id string) types.BaseMethodFault { @@ -1499,6 +1538,7 @@ func (vm *VirtualMachine) genVmdkPath(p object.DatastorePath) (string, types.Bas func (vm *VirtualMachine) configureDevices(ctx *Context, spec *types.VirtualMachineConfigSpec) types.BaseMethodFault { devices := object.VirtualDeviceList(vm.Config.Hardware.Device) + var err types.BaseMethodFault for i, change := range spec.DeviceChange { dspec := change.GetVirtualDeviceConfigSpec() device := dspec.Device.GetVirtualDevice() @@ -1535,7 +1575,7 @@ func (vm *VirtualMachine) configureDevices(ctx *Context, spec *types.VirtualMach } key := device.Key - err := vm.configureDevice(ctx, devices, dspec, nil) + err = vm.configureDevice(ctx, devices, dspec, nil) if err != nil { return err } @@ -1562,7 +1602,7 @@ func (vm *VirtualMachine) configureDevices(ctx *Context, spec *types.VirtualMach device.DeviceInfo.GetDescription().Summary = "" // regenerate summary } - err := vm.configureDevice(ctx, devices, dspec, oldDevice) + err = vm.configureDevice(ctx, devices, dspec, oldDevice) if err != nil { return err } @@ -1577,9 +1617,16 @@ func (vm *VirtualMachine) configureDevices(ctx *Context, spec *types.VirtualMach {Name: "config.hardware.device", Val: []types.BaseVirtualDevice(devices)}, }) - vm.updateDiskLayouts() + err = vm.updateDiskLayouts() + if err != nil { + return err + } - vm.applyExtraConfig(spec) // Do this after device config, as some may apply to the devices themselves (e.g. ethernet -> guest.net) + // Do this after device config, as some may apply to the devices themselves (e.g. ethernet -> guest.net) + err = vm.applyExtraConfig(ctx, spec) + if err != nil { + return err + } return nil }