Skip to content
Open
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
23 changes: 16 additions & 7 deletions pkg/devspace/kubectl/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package kubectl

import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
Expand Down Expand Up @@ -255,20 +256,28 @@ func IsMinikubeKubernetes(kubeClient Client) bool {
if rawConfig, err := kubeClient.ClientConfig().RawConfig(); err == nil {
clusters := rawConfig.Clusters[rawConfig.Contexts[rawConfig.CurrentContext].Cluster]
for _, extension := range clusters.Extensions {
ext, err := runtime.DefaultUnstructuredConverter.ToUnstructured(extension)
if err == nil {
if provider, ok := ext["provider"].(string); ok {
if provider == minikubeProvider {
return true
}
}
if isMinikubeExtension(extension) {
return true
}
}
}

return false
}

func isMinikubeExtension(extension runtime.Object) bool {
unknown, ok := extension.(*runtime.Unknown)
if !ok {
return false
}
var ext map[string]interface{}
if err := json.Unmarshal(unknown.Raw, &ext); err != nil {
return false
}
provider, ok := ext["provider"].(string)
return ok && provider == minikubeProvider
}

// GetKindContext returns the kind cluster name
func GetKindContext(context string) string {
if !strings.HasPrefix(context, "kind-") {
Expand Down
132 changes: 132 additions & 0 deletions pkg/devspace/kubectl/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package kubectl

import (
"context"
"io"
"testing"

"github.com/loft-sh/devspace/pkg/util/kubeconfig"
"gotest.tools/assert"
k8sv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)

// minimalClient implements kubectl.Client with only the two methods that
// IsMinikubeKubernetes actually calls. All other methods panic if invoked.
type minimalClient struct {
context string
clientConfig clientcmd.ClientConfig
}

func (c *minimalClient) CurrentContext() string { return c.context }
func (c *minimalClient) ClientConfig() clientcmd.ClientConfig { return c.clientConfig }
func (c *minimalClient) KubeClient() kubernetes.Interface { panic("not implemented") }
func (c *minimalClient) Namespace() string { panic("not implemented") }
func (c *minimalClient) RestConfig() *rest.Config { panic("not implemented") }
func (c *minimalClient) KubeConfigLoader() kubeconfig.Loader { panic("not implemented") }
func (c *minimalClient) IsInCluster() bool { panic("not implemented") }
func (c *minimalClient) CopyFromReader(_ context.Context, _ *k8sv1.Pod, _, _ string, _ io.Reader) error {
panic("not implemented")
}
func (c *minimalClient) Copy(_ context.Context, _ *k8sv1.Pod, _, _, _ string, _ []string) error {
panic("not implemented")
}
func (c *minimalClient) ExecStream(_ context.Context, _ *ExecStreamOptions) error {
panic("not implemented")
}
func (c *minimalClient) ExecBuffered(_ context.Context, _ *k8sv1.Pod, _ string, _ []string, _ io.Reader) ([]byte, []byte, error) {
panic("not implemented")
}
func (c *minimalClient) ExecBufferedCombined(_ context.Context, _ *k8sv1.Pod, _ string, _ []string, _ io.Reader) ([]byte, error) {
panic("not implemented")
}
func (c *minimalClient) GenericRequest(_ context.Context, _ *GenericRequestOptions) (string, error) {
panic("not implemented")
}
func (c *minimalClient) ReadLogs(_ context.Context, _, _, _ string, _ bool, _ *int64) (string, error) {
panic("not implemented")
}
func (c *minimalClient) Logs(_ context.Context, _, _, _ string, _ bool, _ *int64, _ bool) (io.ReadCloser, error) {
panic("not implemented")
}
func (c *minimalClient) EnsureNamespace(_ context.Context, _ string, _ interface{ Debug(args ...interface{}) }) error {
panic("not implemented")
}

// makeClient builds a test Client whose current context points at a cluster
// with the given extensions map.
func makeClient(contextName string, extensions map[string]runtime.Object) *minimalClient {
apiCfg := clientcmdapi.NewConfig()
apiCfg.Clusters[contextName] = &clientcmdapi.Cluster{
Server: "https://example.test:6443",
Extensions: extensions,
}
apiCfg.Contexts[contextName] = &clientcmdapi.Context{
Cluster: contextName,
}
apiCfg.CurrentContext = contextName

cfg := clientcmd.NewNonInteractiveClientConfig(
*apiCfg,
contextName,
&clientcmd.ConfigOverrides{},
nil,
)
return &minimalClient{context: contextName, clientConfig: cfg}
}

func TestIsMinikubeKubernetes(t *testing.T) {
t.Run("nil client returns false", func(t *testing.T) {
assert.Equal(t, false, IsMinikubeKubernetes(nil))
})

t.Run("nil ClientConfig returns false", func(t *testing.T) {
c := &minimalClient{context: "some-cluster", clientConfig: nil}
assert.Equal(t, false, IsMinikubeKubernetes(c))
})

t.Run("context named 'minikube' returns true", func(t *testing.T) {
c := makeClient(minikubeContext, nil)
assert.Equal(t, true, IsMinikubeKubernetes(c))
})

t.Run("non-minikube context with no extensions returns false", func(t *testing.T) {
c := makeClient("my-cluster", nil)
assert.Equal(t, false, IsMinikubeKubernetes(c))
})

t.Run("cluster extension with minikube provider returns true", func(t *testing.T) {
ext := &runtime.Unknown{
Raw: []byte(`{"provider":"minikube.sigs.k8s.io"}`),
ContentType: runtime.ContentTypeJSON,
}
c := makeClient("my-cluster", map[string]runtime.Object{minikubeProvider: ext})
assert.Equal(t, true, IsMinikubeKubernetes(c))
})

t.Run("cluster extension with different provider returns false", func(t *testing.T) {
ext := &runtime.Unknown{
Raw: []byte(`{"provider":"some-other-provider"}`),
ContentType: runtime.ContentTypeJSON,
}
c := makeClient("my-cluster", map[string]runtime.Object{"some-other-provider": ext})
assert.Equal(t, false, IsMinikubeKubernetes(c))
})

// Some tools (e.g. Teleport) serialise kubeconfig extensions as plain YAML
// strings rather than structured objects. isMinikubeExtension detects this
// by type-asserting the extension to *runtime.Unknown and using json.Unmarshal,
// which returns an error for non-object JSON (e.g. a bare string) rather than panicking.
t.Run("string-valued extension does not panic and returns false", func(t *testing.T) {
ext := &runtime.Unknown{
Raw: []byte(`"my-cluster-name"`), // bare JSON string, not an object
ContentType: runtime.ContentTypeJSON,
}
c := makeClient("my-cluster", map[string]runtime.Object{"example.dev/cluster-name": ext})
assert.Equal(t, false, IsMinikubeKubernetes(c))
})
}