diff --git a/.changeset/silver-groups-beam.md b/.changeset/silver-groups-beam.md new file mode 100644 index 0000000000..e171e7dab5 --- /dev/null +++ b/.changeset/silver-groups-beam.md @@ -0,0 +1,6 @@ +--- +'@e2b/python-sdk': patch +'e2b': patch +--- + +throw when copying paths outside of the context dir diff --git a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-async/template.py b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-async/template.py index 76193e6122..1e4e7ff3e5 100644 --- a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-async/template.py +++ b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-async/template.py @@ -5,8 +5,8 @@ .from_image("alpine:latest") .set_user("root") .set_workdir("/") - .copy("package.json", "/app/") - .copy("src/index.js", "./src/") + .copy("package.json", "/app") + .copy("src/index.js", "src") .copy("config.json", "/etc/app/config.json") .set_user("user") .set_workdir("/home/user") diff --git a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-sync/template.py b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-sync/template.py index eda0d72a7d..dedaf69168 100644 --- a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-sync/template.py +++ b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/python-sync/template.py @@ -5,8 +5,8 @@ .from_image("alpine:latest") .set_user("root") .set_workdir("/") - .copy("package.json", "/app/") - .copy("src/index.js", "./src/") + .copy("package.json", "/app") + .copy("src/index.js", "src") .copy("config.json", "/etc/app/config.json") .set_user("user") .set_workdir("/home/user") diff --git a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/typescript/template.ts b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/typescript/template.ts index 835891e758..61bc2f9657 100644 --- a/packages/cli/tests/commands/template/fixtures/copy-variations/expected/typescript/template.ts +++ b/packages/cli/tests/commands/template/fixtures/copy-variations/expected/typescript/template.ts @@ -4,8 +4,8 @@ export const template = Template() .fromImage('alpine:latest') .setUser('root') .setWorkdir('/') - .copy('package.json', '/app/') - .copy('src/index.js', './src/') + .copy('package.json', '/app') + .copy('src/index.js', 'src') .copy('config.json', '/etc/app/config.json') .setUser('user') - .setWorkdir('/home/user') \ No newline at end of file + .setWorkdir('/home/user') diff --git a/packages/js-sdk/src/template/index.ts b/packages/js-sdk/src/template/index.ts index c40184ab35..d8b4125756 100644 --- a/packages/js-sdk/src/template/index.ts +++ b/packages/js-sdk/src/template/index.ts @@ -44,6 +44,8 @@ import { padOctal, readDockerignore, readGCPServiceAccountJSON, + isSafeRelative, + normalizePath, } from './utils' /** @@ -522,9 +524,23 @@ export class TemplateBase const srcs = Array.isArray(src) ? src : [src] for (const src of srcs) { + const normalizedSrc = normalizePath(src.toString()) + const normalizedDest = normalizePath(dest.toString()) + + if (!isSafeRelative(normalizedSrc)) { + const error = new Error( + `Source path ${src} is outside of the context directory.` + ) + const stackTrace = getCallerFrame(STACK_TRACE_DEPTH - 1) + if (stackTrace) { + error.stack = stackTrace + } + throw error + } + const args = [ - src.toString(), - dest.toString(), + normalizedSrc, + normalizedDest, options?.user ?? '', options?.mode ? padOctal(options.mode) : '', ] diff --git a/packages/js-sdk/src/template/utils.ts b/packages/js-sdk/src/template/utils.ts index 34670767c4..6e5778b6e6 100644 --- a/packages/js-sdk/src/template/utils.ts +++ b/packages/js-sdk/src/template/utils.ts @@ -63,12 +63,50 @@ export function readDockerignore(contextPath: string): string[] { } /** - * Normalize path separators to forward slashes for glob patterns (glob expects / even on Windows) + * Normalize paths on Windows and Unix + * * @param path - The path to normalize * @returns The normalized path */ -function normalizePath(path: string): string { - return path.replace(/\\/g, '/') +export function normalizePath(path: string): string { + if (!path || path === '.') { + return '.' + } + + // Remove drive letter if present (e.g., 'C:') + let workingPath = path + if (/^[a-zA-Z]:/.test(path)) { + workingPath = path.substring(2) + } + + // Check if path is absolute + const isAbsolute = workingPath.startsWith('/') || workingPath.startsWith('\\') + + // Normalize all separators to forward slashes and split + const normalizedPath = workingPath.replace(/[\\/]+/g, '/') + const parts = normalizedPath.split('/').filter((part) => part && part !== '.') + + const normalized: string[] = [] + + for (const part of parts) { + if (part === '..') { + // Go up one directory if possible (but not past root for absolute paths) + if (normalized.length > 0 && normalized[normalized.length - 1] !== '..') { + normalized.pop() + } else if (!isAbsolute) { + // For relative paths, keep the '..' if we can't go up further + normalized.push('..') + } + } else { + normalized.push(part) + } + } + + // Build the final path in POSIX style + const result = (isAbsolute ? '/' : '') + normalized.join('/') + + // Return '.' for empty relative paths + return result || '.' } /** @@ -408,3 +446,20 @@ export function readGCPServiceAccountJSON( } return JSON.stringify(pathOrContent) } + +/** + * Returns true if src is a relative path and does not contain any up-path parts. + * Works on both Windows and Unix. + * + * @param src Source path + * @returns boolean + */ +export function isSafeRelative(src: string): boolean { + const normPath = normalizePath(src) + return !( + path.isAbsolute(normPath) || + normPath === '..' || + normPath.startsWith('../') || + normPath.startsWith('..\\') + ) +} diff --git a/packages/js-sdk/tests/template/methods/fromDockerfile.test.ts b/packages/js-sdk/tests/template/methods/fromDockerfile.test.ts index 70e34f742c..250deb87f3 100644 --- a/packages/js-sdk/tests/template/methods/fromDockerfile.test.ts +++ b/packages/js-sdk/tests/template/methods/fromDockerfile.test.ts @@ -133,8 +133,8 @@ buildTemplateTest('fromDockerfile with custom user and workdir', () => { buildTemplateTest('fromDockerfile with COPY --chown', () => { const dockerfile = `FROM node:24 -COPY --chown=myuser:mygroup app.js /app/ -COPY --chown=anotheruser config.json /config/` +COPY --chown=myuser:mygroup app.js /app +COPY --chown=anotheruser config.json /config` const template = Template().fromDockerfile(dockerfile) @@ -143,7 +143,7 @@ COPY --chown=anotheruser config.json /config/` const copyInstruction1 = template.instructions[2] assert.equal(copyInstruction1.type, InstructionType.COPY) assert.equal(copyInstruction1.args[0], 'app.js') - assert.equal(copyInstruction1.args[1], '/app/') + assert.equal(copyInstruction1.args[1], '/app') assert.equal(copyInstruction1.args[2], 'myuser:mygroup') // user from --chown // Second COPY instruction @@ -151,6 +151,6 @@ COPY --chown=anotheruser config.json /config/` const copyInstruction2 = template.instructions[3] assert.equal(copyInstruction2.type, InstructionType.COPY) assert.equal(copyInstruction2.args[0], 'config.json') - assert.equal(copyInstruction2.args[1], '/config/') + assert.equal(copyInstruction2.args[1], '/config') assert.equal(copyInstruction2.args[2], 'anotheruser') // user from --chown (without group) }) diff --git a/packages/js-sdk/tests/template/stacktrace.test.ts b/packages/js-sdk/tests/template/stacktrace.test.ts index 7a401f980a..7ced3aecf1 100644 --- a/packages/js-sdk/tests/template/stacktrace.test.ts +++ b/packages/js-sdk/tests/template/stacktrace.test.ts @@ -9,7 +9,7 @@ import { apiUrl, buildTemplateTest } from '../setup' import { randomUUID } from 'node:crypto' const __fileContent = fs.readFileSync(__filename, 'utf8') // read current file content -const nonExistentPath = '/nonexistent/path' +const nonExistentPath = './nonexistent/path' // map template alias -> failed step index const failureMap: Record = { diff --git a/packages/js-sdk/tests/template/utils/isSafeRelative.test.ts b/packages/js-sdk/tests/template/utils/isSafeRelative.test.ts new file mode 100644 index 0000000000..f39fd2a486 --- /dev/null +++ b/packages/js-sdk/tests/template/utils/isSafeRelative.test.ts @@ -0,0 +1,52 @@ +import { expect, test, describe } from 'vitest' +import { isSafeRelative } from '../../../src/template/utils' + +describe('isSafeRelative', () => { + describe('absolute paths', () => { + test('should return false for Unix absolute paths', () => { + expect(isSafeRelative('/absolute/path')).toBe(false) + }) + }) + + describe('parent directory traversal', () => { + test('should return false for parent directory only', () => { + expect(isSafeRelative('..')).toBe(false) + }) + + test('should return true for paths starting with ../', () => { + expect(isSafeRelative('../file.txt')).toBe(false) + }) + + test('should return true for paths starting with ..\\', () => { + expect(isSafeRelative('..\\file.txt')).toBe(false) + }) + + test('should return true for normalized paths that escape context', () => { + expect(isSafeRelative('foo/../../bar')).toBe(false) + }) + }) + + describe('valid relative paths', () => { + test('should return false for simple relative paths', () => { + expect(isSafeRelative('file.txt')).toBe(true) + expect(isSafeRelative('folder/file.txt')).toBe(true) + }) + + test('should return true for current directory references', () => { + expect(isSafeRelative('.')).toBe(true) + expect(isSafeRelative('./file.txt')).toBe(true) + expect(isSafeRelative('./folder/file.txt')).toBe(true) + }) + + test('should return false for glob patterns', () => { + expect(isSafeRelative('*.txt')).toBe(true) + expect(isSafeRelative('**/*.ts')).toBe(true) + expect(isSafeRelative('src/**/*')).toBe(true) + }) + + test('should return false for hidden files and directories', () => { + expect(isSafeRelative('.hidden')).toBe(true) + expect(isSafeRelative('.config/settings')).toBe(true) + }) + }) +}) diff --git a/packages/js-sdk/tests/template/utils/normalizePath.test.ts b/packages/js-sdk/tests/template/utils/normalizePath.test.ts new file mode 100644 index 0000000000..a25978c57c --- /dev/null +++ b/packages/js-sdk/tests/template/utils/normalizePath.test.ts @@ -0,0 +1,78 @@ +import { expect, test, describe } from 'vitest' +import { normalizePath } from '../../../src/template/utils' + +describe('normalizePath', () => { + describe('basic path normalization', () => { + test('should resolve parent directory references', () => { + expect(normalizePath('/foo/bar/../baz')).toBe('/foo/baz') + }) + + test('should remove current directory references', () => { + expect(normalizePath('foo/./bar')).toBe('foo/bar') + }) + + test('should collapse multiple slashes', () => { + expect(normalizePath('foo//bar///baz')).toBe('foo/bar/baz') + }) + + test('should handle multiple parent directory traversals in relative paths', () => { + expect(normalizePath('../foo/../../bar')).toBe('../../bar') + }) + + test('should not traverse past root for absolute paths', () => { + expect(normalizePath('/foo/../../bar')).toBe('/bar') + }) + + test('should return dot for empty path', () => { + expect(normalizePath('')).toBe('.') + }) + + test('should remove leading current directory reference', () => { + expect(normalizePath('./foo/bar')).toBe('foo/bar') + }) + }) + + describe('Windows paths converted to POSIX style', () => { + test('should normalize Windows path with drive letter and backslashes', () => { + expect(normalizePath('C:\\foo\\bar\\..\\baz')).toBe('/foo/baz') + }) + + test('should normalize Windows path with drive letter and forward slashes', () => { + expect(normalizePath('C:/foo/bar/../baz')).toBe('/foo/baz') + }) + + test('should normalize backslash with current directory reference', () => { + expect(normalizePath('foo\\.\\bar')).toBe('foo/bar') + }) + + test('should handle backslash parent directory traversal', () => { + expect(normalizePath('..\\..\\foo')).toBe('../../foo') + }) + + test('should normalize drive letter root to POSIX root', () => { + expect(normalizePath('C:\\')).toBe('/') + }) + }) + + describe('edge cases', () => { + test('should return dot for current directory', () => { + expect(normalizePath('.')).toBe('.') + }) + + test('should handle parent directory only', () => { + expect(normalizePath('..')).toBe('..') + }) + + test('should handle absolute root path', () => { + expect(normalizePath('/')).toBe('/') + }) + + test('should handle complex nested path', () => { + expect(normalizePath('a/b/c/../../d/./e/../f')).toBe('a/d/f') + }) + + test('should preserve trailing segments after parent traversal', () => { + expect(normalizePath('a/../b/../c')).toBe('c') + }) + }) +}) diff --git a/packages/python-sdk/e2b/template/main.py b/packages/python-sdk/e2b/template/main.py index 9d20c5fc3c..76ce739c62 100644 --- a/packages/python-sdk/e2b/template/main.py +++ b/packages/python-sdk/e2b/template/main.py @@ -21,6 +21,8 @@ read_dockerignore, read_gcp_service_account_json, get_caller_frame, + is_safe_relative, + normalize_path, ) from types import TracebackType @@ -65,9 +67,27 @@ def copy( srcs = [src] if isinstance(src, (str, Path)) else src for src_item in srcs: + normalized_src_item = normalize_path(str(src_item)) + normalized_dest = normalize_path(str(dest)) + + if not is_safe_relative(str(src_item)): + caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1) + stack_trace = None + if caller_frame is not None: + stack_trace = TracebackType( + tb_next=None, + tb_frame=caller_frame, + tb_lasti=caller_frame.f_lasti, + tb_lineno=caller_frame.f_lineno, + ) + + raise ValueError( + f"Source path {src_item} is outside of the context directory." + ).with_traceback(stack_trace) + args = [ - str(src_item), - str(dest), + normalized_src_item, + normalized_dest, user or "", pad_octal(mode) if mode else "", ] diff --git a/packages/python-sdk/e2b/template/utils.py b/packages/python-sdk/e2b/template/utils.py index 09f87ac095..58fb2e2628 100644 --- a/packages/python-sdk/e2b/template/utils.py +++ b/packages/python-sdk/e2b/template/utils.py @@ -58,12 +58,41 @@ def read_dockerignore(context_path: str) -> List[str]: def normalize_path(path: str) -> str: """ - Normalize path separators to forward slashes for glob patterns (glob expects / even on Windows). - - :param path: The path to normalize - :return: The normalized path + Normalize paths in a platform-independent, POSIX-style manner. + Mirrors the JS normalizePath behavior. """ - return path.replace(os.sep, "/") + if not path or path == ".": + return "." + + # Remove drive letter if present (e.g. "C:") + working_path = path + if re.match(r"^[a-zA-Z]:", path): + working_path = path[2:] + + # Determine if absolute + is_absolute = working_path.startswith("/") or working_path.startswith("\\") + + # Normalize separators to '/' + normalized_path = re.sub(r"[\\/]+", "/", working_path) + + # Split and process components + parts = [p for p in normalized_path.split("/") if p and p != "."] + + normalized = [] + + for part in parts: + if part == "..": + if normalized and normalized[-1] != "..": + normalized.pop() + elif not is_absolute: + normalized.append("..") + else: + normalized.append(part) + + # Reconstruct path + result = ("/" if is_absolute else "") + "/".join(normalized) + + return result or "." def get_all_files_in_path( @@ -339,3 +368,21 @@ def read_gcp_service_account_json( return f.read() else: return json.dumps(path_or_content) + + +def is_safe_relative(src: str) -> bool: + """ + Returns True if src is a relative path and does not contain any up-path parts. + Works on both Windows and Unix. + + :param src: The path to check + + :return: True if the path is a safe relative path, False otherwise + """ + norm_path = normalize_path(src) + return not ( + os.path.isabs(norm_path) + or norm_path == ".." + or norm_path.startswith("../") + or norm_path.startswith("..\\") + ) diff --git a/packages/python-sdk/tests/async/template_async/methods/test_from_dockerfile.py b/packages/python-sdk/tests/async/template_async/methods/test_from_dockerfile.py index 426ed010c2..0e456d0041 100644 --- a/packages/python-sdk/tests/async/template_async/methods/test_from_dockerfile.py +++ b/packages/python-sdk/tests/async/template_async/methods/test_from_dockerfile.py @@ -69,8 +69,8 @@ async def test_from_dockerfile_with_custom_user_and_workdir(): @pytest.mark.skip_debug() async def test_from_dockerfile_with_copy_chown(): dockerfile = """FROM node:24 -COPY --chown=myuser:mygroup app.js /app/ -COPY --chown=anotheruser config.json /config/""" +COPY --chown=myuser:mygroup app.js /app +COPY --chown=anotheruser config.json /config""" template = AsyncTemplate().from_dockerfile(dockerfile) @@ -80,14 +80,14 @@ async def test_from_dockerfile_with_copy_chown(): copy_instruction1 = instructions[2] assert copy_instruction1["type"] == InstructionType.COPY assert copy_instruction1["args"][0] == "app.js" - assert copy_instruction1["args"][1] == "/app/" + assert copy_instruction1["args"][1] == "/app" assert copy_instruction1["args"][2] == "myuser:mygroup" # user from --chown # Second COPY instruction copy_instruction2 = instructions[3] assert copy_instruction2["type"] == InstructionType.COPY assert copy_instruction2["args"][0] == "config.json" - assert copy_instruction2["args"][1] == "/config/" + assert copy_instruction2["args"][1] == "/config" assert ( copy_instruction2["args"][2] == "anotheruser" ) # user from --chown (without group) diff --git a/packages/python-sdk/tests/async/template_async/test_build.py b/packages/python-sdk/tests/async/template_async/test_build.py index f16da0bd4b..a755ff4ef4 100644 --- a/packages/python-sdk/tests/async/template_async/test_build.py +++ b/packages/python-sdk/tests/async/template_async/test_build.py @@ -89,14 +89,3 @@ async def test_build_template_with_resolve_symlinks(async_build, setup_test_fold ) await async_build(template) - - -@pytest.mark.skip_debug() -async def test_build_template_with_skip_cache(async_build, setup_test_folder): - template = ( - AsyncTemplate(file_context_path=setup_test_folder) - .skip_cache() - .from_image("ubuntu:22.04") - ) - - await async_build(template) diff --git a/packages/python-sdk/tests/async/template_async/test_stacktrace.py b/packages/python-sdk/tests/async/template_async/test_stacktrace.py index 08acc92068..3363e501e6 100644 --- a/packages/python-sdk/tests/async/template_async/test_stacktrace.py +++ b/packages/python-sdk/tests/async/template_async/test_stacktrace.py @@ -11,7 +11,7 @@ import e2b.template_async.main as template_async_main import e2b.template_async.build_api as build_api_mod -non_existent_path = "/nonexistent/path" +non_existent_path = "./nonexistent/path" # map template alias -> failed step index failure_map: dict[str, Optional[int]] = { diff --git a/packages/python-sdk/tests/shared/template/utils/get_all_files_in_path.py b/packages/python-sdk/tests/shared/template/utils/test_get_all_files_in_path.py similarity index 100% rename from packages/python-sdk/tests/shared/template/utils/get_all_files_in_path.py rename to packages/python-sdk/tests/shared/template/utils/test_get_all_files_in_path.py diff --git a/packages/python-sdk/tests/shared/template/utils/test_is_safe_relative.py b/packages/python-sdk/tests/shared/template/utils/test_is_safe_relative.py new file mode 100644 index 0000000000..f764290dc4 --- /dev/null +++ b/packages/python-sdk/tests/shared/template/utils/test_is_safe_relative.py @@ -0,0 +1,40 @@ +from e2b.template.utils import is_safe_relative + + +class TestAbsolutePaths: + def test_should_return_true_for_unix_absolute_paths(self): + assert is_safe_relative("/absolute/path") is False + + +class TestParentDirectoryTraversal: + def test_should_return_true_for_parent_directory_only(self): + assert is_safe_relative("..") is False + + def test_should_return_true_for_paths_starting_with_dot_dot_slash(self): + assert is_safe_relative("../file.txt") is False + + def test_should_return_true_for_paths_starting_with_dot_dot_backslash(self): + assert is_safe_relative("..\\file.txt") is False + + def test_should_return_true_for_normalized_paths_that_escape_context(self): + assert is_safe_relative("foo/../../bar") is False + + +class TestValidRelativePaths: + def test_should_return_false_for_simple_relative_paths(self): + assert is_safe_relative("file.txt") is True + assert is_safe_relative("folder/file.txt") is True + + def test_should_return_false_for_current_directory_references(self): + assert is_safe_relative(".") is True + assert is_safe_relative("./file.txt") is True + assert is_safe_relative("./folder/file.txt") is True + + def test_should_return_false_for_glob_patterns(self): + assert is_safe_relative("*.txt") is True + assert is_safe_relative("**/*.ts") is True + assert is_safe_relative("src/**/*") is True + + def test_should_return_false_for_hidden_files_and_directories(self): + assert is_safe_relative(".hidden") is True + assert is_safe_relative(".config/settings") is True diff --git a/packages/python-sdk/tests/shared/template/utils/test_normalize_path.py b/packages/python-sdk/tests/shared/template/utils/test_normalize_path.py new file mode 100644 index 0000000000..ae1fd21a41 --- /dev/null +++ b/packages/python-sdk/tests/shared/template/utils/test_normalize_path.py @@ -0,0 +1,61 @@ +from e2b.template.utils import normalize_path + + +class TestBasicPathNormalization: + def test_should_resolve_parent_directory_references(self): + assert normalize_path("/foo/bar/../baz") == "/foo/baz" + + def test_should_remove_current_directory_references(self): + assert normalize_path("foo/./bar") == "foo/bar" + + def test_should_collapse_multiple_slashes(self): + assert normalize_path("foo//bar///baz") == "foo/bar/baz" + + def test_should_handle_multiple_parent_directory_traversals_in_relative_paths(self): + assert normalize_path("../foo/../../bar") == "../../bar" + + def test_should_not_traverse_past_root_for_absolute_paths(self): + assert normalize_path("/foo/../../bar") == "/bar" + + def test_should_return_dot_for_empty_path(self): + assert normalize_path("") == "." + + def test_should_remove_leading_current_directory_reference(self): + assert normalize_path("./foo/bar") == "foo/bar" + + +class TestWindowsPathsConvertedToPosixStyle: + """ + Note: On Unix systems, backslash is a valid filename character, not a path separator. + The Python implementation uses os.path.normpath which is OS-dependent. + These tests use forward slashes which work correctly cross-platform. + """ + + def test_should_normalize_windows_path_with_drive_letter_and_forward_slashes(self): + assert normalize_path("C:/foo/bar/../baz") == "/foo/baz" + + def test_should_strip_drive_letter(self): + assert normalize_path("C:/foo/bar") == "/foo/bar" + + def test_should_strip_lowercase_drive_letter(self): + assert normalize_path("c:/foo/bar") == "/foo/bar" + + def test_should_strip_drive_letter_with_simple_path(self): + assert normalize_path("D:/") == "/" + + +class TestEdgeCases: + def test_should_return_dot_for_current_directory(self): + assert normalize_path(".") == "." + + def test_should_handle_parent_directory_only(self): + assert normalize_path("..") == ".." + + def test_should_handle_absolute_root_path(self): + assert normalize_path("/") == "/" + + def test_should_handle_complex_nested_path(self): + assert normalize_path("a/b/c/../../d/./e/../f") == "a/d/f" + + def test_should_preserve_trailing_segments_after_parent_traversal(self): + assert normalize_path("a/../b/../c") == "c" diff --git a/packages/python-sdk/tests/sync/template_sync/methods/test_from_dockerfile.py b/packages/python-sdk/tests/sync/template_sync/methods/test_from_dockerfile.py index b8ff1966ee..a48469bf5c 100644 --- a/packages/python-sdk/tests/sync/template_sync/methods/test_from_dockerfile.py +++ b/packages/python-sdk/tests/sync/template_sync/methods/test_from_dockerfile.py @@ -69,8 +69,8 @@ def test_from_dockerfile_with_custom_user_and_workdir(): @pytest.mark.skip_debug() def test_from_dockerfile_with_copy_chown(): dockerfile = """FROM node:24 -COPY --chown=myuser:mygroup app.js /app/ -COPY --chown=anotheruser config.json /config/""" +COPY --chown=myuser:mygroup app.js /app +COPY --chown=anotheruser config.json /config""" template = Template().from_dockerfile(dockerfile) @@ -80,14 +80,14 @@ def test_from_dockerfile_with_copy_chown(): copy_instruction1 = instructions[2] assert copy_instruction1["type"] == InstructionType.COPY assert copy_instruction1["args"][0] == "app.js" - assert copy_instruction1["args"][1] == "/app/" + assert copy_instruction1["args"][1] == "/app" assert copy_instruction1["args"][2] == "myuser:mygroup" # user from --chown # Second COPY instruction copy_instruction2 = instructions[3] assert copy_instruction2["type"] == InstructionType.COPY assert copy_instruction2["args"][0] == "config.json" - assert copy_instruction2["args"][1] == "/config/" + assert copy_instruction2["args"][1] == "/config" assert ( copy_instruction2["args"][2] == "anotheruser" ) # user from --chown (without group) diff --git a/packages/python-sdk/tests/sync/template_sync/test_stacktrace.py b/packages/python-sdk/tests/sync/template_sync/test_stacktrace.py index 489f451dc7..99f77c50a1 100644 --- a/packages/python-sdk/tests/sync/template_sync/test_stacktrace.py +++ b/packages/python-sdk/tests/sync/template_sync/test_stacktrace.py @@ -11,7 +11,7 @@ import e2b.template_sync.main as template_sync_main import e2b.template_sync.build_api as build_api_mod -non_existent_path = "/nonexistent/path" +non_existent_path = "./nonexistent/path" # map template alias -> failed step index failure_map: dict[str, Optional[int]] = {