diff --git a/cmd/minikube/cmd/config/profile_list.go b/cmd/minikube/cmd/config/profile_list.go new file mode 100644 index 000000000000..24d51e2eef13 --- /dev/null +++ b/cmd/minikube/cmd/config/profile_list.go @@ -0,0 +1,73 @@ +/* +Copyright 2019 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. +*/ + +package config + +import ( + "fmt" + "os" + "strconv" + + "k8s.io/minikube/pkg/minikube/config" + "k8s.io/minikube/pkg/minikube/console" + "k8s.io/minikube/pkg/minikube/exit" + + "github.com/olekukonko/tablewriter" + "github.com/spf13/cobra" +) + +var profileListCmd = &cobra.Command{ + Use: "list", + Short: "Lists all minikube profiles.", + Long: "Lists all minikube profiles.", + Run: func(cmd *cobra.Command, args []string) { + + var validData [][]string + + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"Profile", "VM Driver", "NodeIP", "Node Port", "Kubernetes Version"}) + table.SetAutoFormatHeaders(false) + table.SetBorders(tablewriter.Border{Left: true, Top: true, Right: true, Bottom: true}) + table.SetCenterSeparator("|") + validProfiles, invalidProfiles, err := config.ListProfiles() + + for _, p := range validProfiles { + validData = append(validData, []string{p.Name, p.Config.MachineConfig.VMDriver, p.Config.KubernetesConfig.NodeIP, strconv.Itoa(p.Config.KubernetesConfig.NodePort), p.Config.KubernetesConfig.KubernetesVersion}) + } + + table.AppendBulk(validData) + table.Render() + + if invalidProfiles != nil { + console.OutT(console.WarningType, "Found {{.number}} invalid profile(s) ! ", console.Arg{"number": len(invalidProfiles)}) + for _, p := range invalidProfiles { + console.OutT(console.Empty, "\t "+p.Name) + } + console.OutT(console.Tip, "You can delete them using the following command(s): ") + for _, p := range invalidProfiles { + console.Out(fmt.Sprintf("\t $ minikube delete -p %s \n", p.Name)) + } + + } + if err != nil { + exit.WithCode(exit.Config, fmt.Sprintf("error loading profiles: %v", err)) + } + }, +} + +func init() { + ProfileCmd.AddCommand(profileListCmd) +} diff --git a/pkg/minikube/config/config.go b/pkg/minikube/config/config.go index 7a80446423b9..edcd5d91dffa 100644 --- a/pkg/minikube/config/config.go +++ b/pkg/minikube/config/config.go @@ -109,7 +109,7 @@ func Load() (*Config, error) { // Loader loads the kubernetes and machine config based on the machine profile name type Loader interface { - LoadConfigFromFile(profile string) (*Config, error) + LoadConfigFromFile(profile string, miniHome ...string) (*Config, error) } type simpleConfigLoader struct{} @@ -117,10 +117,10 @@ type simpleConfigLoader struct{} // DefaultLoader is the default config loader var DefaultLoader Loader = &simpleConfigLoader{} -func (c *simpleConfigLoader) LoadConfigFromFile(profile string) (*Config, error) { +func (c *simpleConfigLoader) LoadConfigFromFile(profile string, miniHome ...string) (*Config, error) { var cc Config - path := constants.GetProfileFile(profile) + path := constants.GetProfileFile(profile, miniHome...) if _, err := os.Stat(path); os.IsNotExist(err) { return nil, err diff --git a/pkg/minikube/config/profile.go b/pkg/minikube/config/profile.go new file mode 100644 index 000000000000..bd56d070bd15 --- /dev/null +++ b/pkg/minikube/config/profile.go @@ -0,0 +1,84 @@ +/* +Copyright 2019 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. +*/ + +package config + +import ( + "io/ioutil" + "path/filepath" + + "k8s.io/minikube/pkg/minikube/constants" +) + +// isValid checks if the profile has the essential info needed for a profile +func (p *Profile) isValid() bool { + if p.Config.MachineConfig.VMDriver == "" { + return false + } + if p.Config.KubernetesConfig.KubernetesVersion == "" { + return false + } + return true +} + +// ListProfiles returns all valid and invalid (if any) minikube profiles +// invalidPs are the profiles that have a directory or config file but not usable +// invalidPs would be suggeted to be deleted +func ListProfiles(miniHome ...string) (validPs []*Profile, inValidPs []*Profile, err error) { + pDirs, err := profileDirs(miniHome...) + if err != nil { + return nil, nil, err + } + for _, n := range pDirs { + p, err := loadProfile(n, miniHome...) + if err != nil { + inValidPs = append(inValidPs, p) + continue + } + if !p.isValid() { + inValidPs = append(inValidPs, p) + continue + } + validPs = append(validPs, p) + } + return validPs, inValidPs, nil +} + +// loadProfile loads type Profile based on its name +func loadProfile(name string, miniHome ...string) (*Profile, error) { + cfg, err := DefaultLoader.LoadConfigFromFile(name, miniHome...) + p := &Profile{ + Name: name, + Config: cfg, + } + return p, err +} + +// profileDirs gets all the folders in the user's profiles folder regardless of valid or invalid config +func profileDirs(miniHome ...string) (dirs []string, err error) { + miniPath := constants.GetMinipath() + if len(miniHome) > 0 { + miniPath = miniHome[0] + } + pRootDir := filepath.Join(miniPath, "profiles") + items, err := ioutil.ReadDir(pRootDir) + for _, f := range items { + if f.IsDir() { + dirs = append(dirs, f.Name()) + } + } + return dirs, err +} diff --git a/pkg/minikube/config/profile_test.go b/pkg/minikube/config/profile_test.go new file mode 100644 index 000000000000..ac96602ee61c --- /dev/null +++ b/pkg/minikube/config/profile_test.go @@ -0,0 +1,72 @@ +/* +Copyright 2019 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. +*/ + +package config + +import ( + "path/filepath" + "testing" +) + +func TestListProfiles(t *testing.T) { + miniDir, err := filepath.Abs("./testdata/.minikube") + if err != nil { + t.Errorf("error getting dir path for ./testdata/.minikube : %v", err) + } + // test cases for valid profiles + var testCasesValidProfs = []struct { + index int + expectName string + vmDriver string + }{ + {0, "p1", "hyperkit"}, + {1, "p2", "virtualbox"}, + } + + // test cases for invalid profiles + var testCasesInValidProfs = []struct { + index int + expectName string + vmDriver string + }{ + {0, "p3_empty", ""}, + {1, "p4_invalid_file", ""}, + {2, "p5_partial_config", ""}, + } + + val, inv, err := ListProfiles(miniDir) + + for _, tt := range testCasesValidProfs { + if val[tt.index].Name != tt.expectName { + t.Errorf("expected %s got %v", tt.expectName, val[tt.index].Name) + } + if val[tt.index].Config.MachineConfig.VMDriver != tt.vmDriver { + t.Errorf("expected %s got %v", tt.vmDriver, val[tt.index].Config.MachineConfig.VMDriver) + } + + } + + // making sure it returns the invalid profiles + for _, tt := range testCasesInValidProfs { + if inv[tt.index].Name != tt.expectName { + t.Errorf("expected %s got %v", tt.expectName, inv[tt.index].Name) + } + } + + if err != nil { + t.Errorf("error listing profiles %v", err) + } +} diff --git a/pkg/minikube/config/testdata/.minikube/profiles/p1/config.json b/pkg/minikube/config/testdata/.minikube/profiles/p1/config.json new file mode 100644 index 000000000000..86699a29bb9e --- /dev/null +++ b/pkg/minikube/config/testdata/.minikube/profiles/p1/config.json @@ -0,0 +1,50 @@ +{ + "MachineConfig": { + "KeepContext": false, + "MinikubeISO": "https://storage.googleapis.com/minikube/iso/minikube-v1.2.0.iso", + "Memory": 2000, + "CPUs": 2, + "DiskSize": 20000, + "VMDriver": "hyperkit", + "ContainerRuntime": "docker", + "HyperkitVpnKitSock": "", + "HyperkitVSockPorts": [], + "XhyveDiskDriver": "ahci-hd", + "DockerEnv": null, + "InsecureRegistry": null, + "RegistryMirror": null, + "HostOnlyCIDR": "192.168.99.1/24", + "HypervVirtualSwitch": "", + "KVMNetwork": "default", + "KVMQemuURI": "qemu:///system", + "KVMGPU": false, + "KVMHidden": false, + "DockerOpt": null, + "DisableDriverMounts": false, + "NFSShare": [], + "NFSSharesRoot": "/nfsshares", + "UUID": "", + "NoVTXCheck": false, + "DNSProxy": false, + "HostDNSResolver": true + }, + "KubernetesConfig": { + "KubernetesVersion": "v1.15.0", + "NodeIP": "192.168.64.75", + "NodePort": 8443, + "NodeName": "minikube", + "APIServerName": "minikubeCA", + "APIServerNames": null, + "APIServerIPs": null, + "DNSDomain": "cluster.local", + "ContainerRuntime": "docker", + "CRISocket": "", + "NetworkPlugin": "", + "FeatureGates": "", + "ServiceCIDR": "10.96.0.0/12", + "ImageRepository": "", + "ExtraOptions": null, + "ShouldLoadCachedImages": true, + "EnableDefaultCNI": false + } +} \ No newline at end of file diff --git a/pkg/minikube/config/testdata/.minikube/profiles/p2/config.json b/pkg/minikube/config/testdata/.minikube/profiles/p2/config.json new file mode 100644 index 000000000000..d77e0221d298 --- /dev/null +++ b/pkg/minikube/config/testdata/.minikube/profiles/p2/config.json @@ -0,0 +1,49 @@ +{ + "MachineConfig": { + "KeepContext": false, + "MinikubeISO": "https://storage.googleapis.com/minikube/iso/minikube-v1.2.0.iso", + "Memory": 2000, + "CPUs": 2, + "DiskSize": 20000, + "VMDriver": "virtualbox", + "ContainerRuntime": "docker", + "HyperkitVpnKitSock": "", + "HyperkitVSockPorts": [], + "DockerEnv": null, + "InsecureRegistry": null, + "RegistryMirror": null, + "HostOnlyCIDR": "192.168.99.1/24", + "HypervVirtualSwitch": "", + "KVMNetwork": "default", + "KVMQemuURI": "qemu:///system", + "KVMGPU": false, + "KVMHidden": false, + "DockerOpt": null, + "DisableDriverMounts": false, + "NFSShare": [], + "NFSSharesRoot": "/nfsshares", + "UUID": "", + "NoVTXCheck": false, + "DNSProxy": false, + "HostDNSResolver": true + }, + "KubernetesConfig": { + "KubernetesVersion": "v1.15.0", + "NodeIP": "192.168.99.136", + "NodePort": 8443, + "NodeName": "minikube", + "APIServerName": "minikubeCA", + "APIServerNames": null, + "APIServerIPs": null, + "DNSDomain": "cluster.local", + "ContainerRuntime": "docker", + "CRISocket": "", + "NetworkPlugin": "", + "FeatureGates": "", + "ServiceCIDR": "10.96.0.0/12", + "ImageRepository": "", + "ExtraOptions": null, + "ShouldLoadCachedImages": true, + "EnableDefaultCNI": false + } +} \ No newline at end of file diff --git a/pkg/minikube/config/testdata/.minikube/profiles/p3_empty/config.json b/pkg/minikube/config/testdata/.minikube/profiles/p3_empty/config.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/minikube/config/testdata/.minikube/profiles/p4_invalid_file/config.json b/pkg/minikube/config/testdata/.minikube/profiles/p4_invalid_file/config.json new file mode 100644 index 000000000000..9e2e347718f9 --- /dev/null +++ b/pkg/minikube/config/testdata/.minikube/profiles/p4_invalid_file/config.json @@ -0,0 +1 @@ +invalid json file :) \ No newline at end of file diff --git a/pkg/minikube/config/testdata/.minikube/profiles/p5_partial_config/config.json b/pkg/minikube/config/testdata/.minikube/profiles/p5_partial_config/config.json new file mode 100644 index 000000000000..29f62c014986 --- /dev/null +++ b/pkg/minikube/config/testdata/.minikube/profiles/p5_partial_config/config.json @@ -0,0 +1,47 @@ +{ + "MachineConfig": { + "KeepContext": false, + "MinikubeISO": "https://storage.googleapis.com/minikube/iso/minikube-v1.2.0.iso", + "Memory": 2000, + "CPUs": 2, + "DiskSize": 20000, + "ContainerRuntime": "docker", + "HyperkitVpnKitSock": "", + "HyperkitVSockPorts": [], + "XhyveDiskDriver": "ahci-hd", + "DockerEnv": null, + "InsecureRegistry": null, + "RegistryMirror": null, + "HostOnlyCIDR": "192.168.99.1/24", + "HypervVirtualSwitch": "", + "KVMNetwork": "default", + "KVMQemuURI": "qemu:///system", + "KVMGPU": false, + "KVMHidden": false, + "DockerOpt": null, + "DisableDriverMounts": false, + "NFSShare": [], + "NFSSharesRoot": "/nfsshares", + "UUID": "", + "NoVTXCheck": false, + "DNSProxy": false, + "HostDNSResolver": true + }, + "KubernetesConfig": { + "NodePort": 8443, + "NodeName": "minikube", + "APIServerName": "minikubeCA", + "APIServerNames": null, + "APIServerIPs": null, + "DNSDomain": "cluster.local", + "ContainerRuntime": "docker", + "CRISocket": "", + "NetworkPlugin": "", + "FeatureGates": "", + "ServiceCIDR": "10.96.0.0/12", + "ImageRepository": "", + "ExtraOptions": null, + "ShouldLoadCachedImages": true, + "EnableDefaultCNI": false + } +} \ No newline at end of file diff --git a/pkg/minikube/config/types.go b/pkg/minikube/config/types.go index 8d9421ec142e..5716bb4c3336 100644 --- a/pkg/minikube/config/types.go +++ b/pkg/minikube/config/types.go @@ -22,6 +22,12 @@ import ( "k8s.io/minikube/pkg/util" ) +// Profile represents a minikube profile +type Profile struct { + Name string + Config *Config +} + // Config contains machine and k8s config type Config struct { MachineConfig MachineConfig diff --git a/pkg/minikube/constants/constants.go b/pkg/minikube/constants/constants.go index af0adb783557..87bafe085144 100644 --- a/pkg/minikube/constants/constants.go +++ b/pkg/minikube/constants/constants.go @@ -188,13 +188,21 @@ var ConfigFilePath = MakeMiniPath("config") var ConfigFile = MakeMiniPath("config", "config.json") // GetProfileFile returns the Minikube profile config file -func GetProfileFile(profile string) string { - return filepath.Join(GetMinipath(), "profiles", profile, "config.json") +func GetProfileFile(profile string, miniHome ...string) string { + miniPath := GetMinipath() + if len(miniHome) > 0 { + miniPath = miniHome[0] + } + return filepath.Join(miniPath, "profiles", profile, "config.json") } // GetProfilePath returns the Minikube profile path of config file -func GetProfilePath(profile string) string { - return filepath.Join(GetMinipath(), "profiles", profile) +func GetProfilePath(profile string, miniHome ...string) string { + miniPath := GetMinipath() + if len(miniHome) > 0 { + miniPath = miniHome[0] + } + return filepath.Join(miniPath, "profiles", profile) } // AddonsPath is the default path of the addons configuration diff --git a/pkg/minikube/tunnel/test_doubles.go b/pkg/minikube/tunnel/test_doubles.go index c461d8dfd896..8f2e4aaef026 100644 --- a/pkg/minikube/tunnel/test_doubles.go +++ b/pkg/minikube/tunnel/test_doubles.go @@ -86,6 +86,6 @@ type stubConfigLoader struct { e error } -func (l *stubConfigLoader) LoadConfigFromFile(profile string) (*config.Config, error) { +func (l *stubConfigLoader) LoadConfigFromFile(profile string, miniHome ...string) (*config.Config, error) { return l.c, l.e } diff --git a/test/integration/functional_test.go b/test/integration/functional_test.go index 5186eae177b0..5b7d1ba82b86 100644 --- a/test/integration/functional_test.go +++ b/test/integration/functional_test.go @@ -32,7 +32,7 @@ func TestFunctional(t *testing.T) { // This one is not parallel, and ensures the cluster comes up // before we run any other tests. t.Run("Status", testClusterStatus) - + t.Run("ProfileList", testProfileList) t.Run("DNS", testClusterDNS) t.Run("Logs", testClusterLogs) t.Run("Addons", testAddons) diff --git a/test/integration/profile_test.go b/test/integration/profile_test.go new file mode 100644 index 000000000000..8c337523d7e1 --- /dev/null +++ b/test/integration/profile_test.go @@ -0,0 +1,35 @@ +// +build integration + +/* +Copyright 2019 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. +*/ + +package integration + +import ( + "strings" + "testing" +) + +// testProfileList tests the `minikube profile list` command +func testProfileList(t *testing.T) { + t.Parallel() + profile := "minikube" + mk := NewMinikubeRunner(t, "--wait=false") + out := mk.RunCommand("profile list", true) + if !strings.Contains(out, profile) { + t.Errorf("Error , failed to read profile name (%s) in `profile list` command output : \n %q ", profile, out) + } +}