Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Identify minikube cluster for any profile name #4701

Merged
merged 9 commits into from
Aug 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions pkg/skaffold/build/local/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ package local
import (
"context"
"io/ioutil"
"os/exec"
"path/filepath"
"testing"

"github.com/GoogleContainerTools/skaffold/pkg/skaffold/cluster"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner/runcontext"
Expand Down Expand Up @@ -82,6 +84,7 @@ func TestDockerCLIBuild(t *testing.T) {
"docker build . --file "+dockerfilePath+" -t tag --force-rm",
test.expectedEnv,
))
t.Override(&cluster.GetClient, func() cluster.Client { return fakeMinikubeClient{} })
t.Override(&util.OSEnviron, func() []string { return []string{"KEY=VALUE"} })
t.Override(&docker.NewAPIClient, func(*runcontext.RunContext) (docker.LocalDaemon, error) {
return docker.NewLocalDaemon(&testutil.FakeAPIClient{}, test.extraEnv, false, nil), nil
Expand All @@ -104,3 +107,10 @@ func TestDockerCLIBuild(t *testing.T) {
})
}
}

type fakeMinikubeClient struct{}

func (fakeMinikubeClient) IsMinikube(kubeContext string) bool { return false }
func (fakeMinikubeClient) MinikubeExec(arg ...string) (*exec.Cmd, error) {
return exec.Command("minikube", arg...), nil
}
214 changes: 214 additions & 0 deletions pkg/skaffold/cluster/minikube.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*
Copyright 2020 The Skaffold Authors

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 cluster

import (
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"

"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/rest"
"k8s.io/client-go/util/homedir"

"github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/context"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
)

var GetClient = getClient

// To override during tests
var (
minikubeBinaryFunc = minikubeBinary
getRestClientConfigFunc = context.GetRestClientConfig
getClusterInfo = context.GetClusterInfo
)

const (
VirtualBox = "virtualbox"
HyperKit = "hyperkit"
)

type Client interface {
// IsMinikube returns true if the given kubeContext maps to a minikube cluster
IsMinikube(kubeContext string) bool
// MinikubeExec returns the Cmd struct to execute minikube with given arguments
MinikubeExec(arg ...string) (*exec.Cmd, error)
}

type clientImpl struct{}

func getClient() Client {
return clientImpl{}
}

func (clientImpl) IsMinikube(kubeContext string) bool {
// short circuit if context is 'minikube'
if kubeContext == constants.DefaultMinikubeContext {
return true
}
if _, err := minikubeBinaryFunc(); err != nil {
logrus.Tracef("Minikube cluster not detected: %v", err)
return false
}

if ok, err := matchClusterCertPath(kubeContext); err != nil {
logrus.Tracef("failed to match cluster certificate path: %v", err)
} else if ok {
logrus.Debugf("Minikube cluster detected: cluster certificate for context %q found inside the minikube directory", kubeContext)
return true
}

if ok, err := matchProfileAndServerURL(kubeContext); err != nil {
logrus.Tracef("failed to match minikube profile: %v", err)
} else if ok {
logrus.Debugf("Minikube cluster detected: context %q has matching profile name or server url", kubeContext)
return true
}
logrus.Tracef("Minikube cluster not detected for context %q", kubeContext)
return false
}

func (clientImpl) MinikubeExec(arg ...string) (*exec.Cmd, error) {
return minikubeExec(arg...)
}

func minikubeExec(arg ...string) (*exec.Cmd, error) {
b, err := minikubeBinaryFunc()
if err != nil {
return nil, fmt.Errorf("getting minikube executable: %w", err)
}
return exec.Command(b, arg...), nil
}

func minikubeBinary() (string, error) {
execName := "minikube"
if found, _ := util.DetectWSL(); found {
execName = "minikube.exe"
}
filename, err := exec.LookPath(execName)
if err != nil {
return "", errors.New("unable to find minikube executable. Please add it to PATH environment variable")
}
if _, err := os.Stat(filename); os.IsNotExist(err) {
return "", fmt.Errorf("unable to find minikube executable. File not found %s", filename)
}
return filename, nil
}

// matchClusterCertPath checks if the cluster certificate for this context is from inside the minikube directory
func matchClusterCertPath(kubeContext string) (bool, error) {
c, err := getClusterInfo(kubeContext)
if err != nil {
return false, fmt.Errorf("getting kubernetes config: %w", err)
}
if c.CertificateAuthority == "" {
return false, nil
}
return util.IsSubPath(minikubePath(), c.CertificateAuthority), nil
}

// matchProfileAndServerURL checks if kubecontext matches any valid minikube profile
// and for selected drivers if the k8s server url is same as any of the minikube nodes IPs
func matchProfileAndServerURL(kubeContext string) (bool, error) {
config, err := getRestClientConfigFunc()
if err != nil {
return false, fmt.Errorf("getting kubernetes config: %w", err)
}
apiServerURL, _, err := rest.DefaultServerURL(config.Host, config.APIPath, schema.GroupVersion{}, false)

if err != nil {
return false, fmt.Errorf("getting kubernetes server url: %w", err)
}

logrus.Tracef("kubernetes server url: %s", apiServerURL)

ok, err := matchServerURLFor(kubeContext, apiServerURL)
if err != nil {
return false, fmt.Errorf("checking minikube node url: %w", err)
}
return ok, nil
}

func matchServerURLFor(kubeContext string, serverURL *url.URL) (bool, error) {
cmd, _ := minikubeExec("profile", "list", "-o", "json")
out, err := util.RunCmdOut(cmd)
if err != nil {
return false, fmt.Errorf("getting minikube profiles: %w", err)
}

var data data
if err = json.Unmarshal(out, &data); err != nil {
return false, fmt.Errorf("failed to unmarshal data: %w", err)
}

for _, v := range data.Valid {
if v.Config.Name != kubeContext {
continue
}

if v.Config.Driver != HyperKit && v.Config.Driver != VirtualBox {
// Since node IPs don't match server API for other drivers we assume profile name match is enough.
// TODO: Revisit once https://github.com/kubernetes/minikube/issues/6642 is fixed
return true, nil
}
for _, n := range v.Config.Nodes {
if serverURL.Host == fmt.Sprintf("%s:%d", n.IP, n.Port) {
return true, nil
}
}
}
return false, nil
}

// minikubePath returns the path to the user's minikube dir
func minikubePath() string {
minikubeHomeEnv := os.Getenv("MINIKUBE_HOME")
if minikubeHomeEnv == "" {
return filepath.Join(homedir.HomeDir(), ".minikube")
}
if filepath.Base(minikubeHomeEnv) == ".minikube" {
return minikubeHomeEnv
}
return filepath.Join(minikubeHomeEnv, ".minikube")
}

type data struct {
Valid []profile `json:"valid,omitempty"`
Invalid []profile `json:"invalid,omitempty"`
}

type profile struct {
Config config
}

type config struct {
Name string
Driver string
Nodes []node
}

type node struct {
IP string
Port int32
}
131 changes: 131 additions & 0 deletions pkg/skaffold/cluster/minikube_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
Copyright 2020 The Skaffold Authors

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 cluster

import (
"fmt"
"path/filepath"
"testing"

"k8s.io/client-go/rest"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/client-go/util/homedir"

"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
"github.com/GoogleContainerTools/skaffold/testutil"
)

func TestClientImpl_IsMinikube(t *testing.T) {
home := homedir.HomeDir()
tests := []struct {
description string
kubeContext string
clusterInfo clientcmdapi.Cluster
config rest.Config
minikubeProfileCmd util.Command
minikubeNotInPath bool
expected bool
}{
{
description: "context is 'minikube'",
kubeContext: "minikube",
expected: true,
},
{
description: "'minikube' binary not found",
kubeContext: "test-cluster",
minikubeNotInPath: true,
expected: false,
},
{
description: "cluster cert inside minikube dir",
kubeContext: "test-cluster",
clusterInfo: clientcmdapi.Cluster{
CertificateAuthority: filepath.Join(home, ".minikube", "ca.crt"),
},
expected: true,
},
{
description: "cluster cert outside minikube dir",
kubeContext: "test-cluster",
clusterInfo: clientcmdapi.Cluster{
CertificateAuthority: filepath.Join(home, "foo", "ca.crt"),
},
expected: false,
},
{
description: "minikube profile name with docker driver matches kubeContext",
kubeContext: "test-cluster",
config: rest.Config{
Host: "127.0.0.1:32768",
},
minikubeProfileCmd: testutil.CmdRunOut("minikube profile list -o json", fmt.Sprintf(profileStr, "test-cluster", "docker", "172.17.0.3", 8443)),
expected: true,
},
{
description: "minikube profile name with hyperkit driver node ip matches api server url",
kubeContext: "test-cluster",
config: rest.Config{
Host: "192.168.64.10:8443",
},
minikubeProfileCmd: testutil.CmdRunOut("minikube profile list -o json", fmt.Sprintf(profileStr, "test-cluster", "hyperkit", "192.168.64.10", 8443)),
expected: true,
},
{
description: "minikube profile name different from kubeContext",
kubeContext: "test-cluster",
config: rest.Config{
Host: "127.0.0.1:32768",
},
minikubeProfileCmd: testutil.CmdRunOut("minikube profile list -o json", fmt.Sprintf(profileStr, "test-cluster2", "docker", "172.17.0.3", 8443)),
expected: false,
},
{
description: "cannot parse minikube profile list",
kubeContext: "test-cluster",
minikubeProfileCmd: testutil.CmdRunOut("minikube profile list -o json", `{"foo":"bar"}`),
expected: false,
},
{
description: "minikube with hyperkit driver node ip different from api server url",
kubeContext: "test-cluster",
config: rest.Config{
Host: "192.168.64.10:8443",
},
minikubeProfileCmd: testutil.CmdRunOut("minikube profile list -o json", fmt.Sprintf(profileStr, "test-cluster", "hyperkit", "192.168.64.11", 8443)),
expected: false,
},
}

for _, test := range tests {
testutil.Run(t, test.description, func(t *testutil.T) {
if test.minikubeNotInPath {
t.Override(&minikubeBinaryFunc, func() (string, error) { return "", fmt.Errorf("minikube not in PATH") })
} else {
t.Override(&minikubeBinaryFunc, func() (string, error) { return "minikube", nil })
}
t.Override(&util.DefaultExecCommand, test.minikubeProfileCmd)
t.Override(&getRestClientConfigFunc, func() (*rest.Config, error) { return &test.config, nil })
t.Override(&getClusterInfo, func(string) (*clientcmdapi.Cluster, error) { return &test.clusterInfo, nil })

ok := GetClient().IsMinikube(test.kubeContext)
t.CheckDeepEqual(test.expected, ok)
})
}
}

var profileStr = `{"invalid": [],"valid": [{"Name": "minikube","Status": "Stopped","Config": {"Name": "%s","Driver": "%s","Nodes": [{"Name": "","IP": "%s","Port": %d}]}}]}`
Loading