Files
velero/pkg/uploader/block/dev_linux_test.go
T
lyndon-li a0749e7658
Run the E2E test on kind / get-go-version (push) Failing after 1m3s
Run the E2E test on kind / build (push) Has been skipped
Run the E2E test on kind / setup-test-matrix (push) Successful in 3s
Run the E2E test on kind / run-e2e-test (push) Has been skipped
Main CI / get-go-version (push) Successful in 13s
Main CI / Build (push) Failing after 22s
Block dev for restore (#10013)
* add block dev operations for block uploader restore

Signed-off-by: Lyndon-Li <lyonghui@vmware.com>

* add block dev operations for block uploader restore

Signed-off-by: Lyndon-Li <lyonghui@vmware.com>

---------

Signed-off-by: Lyndon-Li <lyonghui@vmware.com>
2026-07-17 16:35:19 +08:00

387 lines
10 KiB
Go

//go:build linux
// +build linux
/*
Copyright The Velero Contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package block
import (
"os"
"path/filepath"
"syscall"
"testing"
"time"
"github.com/cockroachdb/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type fakeBlockDevFileInfo struct{}
func (fakeBlockDevFileInfo) Name() string { return "fake-blk" }
func (fakeBlockDevFileInfo) Size() int64 { return 0 }
func (fakeBlockDevFileInfo) Mode() os.FileMode { return os.ModeDevice }
func (fakeBlockDevFileInfo) ModTime() time.Time { return time.Time{} }
func (fakeBlockDevFileInfo) IsDir() bool { return false }
func (fakeBlockDevFileInfo) Sys() any {
return &syscall.Stat_t{Mode: syscall.S_IFBLK}
}
func TestResolveSymlink(t *testing.T) {
testCases := []struct {
name string
setupPath func(t *testing.T) string
expectError bool
errContains string
checkResult func(t *testing.T, input, result string)
}{
{
name: "path does not exist returns error",
setupPath: func(t *testing.T) string {
t.Helper()
return filepath.Join(t.TempDir(), "nonexistent")
},
expectError: true,
errContains: "stat",
},
{
name: "regular file returns same path",
setupPath: func(t *testing.T) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "regular-*")
require.NoError(t, err)
f.Close()
return f.Name()
},
checkResult: func(t *testing.T, input, result string) {
t.Helper()
assert.Equal(t, input, result)
},
},
{
name: "directory returns same path",
setupPath: func(t *testing.T) string {
t.Helper()
return t.TempDir()
},
checkResult: func(t *testing.T, input, result string) {
t.Helper()
assert.Equal(t, input, result)
},
},
{
name: "symlink to existing file returns target real path",
setupPath: func(t *testing.T) string {
t.Helper()
dir := t.TempDir()
target, err := os.CreateTemp(dir, "target-*")
require.NoError(t, err)
target.Close()
linkPath := filepath.Join(dir, "link")
require.NoError(t, os.Symlink(target.Name(), linkPath))
return linkPath
},
checkResult: func(t *testing.T, input, result string) {
t.Helper()
assert.NotEqual(t, input, result)
fi, err := os.Lstat(result)
require.NoError(t, err)
assert.Zero(t, fi.Mode()&os.ModeSymlink)
},
},
{
name: "symlink to existing directory returns resolved path",
setupPath: func(t *testing.T) string {
t.Helper()
outer := t.TempDir()
inner := t.TempDir()
linkPath := filepath.Join(outer, "dirlink")
require.NoError(t, os.Symlink(inner, linkPath))
return linkPath
},
checkResult: func(t *testing.T, input, result string) {
t.Helper()
assert.NotEqual(t, input, result)
fi, err := os.Lstat(result)
require.NoError(t, err)
assert.True(t, fi.IsDir())
},
},
{
name: "broken symlink — target does not exist — returns error",
setupPath: func(t *testing.T) string {
t.Helper()
dir := t.TempDir()
linkPath := filepath.Join(dir, "broken-link")
require.NoError(t, os.Symlink(filepath.Join(dir, "nonexistent-target"), linkPath))
return linkPath
},
expectError: true,
errContains: "no such file or directory",
},
{
name: "chain of symlinks is fully resolved",
setupPath: func(t *testing.T) string {
t.Helper()
dir := t.TempDir()
// real → link1 → link2 (two-hop chain)
real, err := os.CreateTemp(dir, "real-*")
require.NoError(t, err)
real.Close()
link1 := filepath.Join(dir, "link1")
require.NoError(t, os.Symlink(real.Name(), link1))
link2 := filepath.Join(dir, "link2")
require.NoError(t, os.Symlink(link1, link2))
return link2
},
checkResult: func(t *testing.T, input, result string) {
t.Helper()
assert.NotEqual(t, input, result)
fi, err := os.Lstat(result)
require.NoError(t, err)
assert.Zero(t, fi.Mode()&os.ModeSymlink)
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
input := tc.setupPath(t)
result, err := resolveSymlink(input)
if tc.expectError {
require.Error(t, err)
if tc.errContains != "" {
require.ErrorContains(t, err, tc.errContains)
}
assert.Empty(t, result)
} else {
require.NoError(t, err)
if tc.checkResult != nil {
tc.checkResult(t, input, result)
}
}
})
}
}
func TestOpenBlockDevice(t *testing.T) {
testCases := []struct {
name string
setupPath func(t *testing.T) string
read bool
expectError bool
errContains string
injectLstat func(string) (os.FileInfo, error)
injectOpenFile func(string, int, os.FileMode) (*os.File, error)
}{
{
name: "path does not exist — resolveSymlink fails",
setupPath: func(t *testing.T) string {
t.Helper()
return filepath.Join(t.TempDir(), "nonexistent")
},
read: true,
expectError: true,
errContains: "resolveSymlink",
},
{
name: "regular file is not a block device — read mode",
setupPath: func(t *testing.T) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "regular-*")
require.NoError(t, err)
f.Close()
return f.Name()
},
read: true,
expectError: true,
errContains: "is not a block device",
},
{
name: "regular file is not a block device — write mode",
setupPath: func(t *testing.T) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "regular-*")
require.NoError(t, err)
f.Close()
return f.Name()
},
read: false,
expectError: true,
errContains: "is not a block device",
},
{
name: "directory is not a block device",
setupPath: func(t *testing.T) string {
t.Helper()
return t.TempDir()
},
read: true,
expectError: true,
errContains: "is not a block device",
},
{
name: "symlink to regular file is not a block device",
setupPath: func(t *testing.T) string {
t.Helper()
dir := t.TempDir()
target, err := os.CreateTemp(dir, "target-*")
require.NoError(t, err)
target.Close()
linkPath := filepath.Join(dir, "link")
require.NoError(t, os.Symlink(target.Name(), linkPath))
return linkPath
},
read: true,
expectError: true,
errContains: "is not a block device",
},
{
name: "broken symlink — resolveSymlink fails",
setupPath: func(t *testing.T) string {
t.Helper()
dir := t.TempDir()
linkPath := filepath.Join(dir, "broken-link")
require.NoError(t, os.Symlink(filepath.Join(dir, "ghost"), linkPath))
return linkPath
},
read: true,
expectError: true,
errContains: "resolveSymlink",
},
{
name: "EACCES from OpenFile — permission denied message",
setupPath: func(t *testing.T) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "blk-*")
require.NoError(t, err)
f.Close()
return f.Name()
},
read: true,
expectError: true,
errContains: "no permission to open device",
injectLstat: func(_ string) (os.FileInfo, error) {
return fakeBlockDevFileInfo{}, nil
},
injectOpenFile: func(name string, _ int, _ os.FileMode) (*os.File, error) {
t.Helper()
return nil, &os.PathError{Op: "open", Path: name, Err: syscall.EACCES}
},
},
{
name: "EPERM from OpenFile — permission denied message",
setupPath: func(t *testing.T) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "blk-*")
require.NoError(t, err)
f.Close()
return f.Name()
},
read: false,
expectError: true,
errContains: "no permission to open device",
injectLstat: func(_ string) (os.FileInfo, error) {
return fakeBlockDevFileInfo{}, nil
},
injectOpenFile: func(name string, _ int, _ os.FileMode) (*os.File, error) {
return nil, &os.PathError{Op: "open", Path: name, Err: syscall.EPERM}
},
},
{
name: "generic OpenFile error — unable to open device message",
setupPath: func(t *testing.T) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "blk-*")
require.NoError(t, err)
f.Close()
return f.Name()
},
read: true,
expectError: true,
errContains: "unable to open device",
injectLstat: func(_ string) (os.FileInfo, error) {
t.Helper()
return fakeBlockDevFileInfo{}, nil
},
injectOpenFile: func(name string, _ int, _ os.FileMode) (*os.File, error) {
t.Helper()
return nil, &os.PathError{Op: "open", Path: name, Err: syscall.EIO}
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Cleanup(func() {
lstatFunc = os.Lstat
openFileFunc = os.OpenFile
})
if tc.injectLstat != nil {
lstatFunc = tc.injectLstat
}
if tc.injectOpenFile != nil {
openFileFunc = tc.injectOpenFile
}
path := tc.setupPath(t)
f, err := openBlockDevice(path, tc.read)
if tc.expectError {
require.Error(t, err)
if tc.errContains != "" {
require.ErrorContains(t, err, tc.errContains)
}
assert.Nil(t, f)
} else {
require.NoError(t, err)
require.NotNil(t, f)
f.Close()
}
})
}
}
func TestBlkZeroOut(t *testing.T) {
t.Run("closed file returns error", func(t *testing.T) {
f, err := os.CreateTemp(t.TempDir(), "blkzeroout-test-*")
require.NoError(t, err)
err = f.Close()
require.NoError(t, err)
err = blkZeroOut(f, 0, 1024)
assert.Error(t, err)
})
t.Run("regular file returns ioctl error", func(t *testing.T) {
f, err := os.CreateTemp(t.TempDir(), "blkzeroout-test-*")
require.NoError(t, err)
defer f.Close()
err = blkZeroOut(f, 0, 1024)
require.Error(t, err)
assert.Contains(t, err.Error(), "error calling ioctl on block dev")
// On regular files, ioctl with BLKZEROOUT should fail with ENOTTY (inappropriate ioctl for device) or EINVAL
isENOTTY := errors.Is(err, syscall.ENOTTY)
isEINVAL := errors.Is(err, syscall.EINVAL)
assert.True(t, isENOTTY || isEINVAL, "expected error to be ENOTTY or EINVAL, got: %v", err)
})
}