Skip to content

Commit

Permalink
WIP: Save output to file 01 (#27)
Browse files Browse the repository at this point in the history
* be able to receive -l as parameter viua cli

* save-output-to-file-01

* make sure the path validation fuction deletetes any file it creates

* save-output-to-file-01; Commented out candidate way to implement that. it's failing

* save-output-to-file-01

* remove valid for linux, but invalid for mac os

* save-output-to-file-01; working first version

* add a simple test to the code fuction

* improve some tests
  • Loading branch information
aguzmans authored Feb 26, 2024
1 parent b871ce0 commit 0bbdfbf
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 0 deletions.
28 changes: 28 additions & 0 deletions cli/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import (
type ArgumentsReceived struct {
KubeconfigFile, SourceClusterContext, TargetClusterContext, NamespaceName, FiltersForObject, Include, Exclude *string
VerboseDiffs *int
FileOutput *string
Err error
}
type ArgumentsReceivedValidated struct {
KubeconfigFile, SourceClusterContext, TargetClusterContext, NamespaceName, FiltersForObject string
Include, Exclude []string
VerboseDiffs int
FileOutput string
Err error
}

Expand Down Expand Up @@ -46,6 +48,7 @@ func PaserReader() ArgumentsReceivedValidated {
Excludek8sObjects := parser.String("e", "exclude", &argparse.Options{Help: "List of kubernetes objects to include, this should be an element or a comma separated list."})
namespaceName := parser.String("n", "namespace", &argparse.Options{Help: "Namespace that needs to be copied. defaults to 'default' namespace. The option also accepts wilcard matching of namespace. E.G.: '*-pci' would match any namespace that ends with -pci. Notice that the '' might be required in some consoles like iterm"})
filtersForObject := parser.String("f", "filter", &argparse.Options{Help: "Filter what parts of the object I want to compare. must be used together with -i option to apply to that type of objects"})
fileOutput := parser.String("l", "file", &argparse.Options{Required: false, Help: "Save the output to a file. If not provided, the output will be printed to the console."})
err := parser.Parse(os.Args)
if err != nil {
// In case of error print error and print usage
Expand All @@ -60,6 +63,7 @@ func PaserReader() ArgumentsReceivedValidated {
Include: []string{""},
Exclude: []string{""},
VerboseDiffs: *verboseDiffs,
FileOutput: "",
Err: err}
}
TheArgs := ArgumentsReceived{
Expand All @@ -71,6 +75,7 @@ func PaserReader() ArgumentsReceivedValidated {
Exclude: Excludek8sObjects,
FiltersForObject: filtersForObject,
VerboseDiffs: verboseDiffs,
FileOutput: fileOutput,
Err: err}
ArgumentsReceivedValidated := ValidateParametersFromParserArgs(TheArgs)
return ArgumentsReceivedValidated
Expand Down Expand Up @@ -134,6 +139,28 @@ func ValidateParametersFromParserArgs(TheArgs ArgumentsReceived) ArgumentsReceiv
fmt.Println("The program will try to execute anyway, but the output might not be what you expect.")
fmt.Println("The -f is to be used with one and only one -i include object type at the time.")
}
file := *TheArgs.FileOutput
if file != "" {
valid, filePath, err := tools.IsValidPath(file)
if err == nil {
if valid {
fmt.Printf("The output will be saved to the file: %s\n", filePath)
}
} else {
fmt.Println(err)
}
return ArgumentsReceivedValidated{
KubeconfigFile: configFile,
SourceClusterContext: strSourceClusterContext,
TargetClusterContext: strTargetClusterContext,
NamespaceName: strNamespaceName,
FiltersForObject: *TheArgs.FiltersForObject,
Include: includeStr,
Exclude: excludeStr,
VerboseDiffs: *TheArgs.VerboseDiffs,
FileOutput: filePath,
Err: nil}
}
return ArgumentsReceivedValidated{
KubeconfigFile: configFile,
SourceClusterContext: strSourceClusterContext,
Expand All @@ -143,6 +170,7 @@ func ValidateParametersFromParserArgs(TheArgs ArgumentsReceived) ArgumentsReceiv
Include: includeStr,
Exclude: excludeStr,
VerboseDiffs: *TheArgs.VerboseDiffs,
FileOutput: "",
Err: nil}
}

Expand Down
2 changes: 2 additions & 0 deletions cli/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func TestValidateParametersFromParserArgs(t *testing.T) {
Include: stringPtr("deployment"),
Exclude: stringPtr("service"),
VerboseDiffs: intPtr(1),
FileOutput: stringPtr("output.txt"),
Err: nil,
},
expectedArgsValidated: ArgumentsReceivedValidated{
Expand All @@ -68,6 +69,7 @@ func TestValidateParametersFromParserArgs(t *testing.T) {
Include: []string{"deployment"},
Exclude: []string{"service"},
VerboseDiffs: 1,
FileOutput: "output.txt",
Err: nil,
},
},
Expand Down
32 changes: 32 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,38 @@ func main() {
err := fmt.Errorf("error parsing arguments: %v", args.Err)
panic(err)
}
if args.FileOutput != "" {
defer tools.LogOutput(args.FileOutput)()
}
// var file *os.File
// if args.FileOutput != "" {
// _, fileString, _ := tools.IsValidPath(args.FileOutput)
// var err error
// file, err = os.Create(fileString)
// if err != nil {
// fmt.Println("Error opening file:", err)
// os.Exit(1)
// }
// defer file.Close()
// }
// // Create a multi-writer to write to both stdout and the file (if specified)
// var writers []io.Writer
// writers = append(writers, os.Stdout)
// if file != nil {
// writers = append(writers, file)
// }
// multiWriter := io.MultiWriter(writers...)
// // Use a goroutine to copy output to both stdout and the file
// go func(out io.Writer) {
// for {
// // Copy output to both stdout and the file
// _, err := io.Copy(out, os.Stdin)
// if err != nil {
// fmt.Println("Error copying output:", err)
// return
// }
// }
// }(multiWriter)

// Connect to source cluster
clientsetToSource, err := connect.ConnectToSource(args.SourceClusterContext, &args.KubeconfigFile)
Expand Down
109 changes: 109 additions & 0 deletions tools/file-works.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package tools

import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"runtime"
"strings"
)

// isValidPath checks if the given path is valid for the current operating system.
func IsValidPath(path string) (isValid bool, absPath string, err error) {
// Check if the path contains invalid characters for the current OS.
var invalidChars string
switch runtime.GOOS {
case "windows":
invalidChars = `<>|?*`
default:
invalidChars = `<>:"\|?*`
}

for _, char := range invalidChars {
if strings.ContainsRune(path, char) {
return false, "", fmt.Errorf("invalid character '%c' found in the path", char)
}
}

// Check if the path is empty or consists of only whitespace characters.
if len(strings.TrimSpace(path)) == 0 {
return false, "", fmt.Errorf("empty path")
}

// Convert the path to absolute path if it's relative.
if !filepath.IsAbs(path) {
// Get the current working directory.
cwd, err := os.Getwd()
if err != nil {
// Failed to get the current working directory.
return false, "", err
}
// Join the current working directory with the relative path to get absolute path.
absPath = filepath.Join(cwd, path)
} else {
absPath = path
}

// Try to create or open the file.
file, err := os.OpenFile(absPath, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
// Check if the error is due to a missing directory in the path.
if os.IsNotExist(err) {
return false, "", fmt.Errorf("the file or one or more directories in the path do not exist or can not be created: %v", err)
}
// Check if the error is due to permission issues.
if os.IsPermission(err) {
return false, "", fmt.Errorf("no permission to write on path: %s", absPath)
}
// For other errors, return the error as is.
return false, "", err
}
// Defer the closure of the file and its removal.
defer func() {
file.Close()
os.Remove(absPath)
}()

return true, absPath, nil
}

func LogOutput(logfile string) func() {
// open file read/write | create if not exist | clear file at open if exists
f, _ := os.OpenFile(logfile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)

// save existing stdout | MultiWriter writes to saved stdout and file
out := os.Stdout
mw := io.MultiWriter(out, f)

// get pipe reader and writer | writes to pipe writer come out pipe reader
r, w, _ := os.Pipe()

// replace stdout,stderr with pipe writer | all writes to stdout, stderr will go through pipe instead (fmt.print, log)
os.Stdout = w
os.Stderr = w

// writes with log.Print should also write to mw
log.SetOutput(mw)

//create channel to control exit | will block until all copies are finished
exit := make(chan bool)

go func() {
// copy all reads from pipe to multiwriter, which writes to stdout and file
_, _ = io.Copy(mw, r)
// when r or w is closed copy will finish and true will be sent to channel
exit <- true
}()

// function to be deferred in main until program exits
return func() {
// close writer then block on exit channel | this will let mw finish writing before the program exits
_ = w.Close()
<-exit
// close file after all writes have finished
_ = f.Close()
}

}
122 changes: 122 additions & 0 deletions tools/file-works_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package tools

import (
"log"
"os"
"strings"
"testing"
)

func TestIsValidPath(t *testing.T) {
// Example usage:
paths := map[string]struct {
expectedValid bool
expectedError string
}{
"/path/to/valid/file.txt": {
expectedValid: false,
expectedError: "the file or one or more directories in the path do not exist or can not be created",
},
"invalid*file.txt": {
expectedValid: false,
expectedError: "invalid character '*' found in the path",
},
"": {
expectedValid: false,
expectedError: "empty path",
},
" ": {
expectedValid: false,
expectedError: "empty path",
},
"/path/with/invalid|character": {
expectedValid: false,
expectedError: "invalid character '|' found in the path",
},
"C:\\Program Files\\Example\\file.txt": {
expectedValid: false,
expectedError: "invalid character ':' found in the path",
},
"D:/Documents/Report.docx": {
expectedValid: false,
expectedError: "invalid character ':' found in the path",
},
"/home/user/pictures/photo.jpg": {
expectedValid: false,
expectedError: "the file or one or more directories in the path do not exist or can not be created",
},
"file.txt": {
expectedValid: true,
expectedError: "",
},
"folder1/file.txt": {
expectedValid: false,
expectedError: "the file or one or more directories in the path do not exist or can not be created",
},
"../parent/file.txt": {
expectedValid: false,
expectedError: "the file or one or more directories in the path do not exist or can not be created",
},
"..\\parent\\file.txt": {
expectedValid: false,
expectedError: "invalid character '\\' found in the path",
},
}

for path, expected := range paths {
valid, _, err := IsValidPath(path)
if err != nil {
if !strings.Contains(err.Error(), expected.expectedError) {
t.Errorf("Unexpected error for path %s. Expected: %s, Got: %s", path, expected.expectedError, err.Error())
}
} else {
if valid != expected.expectedValid {
t.Errorf("Path %s validation failed. Expected valid: %t but got: %t", path, expected.expectedValid, valid)
}
}
}
}

func TestLogOutput(t *testing.T) {
// Create a temporary file for logging
tmpfile, err := os.CreateTemp("", "testlog-")
if err != nil {
t.Fatalf("Error creating temporary file: %v", err)
}
defer os.Remove(tmpfile.Name())

// Create a channel to signal when the goroutine has completed
done := make(chan struct{})

// Call LogOutput with the temporary file
go func() {
defer close(done)
LogOutput(tmpfile.Name()) // Assuming LogOutput returns immediately after starting the goroutine
}()

// Write a log message
log.Print("Test log message")

// Wait for the goroutine to finish
<-done

// Read and verify the contents of the log file
// (You should implement this verification logic)
}

func TestLogOutputWithInvalidPath(t *testing.T) {
// Call LogOutput with an invalid path
err := LogOutput("invalid*file.txt")
if err == nil {
t.Errorf("Expected error for invalid path")
}
defer os.Remove("invalid*file.txt")
}

func TestLogOutputWithEmptyPath(t *testing.T) {
// Call LogOutput with an empty path
err := LogOutput("")
if err == nil {
t.Errorf("Expected error for empty path")
}
}

0 comments on commit 0bbdfbf

Please sign in to comment.