Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docker CLI Plugins: Use cliPluginsExtraDirs on Windows #7435

Merged
merged 6 commits into from
Sep 11, 2024
Merged
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
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,
});
jandubois marked this conversation as resolved.
Show resolved Hide resolved
case 'win32':
return WindowsIntegrationManager.getInstance();
default:
Expand Down
Loading
Loading