This repository has been archived by the owner on Jun 13, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 177
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for Docker-Desktop contexts rewriting
Signed-off-by: Simon Ferquel <simon.ferquel@docker.com>
- Loading branch information
1 parent
e62ae09
commit 44d0128
Showing
5 changed files
with
307 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
package commands | ||
|
||
import ( | ||
"fmt" | ||
"net/url" | ||
"runtime" | ||
|
||
"github.com/pkg/errors" | ||
|
||
"github.com/docker/cli/cli/context/docker" | ||
"github.com/docker/cli/cli/context/kubernetes" | ||
"github.com/docker/cli/cli/context/store" | ||
"github.com/docker/docker/client" | ||
apiv1 "k8s.io/api/core/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
v1 "k8s.io/client-go/kubernetes/typed/core/v1" | ||
) | ||
|
||
type dockerDesktopHostProvider func() (string, bool) | ||
|
||
func defaultDockerDesktopHostProvider() (string, bool) { | ||
switch runtime.GOOS { | ||
case "windows", "darwin": | ||
default: | ||
// platforms other than windows or mac can't be Docker Desktop | ||
return "", false | ||
} | ||
return client.DefaultDockerHost, true | ||
} | ||
|
||
type dockerDesktopLinuxKitIPProvider func() (string, error) | ||
|
||
type dockerDesktopDockerEndpointRewriter struct { | ||
defaultHostProvider dockerDesktopHostProvider | ||
} | ||
|
||
func (r *dockerDesktopDockerEndpointRewriter) rewrite(ep *docker.EndpointMeta) { | ||
defaultHost, isDockerDesktop := r.defaultHostProvider() | ||
if !isDockerDesktop { | ||
return | ||
} | ||
// on docker desktop, any context with host="" or host=<default host> should be rewritten as host="unix:///var/run/docker.sock" (docker socket path within the linuxkit VM) | ||
if ep.Host == "" || ep.Host == defaultHost { | ||
ep.Host = "unix:///var/run/docker.sock" | ||
} | ||
} | ||
|
||
type dockerDesktopKubernetesEndpointRewriter struct { | ||
defaultHostProvider dockerDesktopHostProvider | ||
linuxKitIPProvider dockerDesktopLinuxKitIPProvider | ||
} | ||
|
||
func (r *dockerDesktopKubernetesEndpointRewriter) rewrite(ep *kubernetes.EndpointMeta) { | ||
// any error while rewriting makes as if no rewriting rule applies | ||
if _, isDockerDesktop := r.defaultHostProvider(); !isDockerDesktop { | ||
return | ||
} | ||
// if the kube endpoint host points to localhost or 127.0.0.1, we need to rewrite it to whatever is linuxkit VM IP is (with port 6443) | ||
hostURL, err := url.Parse(ep.Host) | ||
if err != nil { | ||
return | ||
} | ||
hostName := hostURL.Hostname() | ||
switch hostName { | ||
case "localhost", "127.0.0.1": | ||
default: | ||
// we are on a context targeting a remote Kubernetes cluster, nothing to rewrite | ||
return | ||
} | ||
ip, err := r.linuxKitIPProvider() | ||
if err != nil { | ||
return | ||
} | ||
ep.Host = fmt.Sprintf("https://%s:6443", ip) | ||
} | ||
|
||
func makeLinuxkitIPProvider(contextName string, s store.Store) dockerDesktopLinuxKitIPProvider { | ||
return func() (string, error) { | ||
clientCfg, err := kubernetes.ConfigFromContext(contextName, s) | ||
if err != nil { | ||
return "", err | ||
} | ||
restCfg, err := clientCfg.ClientConfig() | ||
if err != nil { | ||
return "", err | ||
} | ||
coreClient, err := v1.NewForConfig(restCfg) | ||
if err != nil { | ||
return "", err | ||
} | ||
nodes, err := coreClient.Nodes().List(metav1.ListOptions{}) | ||
if err != nil { | ||
return "", err | ||
} | ||
if len(nodes.Items) == 0 { | ||
return "", errors.New("no node found") | ||
} | ||
for _, address := range nodes.Items[0].Status.Addresses { | ||
if address.Type == apiv1.NodeInternalIP { | ||
return address.Address, nil | ||
} | ||
} | ||
return "", errors.New("no ip found") | ||
} | ||
} | ||
|
||
func rewriteContextIfDockerDesktop(meta *store.ContextMetadata, s store.Store) { | ||
// errors are treated as "don't rewrite" | ||
rewriter := dockerDesktopDockerEndpointRewriter{ | ||
defaultHostProvider: defaultDockerDesktopHostProvider, | ||
} | ||
dockerEp, err := docker.EndpointFromContext(*meta) | ||
if err != nil { | ||
return | ||
} | ||
rewriter.rewrite(&dockerEp) | ||
meta.Endpoints[docker.DockerEndpoint] = dockerEp | ||
kubeEp := kubernetes.EndpointFromContext(*meta) | ||
if kubeEp == nil { | ||
return | ||
} | ||
kubeRewriter := dockerDesktopKubernetesEndpointRewriter{ | ||
defaultHostProvider: defaultDockerDesktopHostProvider, | ||
linuxKitIPProvider: makeLinuxkitIPProvider(meta.Name, s), | ||
} | ||
kubeRewriter.rewrite(kubeEp) | ||
meta.Endpoints[kubernetes.KubernetesEndpoint] = *kubeEp | ||
} | ||
|
||
type dockerDesktopAwareStore struct { | ||
store.Store | ||
} | ||
|
||
func (s dockerDesktopAwareStore) ListContexts() ([]store.ContextMetadata, error) { | ||
contexts, err := s.Store.ListContexts() | ||
if err != nil { | ||
return nil, err | ||
} | ||
for ix, c := range contexts { | ||
rewriteContextIfDockerDesktop(&c, s.Store) | ||
contexts[ix] = c | ||
} | ||
return contexts, nil | ||
} | ||
|
||
func (s dockerDesktopAwareStore) GetContextMetadata(name string) (store.ContextMetadata, error) { | ||
context, err := s.Store.GetContextMetadata(name) | ||
if err != nil { | ||
return store.ContextMetadata{}, err | ||
} | ||
rewriteContextIfDockerDesktop(&context, s.Store) | ||
return context, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
package commands | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/docker/cli/cli/context" | ||
"github.com/docker/cli/cli/context/docker" | ||
"github.com/docker/cli/cli/context/kubernetes" | ||
"github.com/pkg/errors" | ||
"gotest.tools/assert" | ||
) | ||
|
||
var ( | ||
noDesktopProvider = func() (string, bool) { | ||
return "", false | ||
} | ||
desktopProvider = func() (string, bool) { | ||
return "unix:///test", true | ||
} | ||
) | ||
|
||
func TestDockerDesktopDockerEndpointRewriter(t *testing.T) { | ||
cases := []struct { | ||
name string | ||
hostProvider dockerDesktopHostProvider | ||
currentHost string | ||
expectedHost string | ||
}{ | ||
{ | ||
name: "no-desktop", | ||
hostProvider: noDesktopProvider, | ||
currentHost: "", | ||
expectedHost: "", | ||
}, | ||
{ | ||
name: "no-desktop-custom-host", | ||
hostProvider: noDesktopProvider, | ||
currentHost: "test", | ||
expectedHost: "test", | ||
}, | ||
{ | ||
name: "desktop-empty-host", | ||
hostProvider: desktopProvider, | ||
currentHost: "", | ||
expectedHost: "unix:///var/run/docker.sock", | ||
}, | ||
{ | ||
name: "desktop-default-host", | ||
hostProvider: desktopProvider, | ||
currentHost: "unix:///test", | ||
expectedHost: "unix:///var/run/docker.sock", | ||
}, | ||
{ | ||
name: "desktop-custom-host", | ||
hostProvider: desktopProvider, | ||
currentHost: "test", | ||
expectedHost: "test", | ||
}, | ||
} | ||
|
||
for _, c := range cases { | ||
t.Run(c.name, func(t *testing.T) { | ||
testee := dockerDesktopDockerEndpointRewriter{ | ||
defaultHostProvider: c.hostProvider, | ||
} | ||
ep := docker.EndpointMeta{ | ||
Host: c.currentHost, | ||
} | ||
testee.rewrite(&ep) | ||
assert.Check(t, ep.Host == c.expectedHost) | ||
}) | ||
} | ||
} | ||
|
||
func TestDockerDesktopKubernetesEndpointRewriter(t *testing.T) { | ||
cases := []struct { | ||
name string | ||
hostProvider dockerDesktopHostProvider | ||
ipProvider dockerDesktopLinuxKitIPProvider | ||
currentHost string | ||
expectedHost string | ||
}{ | ||
{ | ||
name: "no-desktop", | ||
hostProvider: noDesktopProvider, | ||
currentHost: "https://localhost:6443", | ||
expectedHost: "https://localhost:6443", | ||
}, | ||
{ | ||
name: "no-desktop-custom-host", | ||
hostProvider: noDesktopProvider, | ||
currentHost: "https://custom:6443", | ||
expectedHost: "https://custom:6443", | ||
}, | ||
{ | ||
name: "desktop-localhost", | ||
hostProvider: desktopProvider, | ||
currentHost: "https://localhost:4242", | ||
expectedHost: "https://42.42.42.42:6443", | ||
}, | ||
{ | ||
name: "desktop-127.0.0.01", | ||
hostProvider: desktopProvider, | ||
currentHost: "https://127.0.0.1:4242", | ||
expectedHost: "https://42.42.42.42:6443", | ||
}, | ||
{ | ||
name: "desktop-custom-host", | ||
hostProvider: desktopProvider, | ||
currentHost: "https://custom:6443", | ||
expectedHost: "https://custom:6443", | ||
}, | ||
{ | ||
name: "no-rewrite-on-error", | ||
hostProvider: desktopProvider, | ||
ipProvider: func() (string, error) { | ||
return "", errors.New("boom") | ||
}, | ||
currentHost: "https://127.0.0.1:4242", | ||
expectedHost: "https://127.0.0.1:4242", | ||
}, | ||
} | ||
|
||
for _, c := range cases { | ||
t.Run(c.name, func(t *testing.T) { | ||
ipProvider := c.ipProvider | ||
if ipProvider == nil { | ||
ipProvider = func() (string, error) { | ||
return "42.42.42.42", nil | ||
} | ||
} | ||
testee := dockerDesktopKubernetesEndpointRewriter{ | ||
defaultHostProvider: c.hostProvider, | ||
linuxKitIPProvider: ipProvider, | ||
} | ||
ep := kubernetes.EndpointMeta{ | ||
EndpointMetaBase: context.EndpointMetaBase{ | ||
Host: c.currentHost, | ||
}, | ||
} | ||
testee.rewrite(&ep) | ||
assert.Check(t, ep.Host == c.expectedHost) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters