Skip to content
This repository has been archived by the owner on Jun 13, 2021. It is now read-only.

Commit

Permalink
Add support for Docker-Desktop contexts rewriting
Browse files Browse the repository at this point in the history
Signed-off-by: Simon Ferquel <simon.ferquel@docker.com>
  • Loading branch information
simonferquel committed Mar 14, 2019
1 parent e62ae09 commit 44d0128
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 4 deletions.
2 changes: 2 additions & 0 deletions internal/commands/cnab.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
)

func prepareCredentialSet(contextName string, contextStore store.Store, b *bundle.Bundle, namedCredentialsets []string) (map[string]string, error) {
// docker desktop contexts require some rewriting for being used within a container
contextStore = dockerDesktopAwareStore{Store: contextStore}
creds := map[string]string{}
for _, file := range namedCredentialsets {
if _, err := os.Stat(file); err != nil {
Expand Down
153 changes: 153 additions & 0 deletions internal/commands/dockerdesktop.go
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
}
145 changes: 145 additions & 0 deletions internal/commands/dockerdesktop_test.go
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)
})
}
}
9 changes: 6 additions & 3 deletions internal/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"github.com/pkg/errors"
)

func storeBaseDir() string {
func storeBaseDir() (string, error) {
return config.Path("app-bundle-store")
}
func storePath(ref reference.Named) (string, error) {
Expand All @@ -29,8 +29,11 @@ func storePath(ref reference.Named) (string, error) {
// when parsing the ref) then there will be errors when we try
// to use this as a path later.
name = strings.Replace(name, ":", "_", 1)

storeDir := filepath.Join(storeBaseDir(), filepath.FromSlash(name))
baseDir, err := storeBaseDir()
if err != nil {
return "", err
}
storeDir := filepath.Join(baseDir, filepath.FromSlash(name))

// We rely here on _ not being valid in a name meaning there can be no clashes due to nesting of repositories.
switch t := ref.(type) {
Expand Down
2 changes: 1 addition & 1 deletion internal/store/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func parseRefOrDie(t *testing.T, ref string) reference.Named {

func TestStorePath(t *testing.T) {
testSha := "2957c6606cc94099f7dfe0011b5c8daf4a605ed6124d4eee773bab1e05a8ce87"
basedir := storeBaseDir()
basedir, _ := storeBaseDir()
for _, tc := range []struct {
Name string
Ref reference.Named
Expand Down

0 comments on commit 44d0128

Please sign in to comment.