Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Longhorn toolbox CLI #33

Merged
merged 31 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d9f1248
fix(io): add error handling in FindFile func
c3y1huang Apr 10, 2024
cc5957f
feat(io): implement max directory depth in FindFiles func
c3y1huang Apr 10, 2024
817c5ac
test(io): max directory depth in FindFiles func
c3y1huang Apr 10, 2024
66690b6
feat(io): list open files in a directory
c3y1huang Apr 12, 2024
e27d01f
test(io): ListOpenFiles
c3y1huang Apr 12, 2024
f1744ca
feat(io): check if a directory is empty
c3y1huang Apr 18, 2024
4616255
test(io): IsDirectoryEmpty
c3y1huang Apr 18, 2024
a606c0c
feat(kubernetes): retrieve metadata for kubernetes runtime objects
c3y1huang Apr 25, 2024
b30ebff
test(kubernetes): GetObjMetaAccesser
c3y1huang Apr 19, 2024
0caa657
feat(kubernetes): get in-cluster config
c3y1huang Apr 19, 2024
6fa5ffa
test(kubernetes): GetInClusterConfig
c3y1huang Apr 19, 2024
330094c
feat(kubernetes): is pod container in state
c3y1huang Apr 19, 2024
c7925c6
test(kubernetes): IsPodContainerInState
c3y1huang Apr 19, 2024
f2b6486
feat(misc): convert given value to string
c3y1huang Apr 25, 2024
c956cce
feat(misc): ConvertTypeToString
c3y1huang Apr 25, 2024
38236c3
feat(longhorn): get volume name from replica data directory name
c3y1huang Apr 25, 2024
d552f0e
test(longhorn): GetVolumeNameFromReplicaDataDirectoryName
c3y1huang Apr 25, 2024
55c3183
refactor: move IsEngineProcess from utils to longhorn package
c3y1huang Apr 25, 2024
1976383
refactor: move IsMountPointReadOnly from utils to kubernetes package
c3y1huang Apr 25, 2024
c1ac4cc
feat(kubernetes): create/delete/get ClusterRole
c3y1huang Apr 29, 2024
bc84083
test(kubernetes): Create/Delete/GetClusterRole
c3y1huang Apr 29, 2024
7146ba1
feat(kubernetes): create/delete/get ClusterRoleBinding
c3y1huang Apr 29, 2024
30c8926
test(kubernetes): Create/Delete/GetClusterRoleBinding
c3y1huang Apr 29, 2024
b1082cf
feat(kubernetes): create/delete/get ConfigMap
c3y1huang Apr 29, 2024
d37273a
test(kubernetes): Create/Delete/GetConfigMap
c3y1huang Apr 29, 2024
4f4c5c5
feat(kubernetes): create/delete/get DaemonSet
c3y1huang Apr 29, 2024
2b9d576
test(kubernetes): Create/Delete/GetDaemonSet
c3y1huang Apr 29, 2024
c3f6410
feat(kubernetes): create/delete/get ServiceAccount
c3y1huang Apr 29, 2024
d75e6f9
test(kubernetes): Create/Delete/GetServiceAccount
c3y1huang Apr 29, 2024
b3647b5
feat(kubernetes): check if DaemonSet is ready
c3y1huang Apr 29, 2024
f4f499d
test(kubernetes): IsDaemonSetReady
c3y1huang Apr 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
31 changes: 31 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,53 @@ require (
golang.org/x/sys v0.21.0
google.golang.org/grpc v1.64.0
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
k8s.io/api v0.28.5
k8s.io/apimachinery v0.28.5
k8s.io/client-go v0.28.5
k8s.io/mount-utils v0.30.2
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/moby/sys/mountinfo v0.6.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.120.1 // indirect
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
126 changes: 126 additions & 0 deletions go.sum

Large diffs are not rendered by default.

89 changes: 87 additions & 2 deletions io/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"io"
"os"
"path/filepath"
"strings"
"syscall"
"time"

Expand Down Expand Up @@ -164,12 +165,29 @@

// FindFiles searches for files in the specified directory with the given fileName.
// If fileName is empty, it retrieves all files in the directory.
// If maxDepth is greater than 0, it limits the search to the specified depth.
// It returns a slice of filePaths and an error if any.
func FindFiles(directory, fileName string) (filePaths []string, err error) {
func FindFiles(directory, fileName string, maxDepth int) (filePaths []string, err error) {
derekbit marked this conversation as resolved.
Show resolved Hide resolved
err = filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
// If the directory contains symbolic links, it might lead to a non-existing file error.
// Ignore this error and continue walking the directory.
logrus.WithError(err).Warn("Encountered error while searching for files")
return nil

Check warning on line 176 in io/file.go

View check run for this annotation

Codecov / codecov/patch

io/file.go#L175-L176

Added lines #L175 - L176 were not covered by tests
}

// Calculate the depth of the directory
depth := strings.Count(strings.TrimPrefix(path, directory), string(os.PathSeparator))

// Skip the directory if it exceeds the maximum depth
if maxDepth > 0 && depth > maxDepth {
if info.IsDir() {
return filepath.SkipDir // Skip this directory

Check warning on line 185 in io/file.go

View check run for this annotation

Codecov / codecov/patch

io/file.go#L185

Added line #L185 was not covered by tests
}

return nil // Skip this file
}

if fileName == "" || info.Name() == fileName {
filePaths = append(filePaths, path)
}
Expand Down Expand Up @@ -241,3 +259,70 @@
StorageAvailable: int64(statfs.Bfree) * statfs.Bsize,
}, nil
}

// ListOpenFiles returns a list of open files in the specified directory.
func ListOpenFiles(procDirectory, directory string) ([]string, error) {
// Check if the specified directory exists
if _, err := os.Stat(directory); err != nil {
return nil, err
}

// Get the list of all processes in the provided procDirectory
procs, err := os.ReadDir(procDirectory)
if err != nil {
return nil, err

Check warning on line 273 in io/file.go

View check run for this annotation

Codecov / codecov/patch

io/file.go#L273

Added line #L273 was not covered by tests
}

// Iterate over each process in the procDirectory
var openedFiles []string
for _, proc := range procs {
// Skip non-directory entries
if !proc.IsDir() {
continue
}

// Read the file descriptor directory for the process
pid := proc.Name()
fdDir := filepath.Join(procDirectory, pid, "fd")
files, err := os.ReadDir(fdDir)
if err != nil {
logrus.WithError(err).Tracef("Failed to read file descriptors for process %v", pid)
continue
}

// Iterate over each file in the file descriptor directory
for _, file := range files {
filePath, err := os.Readlink(filepath.Join(fdDir, file.Name()))
if err != nil {
logrus.WithError(err).Tracef("Failed to read link for file descriptor %v", file.Name())
continue
}

// Check if the file path is within the specified directory
if strings.HasPrefix(filePath, directory+"/") || filePath == directory {
openedFiles = append(openedFiles, filePath)
}
}
}

return openedFiles, nil
}

// IsDirectoryEmpty returns true if the specified directory is empty.
func IsDirectoryEmpty(directory string) (bool, error) {
f, err := os.Open(directory)
if err != nil {
return false, err
}
defer f.Close()

_, err = f.Readdirnames(1)
if err == io.EOF {
return true, nil
}
if err != nil {
return false, err

Check warning on line 324 in io/file.go

View check run for this annotation

Codecov / codecov/patch

io/file.go#L324

Added line #L324 was not covered by tests
}

return false, nil
}
174 changes: 169 additions & 5 deletions io/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,17 +341,26 @@ func (s *TestSuite) TestFindFiles(c *C) {
_ = os.RemoveAll(fakeDir)
}()

// Prepare sub directory
fakeDirSub := fake.CreateTempDirectory(fakeDir, c)

// Prepare 2 existing files in root of the fake directory,
// and 2 existing file in sub directory.
existingFileCount := 2
existingFilePaths := make(map[string]bool, 3)
existingFilePaths[fakeDir] = true
for i := 0; i < existingFileCount; i++ {
file := fake.CreateTempFile(fakeDir, fmt.Sprintf("test-%v", i), "content", c)
existingFilePaths[file.Name()] = true
_ = file.Close()
existingFilePaths[fakeDirSub] = true
for _, dir := range []string{fakeDir, fakeDirSub} {
for i := 0; i < existingFileCount; i++ {
file := fake.CreateTempFile(dir, fmt.Sprintf("test-%v", i), "content", c)
existingFilePaths[file.Name()] = true
_ = file.Close()
}
}

type testCase struct {
findFileWithName string
maxDepth int

expectedFilePaths []string
expectError bool
Expand All @@ -362,19 +371,32 @@ func (s *TestSuite) TestFindFiles(c *C) {
fakeDir,
filepath.Join(fakeDir, "test-0"),
filepath.Join(fakeDir, "test-1"),
fakeDirSub,
filepath.Join(fakeDirSub, "test-0"),
filepath.Join(fakeDirSub, "test-1"),
},
},
"FindFiles(...): find file with name": {
findFileWithName: "test-0",
expectedFilePaths: []string{
filepath.Join(fakeDir, "test-0"),
filepath.Join(fakeDirSub, "test-0"),
},
},
"FindFiles(...): max depth": {
maxDepth: 1,
expectedFilePaths: []string{
fakeDir,
fakeDirSub,
filepath.Join(fakeDir, "test-0"),
filepath.Join(fakeDir, "test-1"),
},
},
}
for testName, testCase := range testCases {
c.Logf("testing utils.%v", testName)

result, err := FindFiles(fakeDir, testCase.findFileWithName)
result, err := FindFiles(fakeDir, testCase.findFileWithName, testCase.maxDepth)
if testCase.expectError {
c.Assert(err, NotNil)
continue
Expand Down Expand Up @@ -614,3 +636,145 @@ func getDiskStat(path string) (*types.DiskStat, error) {
StorageAvailable: fsStat.FreeBlock * fsStat.BlockSize,
}, nil
}

func (s *TestSuite) TestListOpenFiles(c *C) {
fakeDir := fake.CreateTempDirectory("", c)
defer func() {
_ = os.RemoveAll(fakeDir)
}()

type _dirInfo struct {
dir string
files []*os.File
}

type _fakeDirs struct {
open _dirInfo
close _dirInfo
}

// create fake directories:
// 1. open: contains 2 opened files
// 2. close: contains 2 closed files
fakeDirs := _fakeDirs{
open: _dirInfo{
dir: fake.CreateTempDirectory(fakeDir, c),
},
close: _dirInfo{
dir: fake.CreateTempDirectory(fakeDir, c),
},
}

fakeDirs.open.files = append(fakeDirs.open.files, fake.CreateTempFile(fakeDirs.open.dir, "file1", "content", c))
fakeDirs.open.files = append(fakeDirs.open.files, fake.CreateTempFile(fakeDirs.open.dir, "file2", "content", c))
defer func() {
for _, file := range fakeDirs.open.files {
err := file.Close()
c.Assert(err, IsNil)
}
}()

// Create and close files in the close directory
fakeDirs.close.files = append(fakeDirs.close.files, fake.CreateTempFile(fakeDirs.close.dir, "file1", "content", c))
fakeDirs.close.files = append(fakeDirs.close.files, fake.CreateTempFile(fakeDirs.close.dir, "file2", "content", c))
for _, file := range fakeDirs.close.files {
err := file.Close()
c.Assert(err, IsNil)
}

time.Sleep(100 * time.Millisecond)

type testCase struct {
directory string
expectedOpenFiles []string
expectError bool
}
testCases := map[string]testCase{
"ListOpenFiles(...)": {
directory: fakeDir,
expectedOpenFiles: []string{
filepath.Join(fakeDirs.open.dir, "file1"),
filepath.Join(fakeDirs.open.dir, "file2"),
},
},
"ListOpenFiles(...): not existing path": {
directory: "not-existing-path",
expectError: true,
},
"ListOpenFiles(...): no open files": {
directory: fakeDirs.close.dir,
expectError: false,
},
}
for testName, testCase := range testCases {
c.Logf("testing utils.%v", testName)

if testCase.directory == "" {
testCase.directory = fakeDir
}

openFiles, err := ListOpenFiles("/proc", testCase.directory)
if testCase.expectError {
c.Assert(err, NotNil)
continue
}
c.Assert(err, IsNil, Commentf(test.ErrErrorFmt, testName, err))
c.Assert(len(openFiles), Equals, len(testCase.expectedOpenFiles),
Commentf(test.ErrResultFmt, fmt.Sprintf("%s: %v", testName, openFiles)),
)
for i, openFile := range openFiles {
c.Assert(openFile, Equals, testCase.expectedOpenFiles[i])
}
}
}

func (s *TestSuite) TestIsDirectoryEmpty(c *C) {
fakeDir := fake.CreateTempDirectory("", c)
defer func() {
_ = os.RemoveAll(fakeDir)
}()

type testCase struct {
isEmpty bool
isNotExist bool
expectError bool
expectResult bool
}
testCases := map[string]testCase{
"IsDirectoryEmpty(...)": {
isEmpty: true,
expectResult: true,
},
"IsDirectoryEmpty(...): not empty": {
isEmpty: false,
expectResult: false,
},
"IsDirectoryEmpty(...): not existing path": {
isNotExist: true,
expectError: true,
expectResult: false,
},
}
for testName, testCase := range testCases {
c.Logf("testing utils.%v", testName)

testDir := fake.CreateTempDirectory(fakeDir, c)

if !testCase.isEmpty {
fake.CreateTempFile(testDir, "file", "content", c)
}

if testCase.isNotExist {
err := os.RemoveAll(testDir)
c.Assert(err, IsNil)
}

result, err := IsDirectoryEmpty(testDir)
if testCase.expectError {
c.Assert(err, NotNil)
continue
}
c.Assert(err, IsNil, Commentf(test.ErrErrorFmt, testName, err))
c.Assert(result, Equals, testCase.expectResult, Commentf(test.ErrResultFmt, testName))
}
}
Loading