Skip to content

Commit

Permalink
Merge pull request #5924 from mook-as/wsl/docker-plugin-integration-go
Browse files Browse the repository at this point in the history
WSL: Implement docker plugin integration in go
  • Loading branch information
mook-as authored Nov 16, 2023
2 parents f9e2d48 + 7e85f32 commit 223d80b
Show file tree
Hide file tree
Showing 7 changed files with 300 additions and 34 deletions.
42 changes: 11 additions & 31 deletions pkg/rancher-desktop/integrations/windowsIntegrationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,30 +390,12 @@ export default class WindowsIntegrationManager implements IntegrationManager {
protected async syncDistroDockerPlugin(distro: string, pluginName: string, state: boolean) {
try {
const srcPath = await this.getLinuxToolPath(distro, 'bin', pluginName);
const destDir = '$HOME/.docker/cli-plugins';
const destPath = `${ destDir }/${ pluginName }`;
const executable = await this.getLinuxToolPath(distro, 'wsl-helper');

console.debug(`Syncing ${ distro } ${ pluginName }: ${ srcPath } -> ${ destDir }`);
if (state) {
await this.execCommand({ distro }, '/bin/sh', '-c', `mkdir -p "${ destDir }"`);
await this.execCommand({ distro }, '/bin/sh', '-c', `if [ ! -e "${ destPath }" -a ! -L "${ destPath }" ] ; then ln -s "${ srcPath }" "${ destPath }" ; fi`);
} else {
try {
// This is preferred to doing the readlink and rm in one long /bin/sh
// statement because then we rely on the distro's readlink supporting
// the -n option. Gnu/linux readlink supports -f, On macOS the -f means
// something else (not that we're likely to see macos WSLs).
const targetPath = (await this.captureCommand({ distro }, '/bin/sh', '-c', `readlink -f "${ destPath }"`)).trimEnd();

if (targetPath === srcPath) {
await this.execCommand({ distro }, '/bin/sh', '-c', `rm "${ destPath }"`);
}
} catch (err) {
console.log(`Failed to readlink/rm ${ destPath }`, err);
}
}
console.debug(`Syncing docker plugin ${ pluginName } for distribution ${ distro }: ${ state }`);
await this.execCommand({ distro }, executable, 'wsl', 'integration', 'docker-plugin', `--plugin=${ srcPath }`, `--state=${ state }`);
} catch (error) {
console.error(`Failed to sync ${ distro } docker plugin ${ pluginName }: ${ error }`);
console.error(`Failed to sync ${ distro } docker plugin ${ pluginName }: ${ error }`.trim());
}
}

Expand Down Expand Up @@ -521,20 +503,18 @@ export default class WindowsIntegrationManager implements IntegrationManager {
const executable = await this.getLinuxToolPath(distro, 'wsl-helper');
const mode = state ? 'set' : 'delete';

await this.execCommand({ distro, root: true }, executable, 'wsl', 'integration-state', `--mode=${ mode }`);
await this.execCommand({ distro, root: true }, executable, 'wsl', 'integration', 'state', `--mode=${ mode }`);
} catch (ex) {
console.error(`Failed to mark integration for ${ distro }:`, ex);
}
}

async listIntegrations(): Promise<Record<string, boolean | string>> {
const result: Record<string, boolean | string> = {};

for (const distro of await this.nonBlacklistedDistros) {
result[distro.name] = await this.getStateForIntegration(distro);
}
// Get the results in parallel
const distros = await this.nonBlacklistedDistros;
const states = distros.map(d => (async() => [d.name, await this.getStateForIntegration(d)] as const)());

return result;
return Object.fromEntries(await Promise.all(states));
}

/**
Expand All @@ -543,15 +523,15 @@ export default class WindowsIntegrationManager implements IntegrationManager {
*/
protected async getStateForIntegration(distro: WSLDistro): Promise<boolean|string> {
if (distro.version !== 2) {
console.log(`WSL distro "${ distro.name }: is version ${ distro.version }`);
console.log(`WSL distro "${ distro.name }": is version ${ distro.version }`);

return `Rancher Desktop can only integrate with v2 WSL distributions (this is v${ distro.version }).`;
}
try {
const executable = await this.getLinuxToolPath(distro.name, 'wsl-helper');
const stdout = await this.captureCommand(
{ distro: distro.name },
executable, 'wsl', 'integration-state', '--mode=show');
executable, 'wsl', 'integration', 'state', '--mode=show');

console.debug(`WSL distro "${ distro.name }": wsl-helper output: "${ stdout.trim() }"`);
if (['true', 'false'].includes(stdout.trim())) {
Expand Down
52 changes: 52 additions & 0 deletions src/go/wsl-helper/cmd/wsl_integration_docker_plugin_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
Copyright © 2023 SUSE LLC
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 cmd

import (
"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/integration"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var wslIntegrationDockerPluginViper = viper.New()

// wslIntegrationDockerPluginCmd represents the `wsl integration docker-plugin` command
var wslIntegrationDockerPluginCmd = &cobra.Command{
Use: "docker-plugin",
Short: "Commands for managing docker plugin WSL integration",
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true

state := wslIntegrationDockerPluginViper.GetBool("state")
pluginPath := wslIntegrationDockerPluginViper.GetString("plugin")

if err := integration.DockerPlugin(pluginPath, state); err != nil {
return err
}

return nil
},
}

func init() {
wslIntegrationDockerPluginCmd.Flags().String("plugin", "", "Full path to plugin")
wslIntegrationDockerPluginCmd.Flags().Bool("state", false, "Desired state")
wslIntegrationDockerPluginCmd.MarkFlagRequired("plugin")
wslIntegrationDockerPluginViper.AutomaticEnv()
wslIntegrationDockerPluginViper.BindPFlags(wslIntegrationDockerPluginCmd.Flags())
wslIntegrationCmd.AddCommand(wslIntegrationDockerPluginCmd)
}
31 changes: 31 additions & 0 deletions src/go/wsl-helper/cmd/wsl_integration_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
Copyright © 2023 SUSE LLC
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 cmd

import (
"github.com/spf13/cobra"
)

// wslIntegrationCmd represents the `wsl integration` command
var wslIntegrationCmd = &cobra.Command{
Use: "integration",
Short: "Commands for managing with WSL integration",
}

func init() {
wslCmd.AddCommand(wslIntegrationCmd)
}
6 changes: 3 additions & 3 deletions src/go/wsl-helper/cmd/wsl_integration_state_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ import (

var wslIntegrationStateViper = viper.New()

// wslIntegrationStateCmd represents the `wsl integration-state` command.
// wslIntegrationStateCmd represents the `wsl integration state` command.
var wslIntegrationStateCmd = &cobra.Command{
Use: "integration-state",
Use: "state",
Short: "Manage markers for WSL integration state",
Long: "Manage markers for Rancher Desktop WSL distro integration state",
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -57,5 +57,5 @@ func init() {
wslIntegrationStateCmd.MarkFlagRequired("mode")
wslIntegrationStateViper.AutomaticEnv()
wslIntegrationStateViper.BindPFlags(wslIntegrationStateCmd.Flags())
wslCmd.AddCommand(wslIntegrationStateCmd)
wslIntegrationCmd.AddCommand(wslIntegrationStateCmd)
}
4 changes: 4 additions & 0 deletions src/go/wsl-helper/pkg/host/hostfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ func RemoveHostsFileEntry(hostsFilePath string) error {
return err
}

if err := hostsFile.Close(); err != nil {
return err
}

if err := os.Chmod(tempFile.Name(), 0644); err != nil {
return err
}
Expand Down
65 changes: 65 additions & 0 deletions src/go/wsl-helper/pkg/integration/docker_plugin_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
Copyright © 2023 SUSE LLC
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 integration

import (
"errors"
"fmt"
"os"
"path/filepath"
)

// DockerPlugin manages a specific docker plugin (given in pluginPath), either
// enabling it or disabling it in the WSL distribution the process is running in.
func DockerPlugin(pluginPath string, enabled bool) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("could not get home directory: %w", err)
}
pluginDir := filepath.Join(homeDir, ".docker", "cli-plugins")
if err = os.MkdirAll(pluginDir, 0o755); err != nil {
return fmt.Errorf("failed to create docker plugins directory: %w", err)
}
destPath := filepath.Join(pluginDir, filepath.Base(pluginPath))

if enabled {
if _, err := os.Readlink(destPath); err == nil {
if _, err := os.Stat(destPath); errors.Is(err, os.ErrNotExist) {
// The destination is a dangling symlink
if err = os.Remove(destPath); err != nil {
return fmt.Errorf("could not remove dangling symlink %q: %w", destPath, err)
}
}
}

if err = os.Symlink(pluginPath, destPath); err != nil {
// ErrExist is fine, that means there's a user-created file there.
if !errors.Is(err, os.ErrExist) {
return fmt.Errorf("failed to create symlink %q: %w", destPath, err)
}
}
} else {
link, err := os.Readlink(destPath)
if err == nil && link == pluginPath {
if err = os.Remove(destPath); err != nil {
return fmt.Errorf("failed to remove link %q: %w", destPath, err)
}
}
}

return nil
}
134 changes: 134 additions & 0 deletions src/go/wsl-helper/pkg/integration/docker_plugin_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
Copyright © 2023 SUSE LLC
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 integration_test

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/integration"
)

func TestDockerPlugin(t *testing.T) {
t.Run("create symlink", func(t *testing.T) {
homeDir := t.TempDir()
pluginDir := t.TempDir()
pluginPath := filepath.Join(pluginDir, "docker-something")
destPath := filepath.Join(homeDir, ".docker", "cli-plugins", "docker-something")
t.Setenv("HOME", homeDir)

require.NoError(t, integration.DockerPlugin(pluginPath, true))
link, err := os.Readlink(destPath)
if assert.NoError(t, err, "error reading created symlink") {
assert.Equal(t, pluginPath, link)
}
})
t.Run("remove dangling symlink", func(t *testing.T) {
homeDir := t.TempDir()
pluginDir := t.TempDir()
pluginPath := filepath.Join(pluginDir, "docker-something")
destPath := filepath.Join(homeDir, ".docker", "cli-plugins", "docker-something")
t.Setenv("HOME", homeDir)

require.NoError(t, os.MkdirAll(filepath.Dir(destPath), 0o755))
require.NoError(t, os.Symlink(filepath.Join(pluginDir, "missing"), destPath))
require.NoError(t, integration.DockerPlugin(pluginPath, true))
link, err := os.Readlink(destPath)
if assert.NoError(t, err, "error reading created symlink") {
assert.Equal(t, pluginPath, link)
}
})
t.Run("leave existing symlink", func(t *testing.T) {
executable, err := os.Executable()
require.NoError(t, err, "failed to locate executable")
homeDir := t.TempDir()
pluginDir := t.TempDir()
pluginPath := filepath.Join(pluginDir, "docker-something")
destPath := filepath.Join(homeDir, ".docker", "cli-plugins", "docker-something")
t.Setenv("HOME", homeDir)

require.NoError(t, os.MkdirAll(filepath.Dir(destPath), 0o755))
require.NoError(t, os.Symlink(executable, destPath))
require.NoError(t, integration.DockerPlugin(pluginPath, true))
link, err := os.Readlink(destPath)
if assert.NoError(t, err, "error reading created symlink") {
assert.Equal(t, executable, link)
}
})
t.Run("leave existing file", func(t *testing.T) {
homeDir := t.TempDir()
pluginDir := t.TempDir()
pluginPath := filepath.Join(pluginDir, "docker-something")
destPath := filepath.Join(homeDir, ".docker", "cli-plugins", "docker-something")
t.Setenv("HOME", homeDir)

require.NoError(t, os.MkdirAll(filepath.Dir(destPath), 0o755))
require.NoError(t, os.WriteFile(destPath, []byte("hello"), 0o644))
require.NoError(t, integration.DockerPlugin(pluginPath, true))
buf, err := os.ReadFile(destPath)
if assert.NoError(t, err, "failed to read destination file") {
assert.Equal(t, []byte("hello"), buf)
}
})
t.Run("remove correct symlink", func(t *testing.T) {
homeDir := t.TempDir()
pluginDir := t.TempDir()
pluginPath := filepath.Join(pluginDir, "docker-something")
destPath := filepath.Join(homeDir, ".docker", "cli-plugins", "docker-something")
t.Setenv("HOME", homeDir)

require.NoError(t, os.MkdirAll(filepath.Dir(destPath), 0o755))
require.NoError(t, os.Symlink(pluginPath, destPath))
require.NoError(t, integration.DockerPlugin(pluginPath, false))
_, err := os.Lstat(destPath)
assert.ErrorIs(t, err, os.ErrNotExist, "symlink was not removed")
})
t.Run("do not remove incorrect symlink", func(t *testing.T) {
homeDir := t.TempDir()
pluginDir := t.TempDir()
pluginPath := filepath.Join(pluginDir, "docker-something")
destPath := filepath.Join(homeDir, ".docker", "cli-plugins", "docker-something")
t.Setenv("HOME", homeDir)

require.NoError(t, os.MkdirAll(filepath.Dir(destPath), 0o755))
require.NoError(t, os.Symlink(destPath, destPath))
require.NoError(t, integration.DockerPlugin(pluginPath, false))
result, err := os.Readlink(destPath)
if assert.NoError(t, err, "error reading symlink") {
assert.Equal(t, destPath, result, "unexpected symlink contents")
}
})
t.Run("do not remove file", func(t *testing.T) {
homeDir := t.TempDir()
pluginDir := t.TempDir()
pluginPath := filepath.Join(pluginDir, "docker-something")
destPath := filepath.Join(homeDir, ".docker", "cli-plugins", "docker-something")
t.Setenv("HOME", homeDir)

require.NoError(t, os.MkdirAll(filepath.Dir(destPath), 0o755))
require.NoError(t, os.WriteFile(destPath, []byte("hello"), 0o644))
require.NoError(t, integration.DockerPlugin(pluginPath, false))
buf, err := os.ReadFile(destPath)
if assert.NoError(t, err, "failed to read destination file") {
assert.Equal(t, []byte("hello"), buf)
}
})
}

0 comments on commit 223d80b

Please sign in to comment.