| // Copyright The Monogon Project Authors. |
| // SPDX-License-Identifier: Apache-2.0 |
| |
| // This is a test for PersistentVolumes provided by our provisioner. It tests |
| // that volumes have the right mount flags, and the expected quotas. |
| // |
| // The package here is a binary which will run in a Pod in our Kubernetes |
| // end-to-end test. See the function makeTestStatefulSet in |
| // metropolis/test/e2e/suites/kubernetes/kubernetes_helpers.go for how the Pod |
| // is created. |
| package main |
| |
| import ( |
| "errors" |
| "flag" |
| "fmt" |
| "os" |
| "path/filepath" |
| "syscall" |
| "time" |
| |
| "golang.org/x/sys/unix" |
| |
| "source.monogon.dev/osbase/blockdev" |
| ) |
| |
| // This is a copy of the constant in metropolis/node/kubernetes/provisioner.go. |
| const inodeCapacityRatio = 4 * 512 |
| |
| var runtimeClass = flag.String("runtimeclass", "", "Name of the runtime class") |
| |
| // checkFilesystemVolume checks that the filesystem containing path has the |
| // given mount flags and capacity. |
| func checkFilesystemVolume(path string, expectedFlags int64, expectedBytes uint64) error { |
| var statfs unix.Statfs_t |
| err := unix.Statfs(path, &statfs) |
| if err != nil { |
| return fmt.Errorf("failed to statfs volume %q: %w", path, err) |
| } |
| |
| if statfs.Flags&unix.ST_RDONLY != expectedFlags&unix.ST_RDONLY { |
| return fmt.Errorf("volume %q has readonly flag %v, expected the opposite", path, statfs.Flags&unix.ST_RDONLY != 0) |
| } |
| if statfs.Flags&unix.ST_NOSUID != expectedFlags&unix.ST_NOSUID { |
| return fmt.Errorf("volume %q has nosuid flag %v, expected the opposite", path, statfs.Flags&unix.ST_NOSUID != 0) |
| } |
| if statfs.Flags&unix.ST_NODEV != expectedFlags&unix.ST_NODEV { |
| return fmt.Errorf("volume %q has nodev flag %v, expected the opposite", path, statfs.Flags&unix.ST_NODEV != 0) |
| } |
| if statfs.Flags&unix.ST_NOEXEC != expectedFlags&unix.ST_NOEXEC { |
| return fmt.Errorf("volume %q has noexec flag %v, expected the opposite", path, statfs.Flags&unix.ST_NOEXEC != 0) |
| } |
| |
| sizeBytes := statfs.Blocks * uint64(statfs.Bsize) |
| if sizeBytes != expectedBytes { |
| return fmt.Errorf("volume %q has capacity %v bytes, expected %v bytes", path, sizeBytes, expectedBytes) |
| } |
| expectedFiles := expectedBytes / inodeCapacityRatio |
| if statfs.Files != expectedFiles { |
| return fmt.Errorf("volume %q has capacity for %v files, expected %v files", path, statfs.Files, expectedFiles) |
| } |
| |
| // Try writing a file. This should only work if the volume is not read-only. |
| err = os.WriteFile(filepath.Join(path, "test.txt"), []byte("hello"), 0o644) |
| if expectedFlags&unix.ST_RDONLY != 0 { |
| if err == nil { |
| return fmt.Errorf("write did not fail in read-only volume %q", path) |
| } else if !errors.Is(err, syscall.EROFS) { |
| return fmt.Errorf("write failed with unexpected error in read-only volume %q: %w", path, err) |
| } |
| } else if err != nil { |
| return fmt.Errorf("failed to write file in volume %q: %w", path, err) |
| } |
| |
| return nil |
| } |
| |
| func checkBlockVolume(path string, expectedBytes uint64) error { |
| blk, err := blockdev.Open(path) |
| if err != nil { |
| return fmt.Errorf("failed to open block device %q: %w", path, err) |
| } |
| defer blk.Close() |
| sizeBytes := blk.BlockCount() * blk.BlockSize() |
| if sizeBytes != int64(expectedBytes) { |
| return fmt.Errorf("block device %q has size %v bytes, expected %v bytes", path, sizeBytes, expectedBytes) |
| } |
| return nil |
| } |
| |
| func testPersistentVolume(expectedBytes uint64) error { |
| if err := checkFilesystemVolume("/vol/default", 0, expectedBytes); err != nil { |
| return err |
| } |
| if err := checkFilesystemVolume("/vol/readonly", unix.ST_RDONLY, expectedBytes); err != nil { |
| return err |
| } |
| // Block volumes are not supported on gVisor. |
| if *runtimeClass != "gvisor" { |
| if err := checkBlockVolume("/vol/block", expectedBytes); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func main() { |
| flag.Parse() |
| fmt.Printf("PersistentVolume tests starting on %s...\n", *runtimeClass) |
| |
| if err := testPersistentVolume(1 * 1024 * 1024); err != nil { |
| fmt.Println(err.Error()) |
| // The final log line communicates the test outcome to the e2e test. |
| fmt.Println("[INIT-FAILED]") |
| } else { |
| fmt.Println("[INIT-PASSED]") |
| |
| nextLog := time.Now().Add(10 * time.Second) |
| for { |
| if err := testPersistentVolume(4 * 1024 * 1024); err != nil { |
| if time.Now().After(nextLog) { |
| fmt.Println("Waiting for resize:", err.Error()) |
| nextLog = time.Now().Add(10 * time.Second) |
| } |
| time.Sleep(100 * time.Millisecond) |
| } else { |
| fmt.Println("[RESIZE-PASSED]") |
| break |
| } |
| } |
| } |
| |
| // Sleep forever, because if the process exits, Kubernetes will restart it. |
| for { |
| time.Sleep(time.Hour) |
| } |
| } |