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

WIP: Save output to file 01 #27

Merged
merged 9 commits into from
Feb 26, 2024
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")
}
}
Loading