diff --git a/cmd/minikube/cmd/status.go b/cmd/minikube/cmd/status.go index 0f118fbde0f672f84d1183c67e188c02c2c5374e..7e9ab6b27cda610396326b261c8124f95aeff5e6 100644 --- a/cmd/minikube/cmd/status.go +++ b/cmd/minikube/cmd/status.go @@ -27,14 +27,16 @@ import ( cmdUtil "k8s.io/minikube/cmd/util" "k8s.io/minikube/pkg/minikube/cluster" "k8s.io/minikube/pkg/minikube/constants" + kcfg "k8s.io/minikube/pkg/minikube/kubeconfig" "k8s.io/minikube/pkg/minikube/machine" ) var statusFormat string type Status struct { - MinikubeStatus string - LocalkubeStatus string + MinikubeStatus string + LocalkubeStatus string + KubeconfigStatus string } // statusCmd represents the status command @@ -57,14 +59,32 @@ var statusCmd = &cobra.Command{ } ls := state.None.String() + ks := state.None.String() if ms == state.Running.String() { ls, err = cluster.GetLocalkubeStatus(api) if err != nil { glog.Errorln("Error localkube status:", err) cmdUtil.MaybeReportErrorAndExit(err) } + ip, err := cluster.GetHostDriverIP(api) + if err != nil { + glog.Errorln("Error host driver ip status:", err) + cmdUtil.MaybeReportErrorAndExit(err) + } + kstatus, err := kcfg.GetKubeConfigStatus(ip, constants.KubeconfigPath) + if err != nil { + glog.Errorln("Error kubeconfig status:", err) + cmdUtil.MaybeReportErrorAndExit(err) + } + if kstatus { + ks = "Correctly Configured: pointing to minikube-vm at " + ip.String() + } else { + ks = "Misconfigured: pointing to stale minikube-vm." + + "\nTo fix the kubectl context, run minikube update-context" + } } - status := Status{ms, ls} + + status := Status{ms, ls, ks} tmpl, err := template.New("status").Parse(statusFormat) if err != nil { diff --git a/cmd/minikube/cmd/update-context.go b/cmd/minikube/cmd/update-context.go new file mode 100644 index 0000000000000000000000000000000000000000..3b889c2bfb9524d29164301c85ed7fb77dcdddff --- /dev/null +++ b/cmd/minikube/cmd/update-context.go @@ -0,0 +1,66 @@ +/* +Copyright 2016 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 cmd + +import ( + "fmt" + "os" + + "github.com/golang/glog" + "github.com/spf13/cobra" + cmdUtil "k8s.io/minikube/cmd/util" + "k8s.io/minikube/pkg/minikube/cluster" + "k8s.io/minikube/pkg/minikube/constants" + kcfg "k8s.io/minikube/pkg/minikube/kubeconfig" + "k8s.io/minikube/pkg/minikube/machine" +) + +// updateContextCmd represents the update-context command +var updateContextCmd = &cobra.Command{ + Use: "update-context", + Short: "Verify the IP address of the running cluster in kubeconfig.", + Long: `Retrieves the IP address of the running cluster, checks it + with IP in kubeconfig, and corrects kubeconfig if incorrect.`, + Run: func(cmd *cobra.Command, args []string) { + api, err := machine.NewAPIClient(clientType) + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting client: %s\n", err) + os.Exit(1) + } + defer api.Close() + ip, err := cluster.GetHostDriverIP(api) + if err != nil { + glog.Errorln("Error host driver ip status:", err) + cmdUtil.MaybeReportErrorAndExit(err) + } + kstatus, err := kcfg.UpdateKubeconfigIP(ip, constants.KubeconfigPath) + if err != nil { + glog.Errorln("Error kubeconfig status:", err) + cmdUtil.MaybeReportErrorAndExit(err) + } + if kstatus { + fmt.Println("Reconfigured kubeconfig IP, now pointing at " + ip.String()) + } else { + fmt.Println("Kubeconfig IP correctly configured, pointing at " + ip.String()) + } + + }, +} + +func init() { + RootCmd.AddCommand(updateContextCmd) +} diff --git a/pkg/minikube/cluster/cluster.go b/pkg/minikube/cluster/cluster.go index bcb344f2039f1dc11869c114cb0afd2e07068bc5..b3480be7a02a04a5882548c5f0fa16effa82d342 100644 --- a/pkg/minikube/cluster/cluster.go +++ b/pkg/minikube/cluster/cluster.go @@ -165,6 +165,24 @@ func GetLocalkubeStatus(api libmachine.API) (string, error) { } } +// GetHostDriverIP gets the ip address of the current minikube cluster +func GetHostDriverIP(api libmachine.API) (net.IP, error) { + host, err := CheckIfApiExistsAndLoad(api) + if err != nil { + return nil, err + } + + ipStr, err := host.Driver.GetIP() + if err != nil { + return nil, errors.Wrap(err, "Error getting IP") + } + ip := net.ParseIP(ipStr) + if ip == nil { + return nil, errors.Wrap(err, "Error parsing IP") + } + return ip, nil +} + // StartCluster starts a k8s cluster on the specified Host. func StartCluster(api libmachine.API, kubernetesConfig KubernetesConfig) error { h, err := CheckIfApiExistsAndLoad(api) diff --git a/pkg/minikube/constants/constants.go b/pkg/minikube/constants/constants.go index 74fe078b80478ab5a88dce021b59f35af5d88eb2..7c6f665828cff23a2eee03d6a2bf8944caf7f9ae 100644 --- a/pkg/minikube/constants/constants.go +++ b/pkg/minikube/constants/constants.go @@ -90,7 +90,7 @@ const ( MinimumDiskSizeMB = 2000 DefaultVMDriver = "virtualbox" DefaultStatusFormat = "minikube: {{.MinikubeStatus}}\n" + - "localkube: {{.LocalkubeStatus}}\n" + "localkube: {{.LocalkubeStatus}}\n" + "kubectl: {{.KubeconfigStatus}}\n" DefaultAddonListFormat = "- {{.AddonName}}: {{.AddonStatus}}\n" DefaultConfigViewFormat = "- {{.ConfigKey}}: {{.ConfigValue}}\n" GithubMinikubeReleasesURL = "https://storage.googleapis.com/minikube/releases.json" diff --git a/pkg/minikube/kubeconfig/config.go b/pkg/minikube/kubeconfig/config.go index c250d2404ed9857febc69ecc99b04bcd888ba241..32c8d98d0316d1cdc8d860b7746fe3b5bae858fc 100644 --- a/pkg/minikube/kubeconfig/config.go +++ b/pkg/minikube/kubeconfig/config.go @@ -17,9 +17,13 @@ limitations under the License. package kubeconfig import ( + "fmt" "io/ioutil" + "net" + "net/url" "os" "path/filepath" + "strconv" "sync/atomic" "github.com/golang/glog" @@ -27,6 +31,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/clientcmd/api" "k8s.io/client-go/tools/clientcmd/api/latest" + cfg "k8s.io/minikube/pkg/minikube/config" + "k8s.io/minikube/pkg/minikube/constants" ) type KubeConfigSetup struct { @@ -178,3 +184,68 @@ func decode(data []byte) (*api.Config, error) { return config.(*api.Config), nil } + +// GetKubeConfigStatus verifys the ip stored in kubeconfig. +func GetKubeConfigStatus(ip net.IP, filename string) (bool, error) { + if ip == nil { + return false, fmt.Errorf("Error, empty ip passed") + } + kip, err := getIPFromKubeConfig(filename) + if err != nil { + return false, err + } + if kip.Equal(ip) { + return true, nil + } + // Kubeconfig IP misconfigured + return false, nil + +} + +// UpdateKubeconfigIP overwrites the IP stored in kubeconfig with the provided IP. +func UpdateKubeconfigIP(ip net.IP, filename string) (bool, error) { + if ip == nil { + return false, fmt.Errorf("Error, empty ip passed") + } + kip, err := getIPFromKubeConfig(filename) + if err != nil { + return false, err + } + if kip.Equal(ip) { + return false, nil + } + con, err := ReadConfigOrNew(filename) + if err != nil { + return false, errors.Wrap(err, "Error getting kubeconfig status") + } + // Safe to lookup server because if field non-existent getIPFromKubeconfig would have given an error + con.Clusters[cfg.GetMachineName()].Server = "https://" + ip.String() + ":" + strconv.Itoa(constants.APIServerPort) + err = WriteConfig(con, filename) + if err != nil { + return false, err + } + // Kubeconfig IP reconfigured + return true, nil +} + +// getIPFromKubeConfig returns the IP address stored for minikube in the kubeconfig specified +func getIPFromKubeConfig(filename string) (net.IP, error) { + con, err := ReadConfigOrNew(filename) + if err != nil { + return nil, errors.Wrap(err, "Error getting kubeconfig status") + } + cluster, ok := con.Clusters[cfg.GetMachineName()] + if !ok { + return nil, errors.Errorf("Kubeconfig does not have a record of the machine cluster") + } + kurl, err := url.Parse(cluster.Server) + if err != nil { + return net.ParseIP(cluster.Server), nil + } + kip, _, err := net.SplitHostPort(kurl.Host) + if err != nil { + return net.ParseIP(kurl.Host), nil + } + ip := net.ParseIP(kip) + return ip, nil +} diff --git a/pkg/minikube/kubeconfig/config_test.go b/pkg/minikube/kubeconfig/config_test.go index fc83334f862f65751ea3a2841e952a4fd12fdc71..6ef482cec9a0a873acc153b31dbd13cce187ec23 100644 --- a/pkg/minikube/kubeconfig/config_test.go +++ b/pkg/minikube/kubeconfig/config_test.go @@ -18,6 +18,7 @@ package kubeconfig import ( "io/ioutil" + "net" "os" "path/filepath" "strconv" @@ -49,6 +50,50 @@ users: client-key: /home/la-croix/apiserver.key `) +var fakeKubeCfg2 = []byte(` +apiVersion: v1 +clusters: +- cluster: + certificate-authority: /home/la-croix/apiserver.crt + server: https://192.168.10.100:8443 + name: minikube +contexts: +- context: + cluster: la-croix + user: la-croix + name: la-croix +current-context: la-croix +kind: Config +preferences: {} +users: +- name: la-croix + user: + client-certificate: /home/la-croix/apiserver.crt + client-key: /home/la-croix/apiserver.key +`) + +var fakeKubeCfg3 = []byte(` +apiVersion: v1 +clusters: +- cluster: + certificate-authority: /home/la-croix/apiserver.crt + server: https://192.168.1.1:8443 + name: minikube +contexts: +- context: + cluster: la-croix + user: la-croix + name: la-croix +current-context: la-croix +kind: Config +preferences: {} +users: +- name: la-croix + user: + client-certificate: /home/la-croix/apiserver.crt + client-key: /home/la-croix/apiserver.key +`) + func TestSetupKubeConfig(t *testing.T) { setupCfg := &KubeConfigSetup{ ClusterName: "test", @@ -128,6 +173,116 @@ func TestSetupKubeConfig(t *testing.T) { } } +func TestGetKubeConfigStatus(t *testing.T) { + + var tests = []struct { + description string + ip net.IP + existing []byte + err bool + status bool + }{ + { + description: "empty ip", + ip: nil, + existing: fakeKubeCfg, + err: true, + }, + { + description: "no minikube cluster", + ip: net.ParseIP("192.168.10.100"), + existing: fakeKubeCfg, + err: true, + }, + { + description: "exactly matching ip", + ip: net.ParseIP("192.168.10.100"), + existing: fakeKubeCfg2, + status: true, + }, + { + description: "different ips", + ip: net.ParseIP("192.168.10.100"), + existing: fakeKubeCfg3, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + t.Parallel() + configFilename := tempFile(t, test.existing) + statusActual, err := GetKubeConfigStatus(test.ip, configFilename) + if err != nil && !test.err { + t.Errorf("Got unexpected error: %s", err) + } + if err == nil && test.err { + t.Errorf("Expected error but got none: %s", err) + } + if test.status != statusActual { + t.Errorf("Expected status %t, but got %t", test.status, statusActual) + } + }) + + } +} + +func TestUpdateKubeconfigIP(t *testing.T) { + + var tests = []struct { + description string + ip net.IP + existing []byte + err bool + status bool + expCfg []byte + }{ + { + description: "empty ip", + ip: nil, + existing: fakeKubeCfg2, + err: true, + expCfg: fakeKubeCfg2, + }, + { + description: "no minikube cluster", + ip: net.ParseIP("192.168.10.100"), + existing: fakeKubeCfg, + err: true, + expCfg: fakeKubeCfg, + }, + { + description: "same IP", + ip: net.ParseIP("192.168.10.100"), + existing: fakeKubeCfg2, + expCfg: fakeKubeCfg2, + }, + { + description: "different IP", + ip: net.ParseIP("192.168.10.100"), + existing: fakeKubeCfg3, + status: true, + expCfg: fakeKubeCfg2, + }, + } + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + t.Parallel() + configFilename := tempFile(t, test.existing) + statusActual, err := UpdateKubeconfigIP(test.ip, configFilename) + if err != nil && !test.err { + t.Errorf("Got unexpected error: %s", err) + } + if err == nil && test.err { + t.Errorf("Expected error but got none: %s", err) + } + if test.status != statusActual { + t.Errorf("Expected status %t, but got %t", test.status, statusActual) + } + }) + + } +} + func TestEmptyConfig(t *testing.T) { tmp := tempFile(t, []byte{}) defer os.Remove(tmp) @@ -178,6 +333,42 @@ func TestNewConfig(t *testing.T) { } } +func TestGetIPFromKubeConfig(t *testing.T) { + + var tests = []struct { + description string + cfg []byte + ip net.IP + err bool + }{ + { + description: "normal IP", + cfg: fakeKubeCfg2, + ip: net.ParseIP("192.168.10.100"), + }, + { + description: "no minikube cluster", + cfg: fakeKubeCfg, + err: true, + }, + } + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + configFilename := tempFile(t, test.cfg) + ip, err := getIPFromKubeConfig(configFilename) + if err != nil && !test.err { + t.Errorf("Got unexpected error: %s", err) + } + if err == nil && test.err { + t.Errorf("Expected error but got none: %s", err) + } + if !ip.Equal(test.ip) { + t.Errorf("IP returned: %s does not match ip given: %s", ip, test.ip) + } + }) + } +} + // tempFile creates a temporary with the provided bytes as its contents. // The caller is responsible for deleting file after use. func tempFile(t *testing.T, data []byte) string {