diff --git a/src/cp.ts b/src/cp.ts index d84d34cf86..52e3bdd72c 100644 --- a/src/cp.ts +++ b/src/cp.ts @@ -18,6 +18,7 @@ export class Cp { * @param {string} containerName - The name of the container in the pod to exec the command inside. * @param {string} srcPath - The source path in the pod * @param {string} tgtPath - The target path in local + * @param {string} [cwd] - The directory that is used as the parent in the pod when downloading */ public async cpFromPod( namespace: string, @@ -25,10 +26,15 @@ export class Cp { containerName: string, srcPath: string, tgtPath: string, + cwd?: string, ): Promise { const tmpFile = tmp.fileSync(); const tmpFileName = tmpFile.name; - const command = ['tar', 'zcf', '-', srcPath]; + const command = ['tar', 'cf', '-']; + if (cwd) { + command.push('-C', cwd); + } + command.push(srcPath); const writerStream = fs.createWriteStream(tmpFileName); const errStream = new WritableStreamBuffer(); this.execInstance.exec( diff --git a/src/cp_test.ts b/src/cp_test.ts index 7cfcafa7d4..fb38247df3 100644 --- a/src/cp_test.ts +++ b/src/cp_test.ts @@ -22,7 +22,7 @@ describe('Cp', () => { const container = 'container'; const srcPath = '/'; const tgtPath = '/'; - const cmdArray = ['tar', 'zcf', '-', srcPath]; + const cmdArray = ['tar', 'cf', '-', srcPath]; const path = `/api/v1/namespaces/${namespace}/pods/${pod}/exec`; const query = { @@ -38,6 +38,35 @@ describe('Cp', () => { await cp.cpFromPod(namespace, pod, container, srcPath, tgtPath); verify(fakeWebSocket.connect(`${path}?${queryStr}`, null, anyFunction())).called(); }); + + it('should run create tar command to a url with cwd', async () => { + const kc = new KubeConfig(); + const fakeWebSocket: WebSocketInterface = mock(WebSocketHandler); + const exec = new Exec(kc, instance(fakeWebSocket)); + const cp = new Cp(kc, exec); + + const namespace = 'somenamespace'; + const pod = 'somepod'; + const container = 'container'; + const srcPath = '/'; + const tgtPath = '/'; + const cwd = '/abc'; + const cmdArray = ['tar', 'cf', '-', '-C', cwd, srcPath]; + const path = `/api/v1/namespaces/${namespace}/pods/${pod}/exec`; + + const query = { + stdout: true, + stderr: true, + stdin: false, + tty: false, + command: cmdArray, + container, + }; + const queryStr = querystring.stringify(query); + + await cp.cpFromPod(namespace, pod, container, srcPath, tgtPath, cwd); + verify(fakeWebSocket.connect(`${path}?${queryStr}`, null, anyFunction())).called(); + }); }); describe('cpToPod', () => { diff --git a/src/test/integration/cpFromPod.ts b/src/test/integration/cpFromPod.ts new file mode 100644 index 0000000000..001368dfdf --- /dev/null +++ b/src/test/integration/cpFromPod.ts @@ -0,0 +1,79 @@ +import assert from 'node:assert'; +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; +import { setTimeout } from 'node:timers/promises'; +import { CoreV1Api, KubeConfig, V1Pod } from '../../index.js'; +import { Cp } from '../../cp.js'; +import { generateName } from './name.js'; + +export default async function cpFromPod() { + const kc = new KubeConfig(); + kc.loadFromDefault(); + + const coreV1Client = kc.makeApiClient(CoreV1Api); + const cp = new Cp(kc); + + const testPodName = generateName('cp-test-pod'); + const namespace = 'default'; + + const pod = new V1Pod(); + pod.metadata = { name: testPodName }; + pod.spec = { + containers: [ + { + name: 'test-container', + image: 'busybox', + command: ['sh', '-c', 'echo "test content" > /tmp/test.txt && sleep 3600'], + }, + ], + restartPolicy: 'Never', + }; + + console.log(`Creating pod ${testPodName}`); + await coreV1Client.createNamespacedPod({ namespace, body: pod }); + + console.log('Waiting for pod to be ready...'); + let podReady = false; + for (let i = 0; i < 30; i++) { + const currentPod = await coreV1Client.readNamespacedPod({ name: testPodName, namespace }); + if (currentPod.status?.phase === 'Running') { + podReady = true; + break; + } + await setTimeout(1000); + } + + assert.strictEqual(podReady, true, 'Pod did not become ready in time'); + console.log('Pod is ready'); + + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'k8s-cp-test-')); + + try { + console.log('Copying file from pod...'); + + cp.cpFromPod(namespace, testPodName, 'test-container', 'test.txt', tempDir, '/tmp'); + + // Wait for file to appear + const copiedFilePath = path.join(tempDir, 'test.txt'); + let fileExists = false; + for (let i = 0; i < 20; i++) { + if (fs.existsSync(copiedFilePath)) { + fileExists = true; + break; + } + await setTimeout(500); + } + + assert.strictEqual(fileExists, true, 'File was not copied'); + + const content = fs.readFileSync(copiedFilePath, 'utf-8'); + assert.strictEqual(content.trim(), 'test content', 'File content does not match'); + + console.log('cpFromPod test passed!'); + } finally { + console.log('Cleaning up...'); + await coreV1Client.deleteNamespacedPod({ name: testPodName, namespace }); + fs.rmSync(tempDir, { recursive: true, force: true }); + } +} diff --git a/src/test/integration/index.ts b/src/test/integration/index.ts index 5065dcd429..ee26fa03df 100644 --- a/src/test/integration/index.ts +++ b/src/test/integration/index.ts @@ -1,5 +1,7 @@ import patchNamespace from './patchNamespace.js'; +import cpFromPod from './cpFromPod.js'; console.log('Integration testing'); await patchNamespace(); +await cpFromPod();