Skip to content

Commit

Permalink
Merge pull request #7435 from mook-as/docker-cli/extra-plugin-dir
Browse files Browse the repository at this point in the history
Docker CLI Plugins: Use `cliPluginsExtraDirs` on Windows
  • Loading branch information
jandubois committed Sep 11, 2024
2 parents bbdd8ae + 6301957 commit 67ece93
Show file tree
Hide file tree
Showing 12 changed files with 497 additions and 273 deletions.
20 changes: 16 additions & 4 deletions build/wix/main.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,18 @@
Type="string"
KeyPath="yes"
/>
<Environment Id="PathWindowsUser" Name="PATH"
<Environment Id="PathWindowsUserBin" Name="PATH"
Action="set" Part="last" System="no" Permanent="no"
Value="[APPLICATIONFOLDER]resources\resources\win32\bin\" />
<Environment Id="PathLinuxUser" Name="PATH"
<Environment Id="PathWindowsUserDockerCLIPlugins" Name="PATH"
Action="set" Part="last" System="no" Permanent="no"
Value="[APPLICATIONFOLDER]resources\resources\win32\docker-cli-plugins\" />
<Environment Id="PathLinuxUserBin" Name="PATH"
Action="set" Part="last" System="no" Permanent="no"
Value="[APPLICATIONFOLDER]resources\resources\linux\bin\" />
<Environment Id="PathLinuxUserDockerCLIPlugins" Name="PATH"
Action="set" Part="last" System="no" Permanent="no"
Value="[APPLICATIONFOLDER]resources\resources\linux\docker-cli-plugins\" />
</Component>
<Component Id="PathSystem" Directory="APPLICATIONFOLDER">
<Condition>
Expand All @@ -151,12 +157,18 @@
Type="string"
KeyPath="yes"
/>
<Environment Id="PathWindowsSystem" Name="PATH"
<Environment Id="PathWindowsSystemBin" Name="PATH"
Action="set" Part="last" System="yes" Permanent="no"
Value="[APPLICATIONFOLDER]resources\resources\win32\bin\" />
<Environment Id="PathLinuxSystem" Name="PATH"
<Environment Id="PathWindowsSystemDockerCLIPlugins" Name="PATH"
Action="set" Part="last" System="yes" Permanent="no"
Value="[APPLICATIONFOLDER]resources\resources\win32\docker-cli-plugins\" />
<Environment Id="PathLinuxSystemBin" Name="PATH"
Action="set" Part="last" System="yes" Permanent="no"
Value="[APPLICATIONFOLDER]resources\resources\linux\bin\" />
<Environment Id="PathLinuxSystemDockerCLIPlugins" Name="PATH"
Action="set" Part="last" System="yes" Permanent="no"
Value="[APPLICATIONFOLDER]resources\resources\linux\docker-cli-plugins\" />
</Component>


Expand Down
7 changes: 5 additions & 2 deletions pkg/rancher-desktop/backend/containerClient/mobyClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,8 +470,11 @@ export class MobyClient implements ContainerEngineClient {
runClient(args: string[], stdio: 'pipe', options?: runClientOptions): Promise<{ stdout: string; stderr: string; }>;
runClient(args: string[], stdio: 'stream', options?: runClientOptions): ReadableProcess;
runClient(args: string[], stdio?: 'ignore' | 'pipe' | 'stream' | Log, options?: runClientOptions) {
const binDir = path.join(paths.resources, process.platform, 'bin');
const executable = path.resolve(binDir, options?.executable ?? this.executable);
const executableName = options?.executable ?? this.executable;
const isCLIPlugin = /^docker-(?!credential-)/.test(executableName);
const binType = isCLIPlugin ? 'docker-cli-plugins' : 'bin';
const binDir = path.join(paths.resources, process.platform, binType);
const executable = path.resolve(binDir, executableName);
const opts = _.merge({}, options ?? {}, {
env: {
DOCKER_HOST: this.endpoint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,26 @@ const INTEGRATION_DIR_NAME = 'integrationDir';
const TMPDIR_PREFIX = 'rdtest-';

const describeUnix = os.platform() === 'win32' ? describe.skip : describe;
const resourcesDir = path.join('resources', os.platform(), 'bin');
const binDir = path.join('resources', os.platform(), 'bin');
const dockerCLIPluginSource = path.join('resources', os.platform(), 'docker-cli-plugins');
let testDir: string;

// Creates integration directory and docker CLI plugin directory with
// relevant symlinks in them. Useful for testing removal parts
// of UnixIntegrationManager.
async function createTestSymlinks(resourcesDirectory: string, integrationDirectory: string, dockerCliPluginDirectory: string): Promise<void> {
async function createTestSymlinks(integrationDirectory: string, dockerCLIPluginDest: string): Promise<void> {
await fs.promises.mkdir(integrationDirectory, { recursive: true, mode: 0o755 });
await fs.promises.mkdir(dockerCliPluginDirectory, { recursive: true, mode: 0o755 });
await fs.promises.mkdir(dockerCLIPluginDest, { recursive: true, mode: 0o755 });

const kubectlSrcPath = path.join(resourcesDirectory, 'kubectl');
const kubectlSrcPath = path.join(binDir, 'kubectl');
const kubectlDstPath = path.join(integrationDirectory, 'kubectl');

await fs.promises.symlink(kubectlSrcPath, kubectlDstPath);

const composeSrcPath = path.join(resourcesDirectory, 'docker-compose');
const composeDstPath = path.join(integrationDirectory, 'docker-compose');
const composeSrcPath = path.join(dockerCLIPluginSource, 'docker-compose');
const composeDstPath = path.join(dockerCLIPluginDest, 'docker-compose');

await fs.promises.symlink(composeSrcPath, composeDstPath);

const composeCliDstPath = path.join(dockerCliPluginDirectory, 'docker-compose');

await fs.promises.symlink(composeDstPath, composeCliDstPath);
}

beforeEach(async() => {
Expand All @@ -45,39 +42,42 @@ afterEach(async() => {

describeUnix('UnixIntegrationManager', () => {
let integrationDir: string;
let dockerCliPluginDir: string;
let dockerCLIPluginDest: string;
let integrationManager: UnixIntegrationManager;

beforeEach(() => {
integrationDir = path.join(testDir, INTEGRATION_DIR_NAME);
dockerCliPluginDir = path.join(testDir, 'dockerCliPluginDir');
integrationManager = new UnixIntegrationManager(
resourcesDir, integrationDir, dockerCliPluginDir);
dockerCLIPluginDest = path.join(testDir, 'dockerCliPluginDir');
integrationManager = new UnixIntegrationManager({
binDir, integrationDir, dockerCLIPluginSource, dockerCLIPluginDest,
});
});

describe('enforce', () => {
test('should create dirs and symlinks properly', async() => {
await integrationManager.enforce();
for (const name of await fs.promises.readdir(resourcesDir)) {
for (const name of await fs.promises.readdir(binDir)) {
const integrationPath = path.join(integrationDir, name);
const expectedValue = path.join(resourcesDir, name);
const expectedValue = path.join(binDir, name);

await expect(fs.promises.readlink(integrationPath, 'utf8')).resolves.toEqual(expectedValue);
}
for (const name of await integrationManager.getDockerCliPluginNames()) {
const pluginPath = path.join(dockerCliPluginDir, name);
const expectedValue = path.join(integrationDir, name);
for (const name of await fs.promises.readdir(dockerCLIPluginSource)) {
const binPath = path.join(integrationDir, name);
const pluginPath = path.join(dockerCLIPluginDest, name);
const expectedValue = path.join(dockerCLIPluginSource, name);

await expect(fs.promises.readlink(pluginPath, 'utf8')).resolves.toEqual(expectedValue);
await expect(fs.promises.readlink(binPath, 'utf8')).resolves.toEqual(expectedValue);
}
});

test('should not overwrite an existing docker CLI plugin that is a regular file', async() => {
// create existing plugin
const existingPluginPath = path.join(dockerCliPluginDir, 'docker-compose');
const existingPluginPath = path.join(dockerCLIPluginDest, 'docker-compose');
const existingPluginContents = 'meaningless contents';

await fs.promises.mkdir(dockerCliPluginDir, { mode: 0o755 });
await fs.promises.mkdir(dockerCLIPluginDest, { mode: 0o755 });
await fs.promises.writeFile(existingPluginPath, existingPluginContents);

await integrationManager.enforce();
Expand All @@ -88,11 +88,11 @@ describeUnix('UnixIntegrationManager', () => {
});

test('should update an existing docker CLI plugin that is a dangling symlink', async() => {
const existingPluginPath = path.join(dockerCliPluginDir, 'docker-compose');
const existingPluginPath = path.join(dockerCLIPluginDest, 'docker-compose');
const nonExistentPath = '/somepaththatshouldnevereverexist';
const expectedTarget = path.join(integrationDir, 'docker-compose');
const expectedTarget = path.join(dockerCLIPluginSource, 'docker-compose');

await fs.promises.mkdir(dockerCliPluginDir, { mode: 0o755 });
await fs.promises.mkdir(dockerCLIPluginDest, { mode: 0o755 });
await fs.promises.symlink(nonExistentPath, existingPluginPath);

await integrationManager.enforce();
Expand All @@ -102,13 +102,13 @@ describeUnix('UnixIntegrationManager', () => {
expect(newTarget).toEqual(expectedTarget);
});

test('should update an existing docker CLI plugin whose target is resources directory', async() => {
const existingPluginPath = path.join(dockerCliPluginDir, 'docker-compose');
const resourcesPath = path.join(resourcesDir, 'docker-compose');
const expectedTarget = path.join(integrationDir, 'docker-compose');
test('should update an existing docker CLI plugin whose target is integrations directory', async() => {
const existingPluginPath = path.join(dockerCLIPluginDest, 'docker-compose');
const integrationsPath = path.join(integrationDir, 'docker-compose');
const expectedTarget = path.join(dockerCLIPluginSource, 'docker-compose');

await fs.promises.mkdir(dockerCliPluginDir, { mode: 0o755 });
await fs.promises.symlink(resourcesPath, existingPluginPath);
await fs.promises.mkdir(dockerCLIPluginDest, { mode: 0o755 });
await fs.promises.symlink(integrationsPath, existingPluginPath);

await integrationManager.enforce();

Expand All @@ -120,19 +120,19 @@ describeUnix('UnixIntegrationManager', () => {
test('should be idempotent', async() => {
await integrationManager.enforce();
const intDirAfterFirstCall = await fs.promises.readdir(integrationDir);
const dockerCliDirAfterFirstCall = await fs.promises.readdir(dockerCliPluginDir);
const dockerCliDirAfterFirstCall = await fs.promises.readdir(dockerCLIPluginDest);

await integrationManager.enforce();
const intDirAfterSecondCall = await fs.promises.readdir(integrationDir);
const dockerCliDirAfterSecondCall = await fs.promises.readdir(dockerCliPluginDir);
const dockerCliDirAfterSecondCall = await fs.promises.readdir(dockerCLIPluginDest);

expect(intDirAfterFirstCall).toEqual(intDirAfterSecondCall);
expect(dockerCliDirAfterFirstCall).toEqual(dockerCliDirAfterSecondCall);
});

test('should convert a regular file in integration directory to correct symlink', async() => {
const integrationPath = path.join(integrationDir, 'kubectl');
const expectedTarget = path.join(resourcesDir, 'kubectl');
const expectedTarget = path.join(binDir, 'kubectl');

await fs.promises.mkdir(integrationDir);
await fs.promises.writeFile(integrationPath, 'contents', 'utf-8');
Expand All @@ -143,7 +143,7 @@ describeUnix('UnixIntegrationManager', () => {
test('should fix an incorrect symlink in integration directory', async() => {
const integrationPath = path.join(integrationDir, 'kubectl');
const originalTargetPath = path.join(testDir, 'kubectl');
const expectedTarget = path.join(resourcesDir, 'kubectl');
const expectedTarget = path.join(binDir, 'kubectl');

await fs.promises.mkdir(integrationDir);
await fs.promises.writeFile(originalTargetPath, 'contents', 'utf-8');
Expand All @@ -155,7 +155,7 @@ describeUnix('UnixIntegrationManager', () => {
test('should fix a dangling symlink in integration directory', async() => {
const integrationPath = path.join(integrationDir, 'kubectl');
const originalTargetPath = path.join(testDir, 'kubectl');
const expectedTarget = path.join(resourcesDir, 'kubectl');
const expectedTarget = path.join(binDir, 'kubectl');

await fs.promises.mkdir(integrationDir);
await fs.promises.symlink(originalTargetPath, integrationPath);
Expand All @@ -173,10 +173,10 @@ describeUnix('UnixIntegrationManager', () => {
});

test('should not modify a docker plugin that does not have a counterpart in resources directory', async() => {
const dockerCliPluginPath = path.join(dockerCliPluginDir, 'nameThatShouldNeverBeInResourcesDir');
const dockerCliPluginPath = path.join(dockerCLIPluginDest, 'nameThatShouldNeverBeInResourcesDir');
const content = 'content';

await fs.promises.mkdir(dockerCliPluginDir);
await fs.promises.mkdir(dockerCLIPluginDest);
await fs.promises.writeFile(dockerCliPluginPath, content, 'utf-8');
await integrationManager.enforce();
await expect(fs.promises.readFile(dockerCliPluginPath, 'utf-8')).resolves.toEqual(content);
Expand All @@ -185,19 +185,19 @@ describeUnix('UnixIntegrationManager', () => {

describe('remove', () => {
test('should remove symlinks and dirs properly', async() => {
await createTestSymlinks(resourcesDir, integrationDir, dockerCliPluginDir);
await createTestSymlinks(integrationDir, dockerCLIPluginDest);

await integrationManager.remove();
await expect(fs.promises.readdir(integrationDir)).rejects.toThrow();
await expect(fs.promises.readdir(dockerCliPluginDir)).resolves.toEqual([]);
await expect(fs.promises.readdir(dockerCLIPluginDest)).resolves.toEqual([]);
});

test('should not remove an existing docker CLI plugin that is a regular file', async() => {
// create existing plugin
const existingPluginPath = path.join(dockerCliPluginDir, 'docker-compose');
const existingPluginPath = path.join(dockerCLIPluginDest, 'docker-compose');
const existingPluginContents = 'meaningless contents';

await fs.promises.mkdir(dockerCliPluginDir, { mode: 0o755 });
await fs.promises.mkdir(dockerCLIPluginDest, { mode: 0o755 });
await fs.promises.writeFile(existingPluginPath, existingPluginContents);

await integrationManager.remove();
Expand All @@ -208,11 +208,11 @@ describeUnix('UnixIntegrationManager', () => {
});

test('should not remove an existing docker CLI plugin that is not an expected symlink', async() => {
const dockerCliPluginPath = path.join(dockerCliPluginDir, 'docker-compose');
const dockerCliPluginPath = path.join(dockerCLIPluginDest, 'docker-compose');
const existingTarget = path.join(testDir, 'docker-compose');
const existingPluginContents = 'meaningless contents';

await fs.promises.mkdir(dockerCliPluginDir, { mode: 0o755 });
await fs.promises.mkdir(dockerCLIPluginDest, { mode: 0o755 });
await fs.promises.writeFile(existingTarget, existingPluginContents);
await fs.promises.symlink(existingTarget, dockerCliPluginPath);

Expand All @@ -222,10 +222,10 @@ describeUnix('UnixIntegrationManager', () => {
});

test('should remove an existing docker CLI plugin that is a dangling symlink', async() => {
const dockerCliPluginPath = path.join(dockerCliPluginDir, 'docker-compose');
const dockerCliPluginPath = path.join(dockerCLIPluginDest, 'docker-compose');
const existingTarget = path.join(testDir, 'docker-compose');

await fs.promises.mkdir(dockerCliPluginDir, { mode: 0o755 });
await fs.promises.mkdir(dockerCLIPluginDest, { mode: 0o755 });
await fs.promises.symlink(existingTarget, dockerCliPluginPath);

await integrationManager.remove();
Expand All @@ -238,27 +238,27 @@ describeUnix('UnixIntegrationManager', () => {
const testDirAfterFirstCall = await fs.promises.readdir(testDir);

expect(testDirAfterFirstCall).not.toContain(INTEGRATION_DIR_NAME);
const dockerCliDirAfterFirstCall = await fs.promises.readdir(dockerCliPluginDir);
const dockerCliDirAfterFirstCall = await fs.promises.readdir(dockerCLIPluginDest);

expect(dockerCliDirAfterFirstCall).toEqual([]);

await integrationManager.remove();
const testDirAfterSecondCall = await fs.promises.readdir(testDir);

expect(testDirAfterSecondCall).not.toContain(INTEGRATION_DIR_NAME);
const dockerCliDirAfterSecondCall = await fs.promises.readdir(dockerCliPluginDir);
const dockerCliDirAfterSecondCall = await fs.promises.readdir(dockerCLIPluginDest);

expect(dockerCliDirAfterFirstCall).toEqual(dockerCliDirAfterSecondCall);
});
});

describe('removeSymlinksOnly', () => {
test('should remove symlinks but not integration directory', async() => {
await createTestSymlinks(resourcesDir, integrationDir, dockerCliPluginDir);
await createTestSymlinks(integrationDir, dockerCLIPluginDest);

await integrationManager.removeSymlinksOnly();
await expect(fs.promises.readdir(integrationDir)).resolves.toEqual([]);
await expect(fs.promises.readdir(dockerCliPluginDir)).resolves.toEqual([]);
await expect(fs.promises.readdir(dockerCLIPluginDest)).resolves.toEqual([]);
});
});

Expand All @@ -267,12 +267,12 @@ describeUnix('UnixIntegrationManager', () => {
const credHelper = 'docker-credential-pass';

beforeEach(async() => {
await fs.promises.mkdir(dockerCliPluginDir, { recursive: true, mode: 0o755 });
dstPath = path.join(dockerCliPluginDir, credHelper);
await fs.promises.mkdir(dockerCLIPluginDest, { recursive: true, mode: 0o755 });
dstPath = path.join(dockerCLIPluginDest, credHelper);
});

test("should return true when the symlink's target matches the integration directory", async() => {
const resourcesPath = path.join(resourcesDir, credHelper);
const resourcesPath = path.join(dockerCLIPluginSource, credHelper);
const srcPath = path.join(integrationDir, credHelper);

// create symlink in integration dir, otherwise it is dangling
Expand All @@ -284,7 +284,7 @@ describeUnix('UnixIntegrationManager', () => {
});

test("should return true when the symlink's target matches the resources directory", async() => {
const srcPath = path.join(resourcesDir, credHelper);
const srcPath = path.join(dockerCLIPluginSource, credHelper);

await fs.promises.symlink(srcPath, dstPath);
expect(integrationManager['weOwnDockerCliFile'](dstPath)).resolves.toEqual(true);
Expand Down Expand Up @@ -315,7 +315,7 @@ describeUnix('UnixIntegrationManager', () => {
});

describeUnix('ensureSymlink', () => {
const srcPath = path.join(resourcesDir, 'kubectl');
const srcPath = path.join(dockerCLIPluginSource, 'kubectl');
let dstPath: string;

beforeEach(() => {
Expand Down
10 changes: 6 additions & 4 deletions pkg/rancher-desktop/integrations/integrationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,16 @@ export interface IntegrationManager {

export function getIntegrationManager(): IntegrationManager {
const platform = os.platform();
const resourcesBinDir = path.join(paths.resources, platform, 'bin');
const dockerCliPluginDir = path.join(os.homedir(), '.docker', 'cli-plugins');
const binDir = path.join(paths.resources, platform, 'bin');
const dockerCLIPluginSource = path.join(paths.resources, platform, 'docker-cli-plugins');
const dockerCLIPluginDest = path.join(os.homedir(), '.docker', 'cli-plugins');

switch (platform) {
case 'linux':
return new UnixIntegrationManager(resourcesBinDir, paths.integration, dockerCliPluginDir);
case 'darwin':
return new UnixIntegrationManager(resourcesBinDir, paths.integration, dockerCliPluginDir);
return new UnixIntegrationManager({
binDir, integrationDir: paths.integration, dockerCLIPluginSource, dockerCLIPluginDest,
});
case 'win32':
return WindowsIntegrationManager.getInstance();
default:
Expand Down
Loading

0 comments on commit 67ece93

Please sign in to comment.