diff --git a/go.mod b/go.mod index 6e3acd7b0a3c..4243ab132585 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( cloud.google.com/go/storage v1.56.1 github.com/Delta456/box-cli-maker/v2 v2.3.0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.29.0 - github.com/Parallels/docker-machine-parallels/v2 v2.0.1 github.com/VividCortex/godaemon v1.0.0 github.com/Xuanwo/go-locale v1.1.3 github.com/blang/semver/v4 v4.0.0 @@ -31,6 +30,7 @@ require ( github.com/google/uuid v1.6.0 github.com/hashicorp/go-getter v1.8.0 github.com/hashicorp/go-retryablehttp v0.7.8 + github.com/hashicorp/go-version v1.6.0 github.com/hooklift/iso9660 v1.0.0 github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0 github.com/johanneswuerbach/nfsexports v0.0.0-20200318065542-c48c3734757f @@ -166,7 +166,6 @@ require ( github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.65 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect - github.com/hashicorp/go-version v1.6.0 // indirect github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 // indirect github.com/hooklift/assert v0.0.0-20170704181755-9d1defd6d214 // indirect github.com/huandu/xstrings v1.3.2 // indirect @@ -256,7 +255,6 @@ require ( ) replace ( - github.com/Parallels/docker-machine-parallels/v2 => github.com/minikube-machine/machine-driver-parallels/v2 v2.0.2-0.20240730142131-ada9375ea417 github.com/docker/machine => github.com/minikube-machine/machine v0.0.0-20251109100456-3b479dcea7a3 github.com/machine-drivers/docker-machine-driver-vmware => github.com/minikube-machine/machine-driver-vmware v0.1.6-0.20230701123042-a391c48b14d5 ) diff --git a/go.sum b/go.sum index d80559e27379..e5749e13c3bf 100644 --- a/go.sum +++ b/go.sum @@ -162,7 +162,6 @@ github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9 github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -181,7 +180,6 @@ github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvM github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v0.0.0-20180621001606-093424bec097/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v17.12.0-ce-rc1.0.20181225093023-5ddb1d410a8b+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v17.12.0-ce-rc1.0.20190115220918-5ec31380a5d3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= @@ -368,7 +366,6 @@ github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVU github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= -github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -435,7 +432,6 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -475,8 +471,6 @@ github.com/miekg/dns v1.1.48 h1:Ucfr7IIVyMBz4lRE8qmGUuZ4Wt3/ZGu9hmcMT3Uu4tQ= github.com/miekg/dns v1.1.48/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/minikube-machine/machine v0.0.0-20251109100456-3b479dcea7a3 h1:LO7khkb1P2LpqCL8GnqPBPn1lZeqA/bqRinjveU2EaQ= github.com/minikube-machine/machine v0.0.0-20251109100456-3b479dcea7a3/go.mod h1:wrzTHaSSmyll2TxLCq4mTFL5RJfeFr6qger+VcqNm9g= -github.com/minikube-machine/machine-driver-parallels/v2 v2.0.2-0.20240730142131-ada9375ea417 h1:f+neTRGCtvmW3Tm1V72vWpoTPuNOnXSQsHZdYOryfGM= -github.com/minikube-machine/machine-driver-parallels/v2 v2.0.2-0.20240730142131-ada9375ea417/go.mod h1:NKwI5KryEmEHMZVj80t9JQcfXWZp4/ZYNBuw4C5sQ9E= github.com/minikube-machine/machine-driver-vmware v0.1.6-0.20230701123042-a391c48b14d5 h1:1z7xOzfMO4aBR9+2nYjlhRXX1773fX60HTS0QGpGRPU= github.com/minikube-machine/machine-driver-vmware v0.1.6-0.20230701123042-a391c48b14d5/go.mod h1:HifYFOWR0bAMN4hWtaSADClogvtPy/jV0aRC5alhrKo= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -601,7 +595,6 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= @@ -616,11 +609,9 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -667,7 +658,6 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= @@ -735,7 +725,6 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= diff --git a/pkg/drivers/parallels/LICENSE b/pkg/drivers/parallels/LICENSE new file mode 100644 index 000000000000..538d72bed678 --- /dev/null +++ b/pkg/drivers/parallels/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015-2020 Parallels IP Holdings GmbH. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pkg/drivers/parallels/parallels.go b/pkg/drivers/parallels/parallels.go new file mode 100644 index 000000000000..ee553bbb4b66 --- /dev/null +++ b/pkg/drivers/parallels/parallels.go @@ -0,0 +1,47 @@ +//go:build !darwin + +/* +Copyright 2022 The Kubernetes Authors 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. +*/ + +/* +Copyright (c) 2015-2020 Parallels IP Holdings GmbH. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package parallels + +import "github.com/docker/machine/libmachine/drivers" + +func NewDriver(hostName, storePath string) drivers.Driver { + return drivers.NewDriverNotSupported("parallels", hostName, storePath) +} diff --git a/pkg/drivers/parallels/parallels_darwin.go b/pkg/drivers/parallels/parallels_darwin.go new file mode 100644 index 000000000000..4d4c4d21f360 --- /dev/null +++ b/pkg/drivers/parallels/parallels_darwin.go @@ -0,0 +1,841 @@ +/* +Copyright 2022 The Kubernetes Authors 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. +*/ + +/* +Copyright (c) 2015-2020 Parallels IP Holdings GmbH. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package parallels + +import ( + "errors" + "fmt" + "io/ioutil" + "net" + "os" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "time" + + "github.com/docker/machine/libmachine/drivers" + "github.com/docker/machine/libmachine/log" + "github.com/docker/machine/libmachine/mcnflag" + "github.com/docker/machine/libmachine/mcnutils" + "github.com/docker/machine/libmachine/ssh" + "github.com/docker/machine/libmachine/state" + "github.com/hashicorp/go-version" +) + +const ( + isoFilename = "boot2docker.iso" + shareFolderNamePrefix = "docker_machine_share_" + minDiskSize = 32 + defaultCPU = 1 + defaultMemory = 1024 + defaultVideoSize = 64 + defaultBoot2DockerURL = "" + defaultShareFolder = "/Users" + defaultNoShare = false + defaultDiskSize = 20000 + defaultSSHPort = 22 + defaultSSHUser = "docker" + defaultNestedVirtualization = false +) + +var ( + reMachineNotFound = regexp.MustCompile(`Failed to get VM config: The virtual machine could not be found..*`) + reParallelsVersion = regexp.MustCompile(`.* (\d+\.\d+\.\d+).*`) + reParallelsEdition = regexp.MustCompile(`edition="(.+)"`) + reSharedAdapterIP = regexp.MustCompile(`\s*IPv4 address:\s*(\d+\.\d+\.\d+\.\d+)`) + reSharedFolder = regexp.MustCompile(`\s*(.+) \(\+\) path='(.+)' mode=.+`) + + errMachineNotExist = errors.New("machine does not exist") + errSharedNetworkNotConnected = errors.New("Your Mac host is not connected to Shared network. Please, ensure this option is set: 'Parallels Desktop' -> 'Preferences' -> 'Network' -> 'Shared' -> 'Connect Mac to this network'") + + v11, _ = version.NewVersion("11.0.0") +) + +// Driver for Parallels Desktop +type Driver struct { + *drivers.BaseDriver + CPU int + Memory int + VideoSize int + DiskSize int + Boot2DockerURL string + NoShare bool + ShareFolders []string + NestedVirtualization bool +} + +// NewDriver creates a new Parallels Desktop driver with default settings +func NewDriver(hostName, storePath string) drivers.Driver { + return &Driver{ + BaseDriver: &drivers.BaseDriver{ + MachineName: hostName, + StorePath: storePath, + SSHUser: defaultSSHUser, + SSHPort: defaultSSHPort, + }, + CPU: defaultCPU, + Memory: defaultMemory, + VideoSize: defaultVideoSize, + DiskSize: defaultDiskSize, + Boot2DockerURL: defaultBoot2DockerURL, + NoShare: defaultNoShare, + NestedVirtualization: defaultNestedVirtualization, + } +} + +// Create a host using the driver's config +// +//nolint:gocyclo +func (d *Driver) Create() error { + var ( + err error + ) + + b2dutils := mcnutils.NewB2dUtils(d.StorePath) + if err = b2dutils.CopyIsoToMachineDir(d.Boot2DockerURL, d.MachineName); err != nil { + return err + } + + log.Infof("Creating SSH key...") + sshKeyPath := d.GetSSHKeyPath() + log.Debugf("SSH key: %s", sshKeyPath) + if err = ssh.GenerateSSHKey(sshKeyPath); err != nil { + return err + } + + log.Infof("Creating Parallels Desktop VM...") + + ver, err := getParallelsVersion() + if err != nil { + return err + } + + absStorePath, _ := filepath.Abs(d.ResolveStorePath(".")) + if err = prlctl("create", d.MachineName, + "--distribution", "linux", + "--dst", absStorePath, + "--no-hdd"); err != nil { + return err + } + + cpus := d.CPU + if cpus < 1 { + cpus = runtime.NumCPU() + } + if cpus > 32 { + cpus = 32 + } + + videoSize := d.VideoSize + if videoSize < 2 { + videoSize = defaultVideoSize + } + + if err = prlctl("set", d.MachineName, + "--select-boot-device", "off", + "--cpus", fmt.Sprintf("%d", cpus), + "--memsize", fmt.Sprintf("%d", d.Memory), + "--videosize", fmt.Sprintf("%d", videoSize), + "--cpu-hotplug", "off", + "--on-window-close", "keep-running", + "--longer-battery-life", "on", + "--3d-accelerate", "off", + "--device-bootorder", "cdrom0"); err != nil { + return err + } + + if d.NestedVirtualization { + if err = prlctl("set", d.MachineName, + "--nested-virt", "on"); err != nil { + return err + } + } + + absISOPath, _ := filepath.Abs(d.ResolveStorePath(isoFilename)) + if err = prlctl("set", d.MachineName, + "--device-set", "cdrom0", + "--iface", "sata", + "--position", "0", + "--image", absISOPath, + "--connect"); err != nil { + return err + } + + initialDiskSize := minDiskSize + + // Fix for [GH-67]. Create a bigger disk on Parallels Desktop 13.0.* + constraints, _ := version.NewConstraint(">= 13.0.0, < 13.1.0") + if constraints.Check(ver) { + initialDiskSize = 1891 + } + + // Create a small plain disk. It will be converted and expanded later + if err = prlctl("set", d.MachineName, + "--device-add", "hdd", + "--iface", "sata", + "--position", "1", + "--image", d.diskPath(), + "--type", "plain", + "--size", fmt.Sprintf("%d", initialDiskSize)); err != nil { + return err + } + + if err = d.generateDiskImage(d.DiskSize); err != nil { + return err + } + + // Enable headless mode + if err = prlctl("set", d.MachineName, + "--startup-view", "headless"); err != nil { + return err + } + + // Don't share any additional folders + if err = prlctl("set", d.MachineName, + "--shf-host-defined", "off"); err != nil { + return err + } + + // Enable time sync, don't touch timezone (this part is buggy) + if err = prlctl("set", d.MachineName, "--time-sync", "on"); err != nil { + return err + } + if err = prlctl("set", d.MachineName, + "--disable-timezone-sync", "on"); err != nil { + return err + } + + // Configure Shared Folders + if err = prlctl("set", d.MachineName, + "--shf-host", "on", + "--shared-cloud", "off", + "--shared-profile", "off", + "--smart-mount", "off"); err != nil { + return err + } + + // Configure Serial Console as a socket + if err = prlctl("set", d.MachineName, + "--device-add", "serial", + "--socket", fmt.Sprintf("parallels-serial-%s", d.MachineName)); err != nil { + return err + } + + if !d.NoShare { + for i, f := range d.ShareFolders { + // Ensure the path is absolute and is available + fAbs, err := filepath.Abs(f) + if err != nil { + return err + } + if _, err := os.Stat(fAbs); err != nil { + if os.IsNotExist(err) { + log.Infof("Host path '%s' does not exist. Skipping sharing it with the machine...", fAbs) + continue + } + return err + } + + if err = prlctl("set", d.MachineName, + "--shf-host-add", fmt.Sprintf("%s%d", shareFolderNamePrefix, i), + "--path", fAbs); err != nil { + return err + } + } + } + + log.Infof("Starting Parallels Desktop VM...") + + // Don't use Start() since it expects to have a dhcp lease already + if err = prlctl("start", d.MachineName); err != nil { + return err + } + + var ip string + + log.Infof("Waiting for VM to come online...") + for i := 1; i <= 60; i++ { + ip, err = d.getIPfromDHCPLease() + if err != nil { + log.Debugf("Not there yet %d/%d, error: %s", i, 60, err) + time.Sleep(2 * time.Second) + continue + } + + if ip != "" { + log.Debugf("Got an ip: %s", ip) + conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, d.SSHPort), 2*time.Second) + if err != nil { + log.Debugf("SSH Daemon not responding yet: %s", err) + time.Sleep(2 * time.Second) + continue + } + conn.Close() + break + } + } + + if ip == "" { + return fmt.Errorf("Machine didn't return an IP after 120 seconds, aborting") + } + + d.IPAddress = ip + + if err := d.Start(); err != nil { + return err + } + + return nil +} + +// DriverName returns the name of the driver as it is registered +func (d *Driver) DriverName() string { + return "parallels" +} + +// GetIP returns an IP or hostname that this host is available at +// e.g. 1.2.3.4 or docker-host-d60b70a14d3a.cloudapp.net +func (d *Driver) GetIP() (string, error) { + // Assume that Parallels Desktop hosts don't have IPs unless they are running + s, err := d.GetState() + if err != nil { + return "", err + } + if s != state.Running { + return "", drivers.ErrHostIsNotRunning + } + + ip, err := d.getIPfromDHCPLease() + if err != nil { + return "", err + } + + return ip, nil +} + +// GetSSHHostname returns hostname for use with ssh +func (d *Driver) GetSSHHostname() (string, error) { + return d.GetIP() +} + +// GetURL returns a Docker compatible host URL for connecting to this host +// e.g. tcp://1.2.3.4:2376 +func (d *Driver) GetURL() (string, error) { + ip, err := d.GetIP() + if err != nil { + return "", err + } + if ip == "" { + return "", nil + } + return fmt.Sprintf("tcp://%s:2376", ip), nil +} + +// GetState returns the state that the host is in (running, stopped, etc) +func (d *Driver) GetState() (state.State, error) { + stdout, stderr, err := prlctlOutErr("list", d.MachineName, "--output", "status", "--no-header") + if err != nil { + if reMachineNotFound.FindString(stderr) != "" { + return state.Error, errMachineNotExist + } + return state.Error, err + } + + switch stdout { + case "running\n": + return state.Running, nil + case "paused\n": + return state.Paused, nil + case "suspended\n": + return state.Saved, nil + case "stopping\n": + return state.Stopping, nil + case "stopped\n": + return state.Stopped, nil + } + return state.None, nil +} + +// Kill stops a host forcefully +func (d *Driver) Kill() error { + return prlctl("stop", d.MachineName, "--kill") +} + +// PreCreateCheck allows for pre-create operations to make sure a driver is ready for creation +func (d *Driver) PreCreateCheck() error { + // Check platform type + if runtime.GOOS != "darwin" { + return fmt.Errorf("Driver \"parallels\" works only on macOS!") + } + + // Check Parallels Desktop version + ver, err := getParallelsVersion() + if err != nil { + return err + } + + if ver.LessThan(v11) { + return fmt.Errorf("Driver \"parallels\" supports only Parallels Desktop 11 and higher. You use: Parallels Desktop %s.", ver) + } + + // Check Parallels Desktop edition + edit, err := getParallelsEdition() + if err != nil { + return err + } + + log.Debugf("Found Parallels Desktop version: %d, edition: %s", ver, edit) + + switch edit { + case "pro", "business": + break + default: + return fmt.Errorf("Docker Machine can be used only with Parallels Desktop Pro or Business edition. You use: %s edition", edit) + } + + // Check whether the host is connected to Shared network + if err := checkSharedNetworkConnected(); err != nil { + return err + } + + // Downloading boot2docker to cache should be done here to make sure + // that a download failure will not leave a machine half created. + b2dutils := mcnutils.NewB2dUtils(d.StorePath) + if err := b2dutils.UpdateISOCache(d.Boot2DockerURL); err != nil { + return err + } + + return nil +} + +// Remove a host +func (d *Driver) Remove() error { + s, err := d.GetState() + if err != nil { + if err == errMachineNotExist { + log.Infof("machine does not exist, assuming it has been removed already") + return nil + } + return err + } + if s == state.Running { + if err := d.Kill(); err != nil { + return err + } + } + return prlctl("delete", d.MachineName) +} + +// Restart a host. This may just call Stop(); Start() if the provider does not +// have any special restart behaviour. +func (d *Driver) Restart() error { + if err := d.Stop(); err != nil { + return err + } + return d.Start() +} + +// GetCreateFlags registers the flags this driver adds to +// "docker hosts create" +func (d *Driver) GetCreateFlags() []mcnflag.Flag { + return []mcnflag.Flag{ + mcnflag.IntFlag{ + EnvVar: "PARALLELS_MEMORY_SIZE", + Name: "parallels-memory", + Usage: "Size of memory for host in MB", + Value: defaultMemory, + }, + mcnflag.IntFlag{ + EnvVar: "PARALLELS_CPU_COUNT", + Name: "parallels-cpu-count", + Usage: "number of CPUs for the machine (-1 to use the number of CPUs available)", + Value: defaultCPU, + }, + mcnflag.IntFlag{ + EnvVar: "PARALLELS_VIDEO_SIZE", + Name: "parallels-video-size", + Usage: "Size of video memory for host in MB", + Value: defaultVideoSize, + }, + mcnflag.IntFlag{ + EnvVar: "PARALLELS_DISK_SIZE", + Name: "parallels-disk-size", + Usage: "Size of disk for host in MB", + Value: defaultDiskSize, + }, + mcnflag.StringFlag{ + EnvVar: "PARALLELS_BOOT2DOCKER_URL", + Name: "parallels-boot2docker-url", + Usage: "The URL of the boot2docker image. Defaults to the latest available version", + Value: defaultBoot2DockerURL, + }, + mcnflag.BoolFlag{ + Name: "parallels-no-share", + Usage: "Disable the mount of shared folder", + }, + mcnflag.StringSliceFlag{ + Name: "parallels-share-folder", + Usage: "Path to the directory which should be shared with the machine. Default: /Users", + Value: []string{defaultShareFolder}, + }, + mcnflag.BoolFlag{ + Name: "parallels-nested-virtualization", + Usage: "Enable nested virutalization", + }, + } +} + +// SetConfigFromFlags configures the driver with the object that was returned +// by RegisterCreateFlags +func (d *Driver) SetConfigFromFlags(opts drivers.DriverOptions) error { + d.CPU = opts.Int("parallels-cpu-count") + d.Memory = opts.Int("parallels-memory") + d.VideoSize = opts.Int("parallels-video-size") + d.DiskSize = opts.Int("parallels-disk-size") + d.Boot2DockerURL = opts.String("parallels-boot2docker-url") + d.SetSwarmConfigFromFlags(opts) + d.SSHUser = defaultSSHUser + d.SSHPort = defaultSSHPort + d.NoShare = opts.Bool("parallels-no-share") + d.ShareFolders = opts.StringSlice("parallels-share-folder") + d.NestedVirtualization = opts.Bool("parallels-nested-virtualization") + + return nil +} + +// Start a host +func (d *Driver) Start() error { + // Check whether the host is connected to Shared network + if err := checkSharedNetworkConnected(); err != nil { + return err + } + + s, err := d.GetState() + if err != nil { + return err + } + + switch s { + case state.Stopped, state.Saved, state.Paused: + if err = prlctl("start", d.MachineName); err != nil { + return err + } + log.Infof("Waiting for VM to start...") + case state.Running: + break + default: + log.Infof("VM not in restartable state") + } + + if err = drivers.WaitForSSH(d); err != nil { + return err + } + + d.IPAddress, err = d.GetIP() + if err != nil { + return err + } + + // Mount Share Folder + shareFoldersMap, err := d.getShareFolders() + if err != nil { + return err + } + + if !d.NoShare { + for shareName, sharePath := range shareFoldersMap { + if err := d.mountShareFolder(shareName, sharePath); err != nil { + return err + } + } + } + + return nil +} + +// Stop a host gracefully +func (d *Driver) Stop() error { + if err := prlctl("stop", d.MachineName); err != nil { + return err + } + for { + s, err := d.GetState() + if err != nil { + return err + } + if s == state.Running { + time.Sleep(1 * time.Second) + } else { + break + } + } + return nil +} + +func (d *Driver) getIPfromDHCPLease() (string, error) { + + DHCPLeaseFile := "/Library/Preferences/Parallels/parallels_dhcp_leases" + + stdout, _, err := prlctlOutErr("list", "-i", d.MachineName) + if err != nil { + return "", err + } + macRe := regexp.MustCompile("net0.* mac=([0-9A-F]{12}) card=.*") + macMatch := macRe.FindAllStringSubmatch(stdout, 1) + + if len(macMatch) != 1 { + return "", fmt.Errorf("MAC address for NIC: nic0 on Virtual Machine: %s not found!\n", d.MachineName) + } + mac := macMatch[0][1] + + if len(mac) != 12 { + return "", fmt.Errorf("Not a valid MAC address: %s. It should be exactly 12 digits.", mac) + } + + leases, err := ioutil.ReadFile(DHCPLeaseFile) + if err != nil { + return "", err + } + + ipRe := regexp.MustCompile("(.*)=\"(.*),(.*)," + strings.ToLower(mac) + ",.*\"") + mostRecentIP := "" + mostRecentLease := uint64(0) + for _, l := range ipRe.FindAllStringSubmatch(string(leases), -1) { + ip := l[1] + expiry, _ := strconv.ParseUint(l[2], 10, 64) + leaseTime, _ := strconv.ParseUint(l[3], 10, 32) + log.Debugf("Found lease: %s for MAC: %s, expiring at %d, leased for %d s.\n", ip, mac, expiry, leaseTime) + if mostRecentLease <= expiry-leaseTime { + mostRecentIP = ip + mostRecentLease = expiry - leaseTime + } + } + + if len(mostRecentIP) == 0 { + return "", fmt.Errorf("IP lease not found for MAC address %s in: %s\n", mac, DHCPLeaseFile) + } + log.Debugf("Found IP lease: %s for MAC address %s\n", mostRecentIP, mac) + + return mostRecentIP, nil +} + +func (d *Driver) diskPath() string { + absDiskPath, _ := filepath.Abs(d.ResolveStorePath("disk.hdd")) + return absDiskPath +} + +func (d *Driver) getShareFolders() (map[string]string, error) { + stdout, _, err := prlctlOutErr("list", "--info", d.MachineName) + if err != nil { + return nil, err + } + + // Parse Shared Folder name (ID) and path + res := make(map[string]string) + for _, match := range reSharedFolder.FindAllStringSubmatch(stdout, -1) { + sName := match[1] + sPath := match[2] + log.Debugf("Found the configured shared folder. Name: %q, Path: %q\n", sName, sPath) + res[sName] = sPath + } + return res, nil +} + +// Mounts shared folder to the specified guest path. It is assumed that host and guest paths are the same +func (d *Driver) mountShareFolder(shareName string, mountPoint string) error { + // Check the host path is available + if _, err := os.Stat(mountPoint); err != nil { + if os.IsNotExist(err) { + log.Infof("Host path %q does not exist. Skipping mount to VM...", mountPoint) + return nil + } + return err + } + + // Ensure that the share is available on the guest side + checkCmd := fmt.Sprintf("sudo modprobe prl_fs && grep -w %q /proc/fs/prl_fs/sf_list", shareName) + if _, err := drivers.RunSSHCommandFromDriver(d, checkCmd); err != nil { + log.Infof("Shared folder %q is unavailable. Skipping mount to VM...", shareName) + return nil + } + + // Mount the shared folder + log.Infof("Mounting shared folder %q ...", mountPoint) + mountCmd := fmt.Sprintf("sudo mkdir -p %q && sudo mount -t prl_fs %q %q", mountPoint, shareName, mountPoint) + if _, err := drivers.RunSSHCommandFromDriver(d, mountCmd); err != nil { + return fmt.Errorf("Error mounting shared folder: %s", err) + } + + return nil +} + +// Make a boot2docker VM disk image. +func (d *Driver) generateDiskImage(size int) error { + tarBuf, err := mcnutils.MakeDiskImage(d.publicSSHKeyPath()) + if err != nil { + return err + } + + minSizeBytes := int64(minDiskSize) << 20 // usually won't fit in 32-bit int (max 2GB) + + //Expand the initial image if needed + if bufLen := int64(tarBuf.Len()); bufLen > minSizeBytes { + bufLenMBytes := bufLen>>20 + 1 + if err = prldisktool("resize", + "--hdd", d.diskPath(), + "--size", fmt.Sprintf("%d", bufLenMBytes)); err != nil { + return err + } + } + + // Find hds file + hdsList, err := filepath.Glob(d.diskPath() + "/*.hds") + if err != nil { + return err + } + if len(hdsList) == 0 { + return fmt.Errorf("Could not find *.hds image in %s", d.diskPath()) + } + hdsPath := hdsList[0] + log.Debugf("HDS image path: %s", hdsPath) + + // Write tar to the hds file + hds, err := os.OpenFile(hdsPath, os.O_WRONLY, 0644) + if err != nil { + return err + } + defer hds.Close() + hds.Seek(0, os.SEEK_SET) + _, err = hds.Write(tarBuf.Bytes()) + if err != nil { + return err + } + hds.Close() + + // Convert image to expanding type and resize it + if err := prldisktool("convert", "--expanding", "--merge", + "--hdd", d.diskPath()); err != nil { + return err + } + + if err := prldisktool("resize", + "--hdd", d.diskPath(), + "--size", fmt.Sprintf("%d", size)); err != nil { + return err + } + + return nil +} + +func (d *Driver) publicSSHKeyPath() string { + return d.GetSSHKeyPath() + ".pub" +} + +// Detects Parallels Desktop major version +func getParallelsVersion() (*version.Version, error) { + stdout, _, err := prlctlOutErr("--version") + if err != nil { + return nil, err + } + + // Parse Parallels Desktop version + verRaw := reParallelsVersion.FindStringSubmatch(stdout) + if verRaw == nil { + return nil, fmt.Errorf("Parallels Desktop version could not be fetched: %s", stdout) + } + + ver, err := version.NewVersion(verRaw[1]) + if err != nil { + return nil, err + } + + return ver, nil +} + +// Detects Parallels Desktop edition +func getParallelsEdition() (string, error) { + stdout, _, err := prlsrvctlOutErr("info", "--license") + if err != nil { + return "", err + } + + // Parse Parallels Desktop version + res := reParallelsEdition.FindStringSubmatch(stdout) + if res == nil { + return "", fmt.Errorf("Driver \"parallels\" requires Parallels Desktop license to be activated. More info: https://kb.parallels.com/en/124225") + } + + return res[1], nil +} + +// Checks whether the host is connected to Shared network +func checkSharedNetworkConnected() error { + stdout, _, err := prlsrvctlOutErr("net", "info", "Shared") + if err != nil { + return err + } + + // Parse the IPv4 of Shared network adapter + res := reSharedAdapterIP.FindStringSubmatch(stdout) + if res == nil { + return errSharedNetworkNotConnected + } + + sharedNetworkIP := net.ParseIP(res[1]) + log.Debugf("IP address of Shared network adapter: %s", sharedNetworkIP) + + hostAddrs, err := net.InterfaceAddrs() + if err != nil { + return err + } + log.Debugf("All host interface addresses: %v", hostAddrs) + + // Check if the there is an interface with the Shared network adapter's IP assigned + for _, netAddr := range hostAddrs { + ipAddr := netAddr.(*net.IPNet).IP + if ipAddr.Equal(sharedNetworkIP) { + log.Debugf("Parallels Shared network adapter is connected") + return nil + } + } + + return errSharedNetworkNotConnected +} diff --git a/pkg/drivers/parallels/prlctl.go b/pkg/drivers/parallels/prlctl.go new file mode 100644 index 000000000000..1f82930b5d3e --- /dev/null +++ b/pkg/drivers/parallels/prlctl.go @@ -0,0 +1,113 @@ +//go:build darwin + +/* +Copyright 2022 The Kubernetes Authors 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. +*/ + +/* +Copyright (c) 2015-2020 Parallels IP Holdings GmbH. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package parallels + +import ( + "bytes" + "errors" + "os" + "os/exec" + "strings" + + "github.com/docker/machine/libmachine/log" +) + +func detectCmdInPath(cmd string) string { + if path, err := exec.LookPath(cmd); err == nil { + return path + } + return cmd +} + +var ( + prlctlCmd = detectCmdInPath("prlctl") + prlsrvctlCmd = detectCmdInPath("prlsrvctl") + prldisktoolCmd = detectCmdInPath("prl_disk_tool") + + errPrlctlNotFound = errors.New("Could not detect `prlctl` binary! Make sure Parallels Desktop Pro or Business edition is installed") + errPrlsrvctlNotFound = errors.New("Could not detect `prlsrvctl` binary! Make sure Parallels Desktop Pro or Business edition is installed") + errPrldisktoolNotFound = errors.New("Could not detect `prl_disk_tool` binary! Make sure Parallels Desktop Pro or Business edition is installed") +) + +func runCmd(cmdName string, args []string, notFound error) (string, string, error) { + cmd := exec.Command(cmdName, args...) + if os.Getenv("MACHINE_DEBUG") != "" { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout, cmd.Stderr = &stdout, &stderr + log.Debugf("executing: %v %v", cmdName, strings.Join(args, " ")) + + err := cmd.Run() + if err != nil { + if ee, ok := err.(*exec.Error); ok && ee.Err == exec.ErrNotFound { + err = notFound + } + } + return stdout.String(), stderr.String(), err +} + +func prlctl(args ...string) error { + _, _, err := runCmd(prlctlCmd, args, errPrlctlNotFound) + return err +} + +func prlctlOutErr(args ...string) (string, string, error) { + return runCmd(prlctlCmd, args, errPrlctlNotFound) +} + +//nolint:unused +func prlsrvctl(args ...string) error { + _, _, err := runCmd(prlsrvctlCmd, args, errPrlsrvctlNotFound) + return err +} + +func prlsrvctlOutErr(args ...string) (string, string, error) { + return runCmd(prlsrvctlCmd, args, errPrlsrvctlNotFound) +} + +func prldisktool(args ...string) error { + _, _, err := runCmd(prldisktoolCmd, args, errPrldisktoolNotFound) + return err +} diff --git a/pkg/minikube/registry/drvs/parallels/parallels.go b/pkg/minikube/registry/drvs/parallels/parallels.go index 6f20364fadd9..ad39f05b7666 100644 --- a/pkg/minikube/registry/drvs/parallels/parallels.go +++ b/pkg/minikube/registry/drvs/parallels/parallels.go @@ -22,8 +22,9 @@ import ( "fmt" "os/exec" - parallels "github.com/Parallels/docker-machine-parallels/v2" "github.com/docker/machine/libmachine/drivers" + + "k8s.io/minikube/pkg/drivers/parallels" "k8s.io/minikube/pkg/minikube/config" "k8s.io/minikube/pkg/minikube/download" "k8s.io/minikube/pkg/minikube/driver"