Files
zeekay b61441f7d7 Volume: node-local persistent volumes, under our own name
A fork of Rancher's local-path-provisioner at v0.0.36. The behaviour is
unchanged and deliberately so — a directory on the node a pod lands on is
the right thing for a disk. What changes is identity.

The provisioner answers to hanzo.ai/volume rather than rancher.io/local-path.
That is not branding: two provisioners sharing one name race for the same
claims, and a stock k3s carries the upstream one, so a cluster running both
would settle our storage by chance. Data lives at /var/lib/hanzo/volume, so
reading a Hanzo cluster does not mean learning another vendor's directory.

Apache-2.0 as published, unaltered, with Rancher's attribution in NOTICE.

The README says what this is not: durability. A volume here dies with its
node. That is hanzoai/replicate's question, one layer up, and keeping the
two apart is what lets the disk stay this simple.
2026-09-07 17:30:01 -07:00

411 lines
10 KiB
Go

//go:build e2e
package test
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/kelseyhightower/envconfig"
"github.com/stretchr/testify/suite"
)
const (
hostPathVolumeType = "hostPath"
localVolumeType = "local"
)
type PodTestSuite struct {
suite.Suite
config testConfig
kustomizeDir string
}
func (p *PodTestSuite) SetupSuite() {
err := envconfig.Process("test", &p.config)
if err != nil {
panic(err)
}
p.T().Logf("using test config: %+v", p.config)
p.TearDownSuite()
//kind load docker-image "$image"
cmds := []string{
fmt.Sprintf("kind create cluster --config=%s --wait=120s", testdataFile("kind-cluster.yaml")),
fmt.Sprintf("kind load docker-image %s", p.config.IMAGE),
}
for _, cmd := range cmds {
_, err = runCmd(
p.T(),
cmd,
"",
p.config.envs(),
nil,
)
if err != nil {
p.FailNow("", "failed to create the cluster or load image", err)
}
}
}
func (p *PodTestSuite) TearDownSuite() {
err := deleteCluster(
p.T(),
p.config.envs(),
)
if err != nil {
p.Failf("", "failed to delete the cluster: %v", err)
}
}
func (p *PodTestSuite) TearDownTest() {
err := deleteKustomizeDeployment(
p.T(),
p.kustomizeDir,
p.config.envs(),
)
if err != nil {
p.Failf("", "failed to delete the deployment: %v", err)
}
}
func TestPVCTestSuite(t *testing.T) {
suite.Run(t, new(PodTestSuite))
}
func (p *PodTestSuite) TestPodWithHostPathVolume() {
p.kustomizeDir = "pod"
runTest(p, []string{p.config.IMAGE}, "ready", hostPathVolumeType)
}
func (p *PodTestSuite) TestPodWithLocalVolume() {
p.kustomizeDir = "pod-with-local-volume"
runTest(p, []string{p.config.IMAGE}, "ready", localVolumeType)
}
func (p *PodTestSuite) TestPodWithLocalVolumeDefault() {
p.kustomizeDir = "pod-with-default-local-volume"
runTest(p, []string{p.config.IMAGE}, "ready", localVolumeType)
}
func (p *PodTestSuite) TestPodWithNodeAffinity() {
p.kustomizeDir = "pod-with-node-affinity"
runTest(p, []string{p.config.IMAGE}, "ready", hostPathVolumeType)
}
func (p *PodTestSuite) TestPodWithRWOPVolume() {
p.kustomizeDir = "pod-with-rwop-volume"
runTest(p, []string{p.config.IMAGE}, "ready", localVolumeType)
}
func (p *PodTestSuite) TestPodWithSecurityContext() {
p.kustomizeDir = "pod-with-security-context"
kustomizeDir := testdataFile(p.kustomizeDir)
runTest(p, []string{p.config.IMAGE}, "podscheduled", hostPathVolumeType)
cmd := fmt.Sprintf(`kubectl get pod -l %s=%s -o=jsonpath='{.items[0].status.conditions[?(@.type=="Ready")].reason}'`, LabelKey, LabelValue)
t := time.Tick(5 * time.Second)
loop:
for {
select {
case <-t:
c := createCmd(p.T(), cmd, kustomizeDir, p.config.envs(), nil)
output, err := c.CombinedOutput()
if err != nil {
p.T().Logf("%s: %v", c.String(), err)
}
if string(output) == "PodCompleted" {
break loop
}
case <-time.After(60 * time.Second):
p.FailNow("", "pod Ready condition reason should be PodCompleted")
break
}
}
}
func (p *PodTestSuite) TestPodWithSubpath() {
p.kustomizeDir = "pod-with-subpath"
runTest(p, []string{p.config.IMAGE}, "ready", hostPathVolumeType)
}
func (p *PodTestSuite) xxTestPodWithMultipleStorageClasses() {
p.kustomizeDir = "multiple-storage-classes"
runTest(p, []string{p.config.IMAGE}, "ready", hostPathVolumeType)
}
func (p *PodTestSuite) TestPodWithCustomNodeAffinityKey() {
p.kustomizeDir = "pod-with-custom-node-affinity-key"
kustomizeDir := testdataFile(p.kustomizeDir)
customLabel := "test.example.com/stable-id"
customValue := "my-stable-node"
// Label the kind-worker node with the custom label before deploying
labelCmd := fmt.Sprintf("kubectl label node kind-worker %s=%s --overwrite", customLabel, customValue)
_, err := runCmd(p.T(), labelCmd, "", p.config.envs(), nil)
if err != nil {
p.FailNow("", "failed to label node", err)
}
runTest(p, []string{p.config.IMAGE}, "ready", hostPathVolumeType)
// Verify the PV was created with the custom node affinity key
affinityCmd := `kubectl get pv -o jsonpath='{.items[0].spec.nodeAffinity.required.nodeSelectorTerms[0].matchExpressions[0].key}'`
c := createCmd(p.T(), affinityCmd, kustomizeDir, p.config.envs(), nil)
output, err := c.CombinedOutput()
if err != nil {
p.FailNow("", "failed to get PV node affinity key", err)
}
p.Equal(customLabel, strings.Trim(string(output), "'"), "PV should use the custom node affinity key")
// Verify the affinity value matches what was set on the node
valueCmd := `kubectl get pv -o jsonpath='{.items[0].spec.nodeAffinity.required.nodeSelectorTerms[0].matchExpressions[0].values[0]}'`
c = createCmd(p.T(), valueCmd, kustomizeDir, p.config.envs(), nil)
output, err = c.CombinedOutput()
if err != nil {
p.FailNow("", "failed to get PV node affinity value", err)
}
p.Equal(customValue, strings.Trim(string(output), "'"), "PV node affinity value should match the custom label value")
}
func (p *PodTestSuite) TestPodWithCustomPathPatternStorageClasses() {
p.kustomizeDir = "custom-path-pattern"
runTest(p, []string{p.config.IMAGE}, "ready", hostPathVolumeType)
}
func (p *PodTestSuite) TestPodWithSkipPathPatternCheck() {
p.kustomizeDir = "skip-path-pattern-check"
runTest(p, []string{p.config.IMAGE}, "ready", hostPathVolumeType)
}
func (p *PodTestSuite) TestPodWithSkipPathPatternCheckByAnnotation() {
p.kustomizeDir = "skip-path-pattern-check-by-annotation"
runTest(p, []string{p.config.IMAGE}, "ready", hostPathVolumeType)
}
// ADD THIS NEW TEST METHOD
func (p *PodTestSuite) TestPathTraversalPrevention() {
testCases := []struct {
name string
kustomizeDir string
expectedError string
description string
}{
{
name: "BasicDirectoryTraversal",
kustomizeDir: "security-basic-traversal-path",
expectedError: "invalid reference",
description: "Basic directory traversal with ../",
},
}
for _, tc := range testCases {
p.Run(tc.name, func() {
p.T().Logf("Testing: %s", tc.description)
p.kustomizeDir = tc.kustomizeDir
p.verifyProvisioningFailed(tc.expectedError)
})
}
}
func (p *PodTestSuite) verifyProvisioningFailed(expectedError string) {
kustomizeDir := testdataFile(p.kustomizeDir)
// Apply the deployment
cmds := []string{
fmt.Sprintf("kustomize edit add label %s:%s -f", LabelKey, LabelValue),
"kustomize build | kubectl apply -f -",
}
for _, cmd := range cmds {
_, err := runCmd(p.T(), cmd, kustomizeDir, p.config.envs(), nil)
if err != nil {
p.FailNow("", "failed to apply deployment", err)
}
}
// Wait a bit for provisioning to be attempted
time.Sleep(10 * time.Second)
// Check that PVC is not bound (provisioning should fail)
checkPVCCmd := fmt.Sprintf("kubectl get pvc -l %s=%s -o jsonpath='{.items[0].status.phase}'", LabelKey, LabelValue)
timeout := time.After(30 * time.Second)
tick := time.Tick(2 * time.Second)
for {
select {
case <-timeout:
// Timeout is expected - PVC should remain Pending due to security rejection
p.T().Log("PVC correctly remained in Pending state due to security validation")
return
case <-tick:
c := createCmd(p.T(), checkPVCCmd, kustomizeDir, p.config.envs(), nil)
output, err := c.CombinedOutput()
if err != nil {
p.T().Logf("PVC check error (expected): %v", err)
continue
}
pvcStatus := strings.TrimSpace(string(output))
p.T().Logf("PVC Status: %s", pvcStatus)
if pvcStatus == "Bound" {
p.FailNow("", "PVC was bound when it should have been rejected due to security validation")
}
// Check provisioner logs for security error
logCmd := `kubectl logs -l app=local-path-provisioner -n local-path-storage --tail=50`
logC := createCmd(p.T(), logCmd, kustomizeDir, p.config.envs(), nil)
logOutput, logErr := logC.CombinedOutput()
if logErr == nil && len(expectedError) > 0 {
logStr := string(logOutput)
if strings.Contains(logStr, expectedError) || strings.Contains(logStr, "invalid reference") {
p.T().Log("Security validation correctly rejected the malicious path pattern")
return
}
}
}
}
}
func runTest(p *PodTestSuite, images []string, waitCondition, volumeType string) {
kustomizeDir := testdataFile(p.kustomizeDir)
var cmds []string
for _, image := range images {
if len(image) > 0 {
cmds = append(cmds, fmt.Sprintf("kustomize edit set image ghcr.io/hanzoai/volume=%s", image))
}
}
cmds = append(
cmds,
fmt.Sprintf("kustomize edit add label %s:%s -f", LabelKey, LabelValue),
"kustomize build | kubectl apply -f -",
fmt.Sprintf("kubectl wait pod -l %s=%s --for condition=%s --timeout=120s", LabelKey, LabelValue, waitCondition),
)
for _, cmd := range cmds {
_, err := runCmd(
p.T(),
cmd,
kustomizeDir,
p.config.envs(),
nil,
)
if err != nil {
p.FailNow("", "failed to run command", cmd, err)
break
}
}
typeCheckCmd := fmt.Sprintf("kubectl get pv $(%s) -o jsonpath='{.spec.%s}'", "kubectl get pv -o jsonpath='{.items[0].metadata.name}'", volumeType)
c := createCmd(p.T(), typeCheckCmd, kustomizeDir, p.config.envs(), nil)
typeCheckOutput, err := c.CombinedOutput()
if err != nil {
p.FailNow("", "failed to check volume type: %v", err)
}
if len(typeCheckOutput) == 0 || !strings.Contains(string(typeCheckOutput), "path") {
p.FailNow("volume Type not correct")
}
}
func testdataFile(fields ...string) string {
return filepath.Join("testdata", filepath.Join(fields...))
}
func deleteKustomizeDeployment(t *testing.T, kustomizeDir string, envs []string) error {
_, err := runCmd(
t,
"kustomize build | kubectl delete --timeout=180s -f -",
testdataFile(kustomizeDir),
envs,
nil,
)
return err
}
func deleteCluster(t *testing.T, envs []string) error {
_, err := runCmd(
t,
"kind delete cluster",
"",
envs,
nil,
)
return err
}
func createCmd(t *testing.T, cmd, kustomizeDir string, envs []string, callback func(*exec.Cmd)) *exec.Cmd {
t.Logf("creating command: %s", cmd)
c := exec.Command("bash", "-c", cmd)
c.Env = append(os.Environ(), envs...)
c.Dir = kustomizeDir
if callback != nil {
callback(c)
}
return c
}
func runCmd(t *testing.T, cmd, kustomizeDir string, envs []string, callback func(*exec.Cmd)) (*exec.Cmd, error) {
t.Logf("running command: %s", cmd)
c := createCmd(t, cmd, kustomizeDir, envs, callback)
stdout, _ := c.StdoutPipe()
stderr, _ := c.StderrPipe()
err := c.Start()
if err != nil {
return nil, err
}
stopCh := make(chan struct{})
go func() {
mergedReader := io.MultiReader(stderr, stdout)
scanner := bufio.NewScanner(mergedReader)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
t.Log(scanner.Text())
}
close(stopCh)
}()
<-stopCh
err = c.Wait()
if err != nil {
return nil, err
}
return c, nil
}