o
odinpkg.dev
packages / library / Simple_container_runtime

Simple_container_runtime

5f8775alibrary

No description provided.

No license · updated 10 months ago

Simple Container Runtime

วันนี้เราจะมาลองสร้าง Simple Container Runtime กันใน ภาษา Odin

ในการสร้าง Container run time เเบบง่ายๆๆจะต้องมี

  • Root File system ( Linux Alpine) https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/x86_64/alpine-minirootfs-3.22.0-x86_64.tar.gz

  • Linux Name Space

    • Domain Namespace เเยก Hostname
    • Process Namespace เเยก ProcessID
    • Net Namespace เเยก Network stack
    • Mnt Namespace เเยก Filesystem
  • Overlay Filesystem การทำ union file system ทำให้เราสามารถทำ isolation ตัว rootfs เพื่อป้องกันไม่ให้ตัว rootfs ถูกเขียนทับ

งั้นมาเริ่มกันเลยย

package main

import "core:fmt" # module formatting เหมือนกับ GO fmt.println("Hello")
import os "core:os" # module ของ OS ใช้จัดการเรื่อง env arguments file operations
import "core:strings" # module จัดการ string
import posix "core:sys/posix" # module system call
import c "core:c" # module ที่ใช้เรีัยก C library

ให้ Odin เรียกใช้ lib ของ ภาษา C

foreign import libc "system:c"

foreign libc {
	unshare :: proc "c" (flags: c.int) -> c.int ---
	setns :: proc "c" (fd: c.int, nstype: c.int) -> c.int ---
	mount :: proc "c" (source, target, fstype: cstring, flags: c.ulong, data: cstring) -> c.int ---
	umount2 :: proc "c" (target: cstring, flags: c.int) -> c.int ---
	execvp :: proc "c" (file: cstring, argv: [^]cstring) -> c.int ---
	chroot :: proc "c" (path: cstring) -> c.int ---
	chdir :: proc "c" (path: cstring) -> c.int ---
	sethostname :: proc "c" (name: cstring, len: c.size_t) -> c.int ---
}

Set ID ให้ กับ เเต่ละ Name Space ที่เรากำลังจะสร้าง

CLONE_NEWNS :: int(0x00020000)
CLONE_NEWPID :: int(0x20000000)
CLONE_NEWNET :: int(0x40000000)
CLONE_NEWUTS :: int(0x04000000)

MS_BIND :: int(0x00001000)
MS_MOVE :: int(0x00002000)
MS_REC :: int(0x00004000)
MS_PRIVATE :: int(0x00040000)

MNT_DETACH :: int(0x2)

สร้าง struct มาเก็บ config ของ Container

Config :: struct {
	rootfs: string,
	hostname: string,
	net_mode: string,
	cmd: []string,
}

สร้าง function มารับ ค่าจ่า Command line เเล้วมาใส่ ใน Struct ที่เราสร้างไว้

parse_args :: proc() -> Config {
	cfg: Config
	cfg.rootfs = "./rootfs"
	cfg.net_mode = "host"
	cfg.hostname = ""

	start := 1

for i := 1; i < len(os.args); i += 1 {
	arg := os.args[i]
	if arg == "--" {
		start = i + 1
		break
	}

	if arg == "--rootfs" && i+1 < len(os.args) {
		cfg.rootfs = os.args[i+1]
		i += 1
		
		continue
	
	}
	
	if arg == "--net" && i+1 < len(os.args) {
		mode := os.args[i+1]
	
		if mode == "host" || mode == "none" {
			cfg.net_mode = mode
		}
		i += 1
		continue
		}
	
		if arg == "--hostname" && i+1 < len(os.args) {
			cfg.hostname = os.args[i+1]
			i += 1
			continue
		}
	}
	
	if start < len(os.args) {
		cfg.cmd = os.args[start:]
	} else {
		cfg.cmd = []string{"/bin/sh"}
	}

	return cfg
}

มาเริ่ม สร้าง Container runtime กานนน

main :: proc() {

	cfg := parse_args()

	flags := CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUTS

	if cfg.net_mode == "none" {
		flags |= CLONE_NEWNET
	}

เราจะเริ่มจาก เรียกใช้ function parse_args() เพื่อดึงค่า จาก struct Config เเล้วก็ ประกาศ flag ของ Namespace ที่เราจะสร้าง คือ Hostname, Process, Mount Namespace โดยที่ ถ้า config net_mode เป็น none ก็จะเพิ่ม Net Namespace มาด้วย

จากนั้น

	if unshare(c.int(flags)) != 0 {
		err := posix.get_errno()
		fmt.printf("unshare failed: %v\n", err)
		return
	}

เรียก system call เเล้วใส่ flag เข้าไป เพื่อทำการเเยก process ออกจาก namespace

	root := strings.clone_to_cstring("/", context.temp_allocator)
	if mount(nil, root, nil, c.ulong(MS_PRIVATE|MS_REC), nil) != 0 {
		err := posix.get_errno()
		fmt.printf("mount(MS_PRIVATE|MS_REC) failed: %v\n", err)
		
		return
	}

Explain function mount

int mount(const char *source, const char *target,
          const char *filesystemtype, unsigned long mountflags,
          const void *data);

จากนั้น เเปลง string เป็น C string เพื่อไปใช้กับ system call เเละทำให้ mount point ของ process เป็น private

	pid := posix.fork()

	if pid < 0 {
		err := posix.get_errno()
		fmt.printf("fork failed: %v\n", err)
		return
	}

จากนั้น Clone Process เดิม มาใช่ใน namespace

	if pid == 0 {
	
	lower := cfg.rootfs
	
	ovl_base := "./ovl"
	upper := fmt.tprintf("%s/upper", ovl_base)
	work := fmt.tprintf("%s/work", ovl_base)
	merged := fmt.tprintf("%s/merged", ovl_base)
	
	_ = os.make_directory(ovl_base, 0)
	_ = os.make_directory(upper, 0)
	_ = os.make_directory(work, 0)
	_ = os.make_directory(merged, 0)

จากนั้น ทำการ overlay file system

	ovl_src := strings.clone_to_cstring("overlay", context.temp_allocator)
	ovl_dir := strings.clone_to_cstring(merged, context.temp_allocator)
	ovl_fs := strings.clone_to_cstring("overlay", context.temp_allocator)
	opts_str := fmt.tprintf("lowerdir=%s,upperdir=%s,workdir=%s", lower, upper, work)
	ovl_opts := strings.clone_to_cstring(opts_str, context.temp_allocator)
	
	  
	
	if mount(ovl_src, ovl_dir, ovl_fs, c.ulong(0), ovl_opts) != 0 {
		err := posix.get_errno()
		fmt.printf("overlay mount failed: %v\n", err)
		os.exit(1)
	}

เอาตัว overlay มา mount

	rt := strings.clone_to_cstring(merged, context.temp_allocator)
	
	if chroot(rt) != 0 {
		err := posix.get_errno()
		fmt.printf("chroot failed: %v\n", err)
		os.exit(1)
	}

	if chdir(strings.clone_to_cstring("/", context.temp_allocator)) != 0 {
		err := posix.get_errno()
		fmt.printf("chdir failed: %v\n", err)
		os.exit(1)
	}

	if cfg.hostname != "" {
		cname := strings.clone_to_cstring(cfg.hostname, context.temp_allocator)
		if sethostname(cname, c.size_t(len(cfg.hostname))) != 0 {
		err := posix.get_errno()
		fmt.printf("sethostname failed: %v\n", err)
	}

จากนั้น เราจะมาเตรียม rootfs เเล้วก็เปียน root เดิม ให้การ root ของ rootfs เเล้วตั้งค่า host name

	_ = os.make_directory("/proc", 0)
	_ = os.make_directory("/sys", 0)

  

	proc_src := strings.clone_to_cstring("proc", context.temp_allocator)
	proc_dir := strings.clone_to_cstring("/proc", context.temp_allocator)
	proc_fs := strings.clone_to_cstring("proc", context.temp_allocator)

	if mount(proc_src, proc_dir, proc_fs, c.ulong(0), nil) != 0 {
		err := posix.get_errno()
		fmt.printf("mount /proc failed: %v\n", err)
		os.exit(1)
	}

	sys_src := strings.clone_to_cstring("sysfs", context.temp_allocator)
	sys_dir := strings.clone_to_cstring("/sys", context.temp_allocator)
	sys_fs := strings.clone_to_cstring("sysfs", context.temp_allocator)
	_ = mount(sys_src, sys_dir, sys_fs, c.ulong(0), nil)

	argv: [dynamic]cstring
	
	for arg in cfg.cmd {
		_, _ = append(&argv, strings.clone_to_cstring(arg, context.temp_allocator))
	
	}

	_, _ = append(&argv, cast(cstring)nil)

	if execvp(argv[0], raw_data(argv)) != 0 {
		err := posix.get_errno()
		fmt.printf("execvp failed: %v\n", err)
		os.exit(127)
	}
  • เตรียม /proc และ /sys → mount procfs และ sysfs
  • สร้าง argv สำหรับ command ที่จะรัน
  • execvp() → เปลี่ยนตัวเองเป็น process จริงใน container
} else {
		status: i32
		_ = posix.waitpid(pid, &status, posix.Wait_Flags{})
		if posix.WIFEXITED(status) {
			ex := posix.WEXITSTATUS(status)
		
			fmt.printf("child exited with status %d\n", ex)
		} else if posix.WIFSIGNALED(status) {
			sig := posix.WTERMSIG(status)
			fmt.printf("child killed by signal %d\n", sig)
		} else {
			fmt.println("child terminated")
		}
	}
}

จากนั้น ถ้ามีอะไร ผิดพลาดก็

  • ออกจากเอง → print exit code
  • ถูก kill → print signal
  • อื่น ๆ → print terminated