blob: 6af125831a65724b5c5d0f640b91faa42db3b5e0 [file] [log] [blame]
// 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)
}
}