Skip to content

Commit 67e81bc

Browse files
gsquared94tejal29
andauthored
Identify minikube cluster for any profile name (#4701)
* Move `DetectWSL` function into util package * Add heuristics to detect minikube cluster * Remove label matching; add cluster cert path matching * change logs to trace level * simplify * Update pkg/skaffold/cluster/minikube.go Co-authored-by: Tejal Desai <[email protected]> * appease linters * test failure Co-authored-by: Tejal Desai <[email protected]>
1 parent abdbe17 commit 67e81bc

File tree

9 files changed

+451
-44
lines changed

9 files changed

+451
-44
lines changed

pkg/skaffold/build/local/docker_test.go

+10
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ package local
1919
import (
2020
"context"
2121
"io/ioutil"
22+
"os/exec"
2223
"path/filepath"
2324
"testing"
2425

26+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/cluster"
2527
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
2628
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
2729
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner/runcontext"
@@ -82,6 +84,7 @@ func TestDockerCLIBuild(t *testing.T) {
8284
"docker build . --file "+dockerfilePath+" -t tag --force-rm",
8385
test.expectedEnv,
8486
))
87+
t.Override(&cluster.GetClient, func() cluster.Client { return fakeMinikubeClient{} })
8588
t.Override(&util.OSEnviron, func() []string { return []string{"KEY=VALUE"} })
8689
t.Override(&docker.NewAPIClient, func(*runcontext.RunContext) (docker.LocalDaemon, error) {
8790
return docker.NewLocalDaemon(&testutil.FakeAPIClient{}, test.extraEnv, false, nil), nil
@@ -104,3 +107,10 @@ func TestDockerCLIBuild(t *testing.T) {
104107
})
105108
}
106109
}
110+
111+
type fakeMinikubeClient struct{}
112+
113+
func (fakeMinikubeClient) IsMinikube(kubeContext string) bool { return false }
114+
func (fakeMinikubeClient) MinikubeExec(arg ...string) (*exec.Cmd, error) {
115+
return exec.Command("minikube", arg...), nil
116+
}

pkg/skaffold/cluster/minikube.go

+214
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
Copyright 2020 The Skaffold Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cluster
18+
19+
import (
20+
"encoding/json"
21+
"errors"
22+
"fmt"
23+
"net/url"
24+
"os"
25+
"os/exec"
26+
"path/filepath"
27+
28+
"github.com/sirupsen/logrus"
29+
"k8s.io/apimachinery/pkg/runtime/schema"
30+
"k8s.io/client-go/rest"
31+
"k8s.io/client-go/util/homedir"
32+
33+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants"
34+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/context"
35+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
36+
)
37+
38+
var GetClient = getClient
39+
40+
// To override during tests
41+
var (
42+
minikubeBinaryFunc = minikubeBinary
43+
getRestClientConfigFunc = context.GetRestClientConfig
44+
getClusterInfo = context.GetClusterInfo
45+
)
46+
47+
const (
48+
VirtualBox = "virtualbox"
49+
HyperKit = "hyperkit"
50+
)
51+
52+
type Client interface {
53+
// IsMinikube returns true if the given kubeContext maps to a minikube cluster
54+
IsMinikube(kubeContext string) bool
55+
// MinikubeExec returns the Cmd struct to execute minikube with given arguments
56+
MinikubeExec(arg ...string) (*exec.Cmd, error)
57+
}
58+
59+
type clientImpl struct{}
60+
61+
func getClient() Client {
62+
return clientImpl{}
63+
}
64+
65+
func (clientImpl) IsMinikube(kubeContext string) bool {
66+
// short circuit if context is 'minikube'
67+
if kubeContext == constants.DefaultMinikubeContext {
68+
return true
69+
}
70+
if _, err := minikubeBinaryFunc(); err != nil {
71+
logrus.Tracef("Minikube cluster not detected: %v", err)
72+
return false
73+
}
74+
75+
if ok, err := matchClusterCertPath(kubeContext); err != nil {
76+
logrus.Tracef("failed to match cluster certificate path: %v", err)
77+
} else if ok {
78+
logrus.Debugf("Minikube cluster detected: cluster certificate for context %q found inside the minikube directory", kubeContext)
79+
return true
80+
}
81+
82+
if ok, err := matchProfileAndServerURL(kubeContext); err != nil {
83+
logrus.Tracef("failed to match minikube profile: %v", err)
84+
} else if ok {
85+
logrus.Debugf("Minikube cluster detected: context %q has matching profile name or server url", kubeContext)
86+
return true
87+
}
88+
logrus.Tracef("Minikube cluster not detected for context %q", kubeContext)
89+
return false
90+
}
91+
92+
func (clientImpl) MinikubeExec(arg ...string) (*exec.Cmd, error) {
93+
return minikubeExec(arg...)
94+
}
95+
96+
func minikubeExec(arg ...string) (*exec.Cmd, error) {
97+
b, err := minikubeBinaryFunc()
98+
if err != nil {
99+
return nil, fmt.Errorf("getting minikube executable: %w", err)
100+
}
101+
return exec.Command(b, arg...), nil
102+
}
103+
104+
func minikubeBinary() (string, error) {
105+
execName := "minikube"
106+
if found, _ := util.DetectWSL(); found {
107+
execName = "minikube.exe"
108+
}
109+
filename, err := exec.LookPath(execName)
110+
if err != nil {
111+
return "", errors.New("unable to find minikube executable. Please add it to PATH environment variable")
112+
}
113+
if _, err := os.Stat(filename); os.IsNotExist(err) {
114+
return "", fmt.Errorf("unable to find minikube executable. File not found %s", filename)
115+
}
116+
return filename, nil
117+
}
118+
119+
// matchClusterCertPath checks if the cluster certificate for this context is from inside the minikube directory
120+
func matchClusterCertPath(kubeContext string) (bool, error) {
121+
c, err := getClusterInfo(kubeContext)
122+
if err != nil {
123+
return false, fmt.Errorf("getting kubernetes config: %w", err)
124+
}
125+
if c.CertificateAuthority == "" {
126+
return false, nil
127+
}
128+
return util.IsSubPath(minikubePath(), c.CertificateAuthority), nil
129+
}
130+
131+
// matchProfileAndServerURL checks if kubecontext matches any valid minikube profile
132+
// and for selected drivers if the k8s server url is same as any of the minikube nodes IPs
133+
func matchProfileAndServerURL(kubeContext string) (bool, error) {
134+
config, err := getRestClientConfigFunc()
135+
if err != nil {
136+
return false, fmt.Errorf("getting kubernetes config: %w", err)
137+
}
138+
apiServerURL, _, err := rest.DefaultServerURL(config.Host, config.APIPath, schema.GroupVersion{}, false)
139+
140+
if err != nil {
141+
return false, fmt.Errorf("getting kubernetes server url: %w", err)
142+
}
143+
144+
logrus.Tracef("kubernetes server url: %s", apiServerURL)
145+
146+
ok, err := matchServerURLFor(kubeContext, apiServerURL)
147+
if err != nil {
148+
return false, fmt.Errorf("checking minikube node url: %w", err)
149+
}
150+
return ok, nil
151+
}
152+
153+
func matchServerURLFor(kubeContext string, serverURL *url.URL) (bool, error) {
154+
cmd, _ := minikubeExec("profile", "list", "-o", "json")
155+
out, err := util.RunCmdOut(cmd)
156+
if err != nil {
157+
return false, fmt.Errorf("getting minikube profiles: %w", err)
158+
}
159+
160+
var data data
161+
if err = json.Unmarshal(out, &data); err != nil {
162+
return false, fmt.Errorf("failed to unmarshal data: %w", err)
163+
}
164+
165+
for _, v := range data.Valid {
166+
if v.Config.Name != kubeContext {
167+
continue
168+
}
169+
170+
if v.Config.Driver != HyperKit && v.Config.Driver != VirtualBox {
171+
// Since node IPs don't match server API for other drivers we assume profile name match is enough.
172+
// TODO: Revisit once https://github.com/kubernetes/minikube/issues/6642 is fixed
173+
return true, nil
174+
}
175+
for _, n := range v.Config.Nodes {
176+
if serverURL.Host == fmt.Sprintf("%s:%d", n.IP, n.Port) {
177+
return true, nil
178+
}
179+
}
180+
}
181+
return false, nil
182+
}
183+
184+
// minikubePath returns the path to the user's minikube dir
185+
func minikubePath() string {
186+
minikubeHomeEnv := os.Getenv("MINIKUBE_HOME")
187+
if minikubeHomeEnv == "" {
188+
return filepath.Join(homedir.HomeDir(), ".minikube")
189+
}
190+
if filepath.Base(minikubeHomeEnv) == ".minikube" {
191+
return minikubeHomeEnv
192+
}
193+
return filepath.Join(minikubeHomeEnv, ".minikube")
194+
}
195+
196+
type data struct {
197+
Valid []profile `json:"valid,omitempty"`
198+
Invalid []profile `json:"invalid,omitempty"`
199+
}
200+
201+
type profile struct {
202+
Config config
203+
}
204+
205+
type config struct {
206+
Name string
207+
Driver string
208+
Nodes []node
209+
}
210+
211+
type node struct {
212+
IP string
213+
Port int32
214+
}

pkg/skaffold/cluster/minikube_test.go

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
Copyright 2020 The Skaffold Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cluster
18+
19+
import (
20+
"fmt"
21+
"path/filepath"
22+
"testing"
23+
24+
"k8s.io/client-go/rest"
25+
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
26+
"k8s.io/client-go/util/homedir"
27+
28+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
29+
"github.com/GoogleContainerTools/skaffold/testutil"
30+
)
31+
32+
func TestClientImpl_IsMinikube(t *testing.T) {
33+
home := homedir.HomeDir()
34+
tests := []struct {
35+
description string
36+
kubeContext string
37+
clusterInfo clientcmdapi.Cluster
38+
config rest.Config
39+
minikubeProfileCmd util.Command
40+
minikubeNotInPath bool
41+
expected bool
42+
}{
43+
{
44+
description: "context is 'minikube'",
45+
kubeContext: "minikube",
46+
expected: true,
47+
},
48+
{
49+
description: "'minikube' binary not found",
50+
kubeContext: "test-cluster",
51+
minikubeNotInPath: true,
52+
expected: false,
53+
},
54+
{
55+
description: "cluster cert inside minikube dir",
56+
kubeContext: "test-cluster",
57+
clusterInfo: clientcmdapi.Cluster{
58+
CertificateAuthority: filepath.Join(home, ".minikube", "ca.crt"),
59+
},
60+
expected: true,
61+
},
62+
{
63+
description: "cluster cert outside minikube dir",
64+
kubeContext: "test-cluster",
65+
clusterInfo: clientcmdapi.Cluster{
66+
CertificateAuthority: filepath.Join(home, "foo", "ca.crt"),
67+
},
68+
expected: false,
69+
},
70+
{
71+
description: "minikube profile name with docker driver matches kubeContext",
72+
kubeContext: "test-cluster",
73+
config: rest.Config{
74+
Host: "127.0.0.1:32768",
75+
},
76+
minikubeProfileCmd: testutil.CmdRunOut("minikube profile list -o json", fmt.Sprintf(profileStr, "test-cluster", "docker", "172.17.0.3", 8443)),
77+
expected: true,
78+
},
79+
{
80+
description: "minikube profile name with hyperkit driver node ip matches api server url",
81+
kubeContext: "test-cluster",
82+
config: rest.Config{
83+
Host: "192.168.64.10:8443",
84+
},
85+
minikubeProfileCmd: testutil.CmdRunOut("minikube profile list -o json", fmt.Sprintf(profileStr, "test-cluster", "hyperkit", "192.168.64.10", 8443)),
86+
expected: true,
87+
},
88+
{
89+
description: "minikube profile name different from kubeContext",
90+
kubeContext: "test-cluster",
91+
config: rest.Config{
92+
Host: "127.0.0.1:32768",
93+
},
94+
minikubeProfileCmd: testutil.CmdRunOut("minikube profile list -o json", fmt.Sprintf(profileStr, "test-cluster2", "docker", "172.17.0.3", 8443)),
95+
expected: false,
96+
},
97+
{
98+
description: "cannot parse minikube profile list",
99+
kubeContext: "test-cluster",
100+
minikubeProfileCmd: testutil.CmdRunOut("minikube profile list -o json", `{"foo":"bar"}`),
101+
expected: false,
102+
},
103+
{
104+
description: "minikube with hyperkit driver node ip different from api server url",
105+
kubeContext: "test-cluster",
106+
config: rest.Config{
107+
Host: "192.168.64.10:8443",
108+
},
109+
minikubeProfileCmd: testutil.CmdRunOut("minikube profile list -o json", fmt.Sprintf(profileStr, "test-cluster", "hyperkit", "192.168.64.11", 8443)),
110+
expected: false,
111+
},
112+
}
113+
114+
for _, test := range tests {
115+
testutil.Run(t, test.description, func(t *testutil.T) {
116+
if test.minikubeNotInPath {
117+
t.Override(&minikubeBinaryFunc, func() (string, error) { return "", fmt.Errorf("minikube not in PATH") })
118+
} else {
119+
t.Override(&minikubeBinaryFunc, func() (string, error) { return "minikube", nil })
120+
}
121+
t.Override(&util.DefaultExecCommand, test.minikubeProfileCmd)
122+
t.Override(&getRestClientConfigFunc, func() (*rest.Config, error) { return &test.config, nil })
123+
t.Override(&getClusterInfo, func(string) (*clientcmdapi.Cluster, error) { return &test.clusterInfo, nil })
124+
125+
ok := GetClient().IsMinikube(test.kubeContext)
126+
t.CheckDeepEqual(test.expected, ok)
127+
})
128+
}
129+
}
130+
131+
var profileStr = `{"invalid": [],"valid": [{"Name": "minikube","Status": "Stopped","Config": {"Name": "%s","Driver": "%s","Nodes": [{"Name": "","IP": "%s","Port": %d}]}}]}`

0 commit comments

Comments
 (0)