diff --git a/Makefile b/Makefile index 28120a7..de24c25 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,9 @@ VERSION=0.0.7 -GITCOMMIT?=$(shell git describe --dirty --always) -LDFLAGS=-ldflags "-w -s -X main.version=${VERSION} -X main.commit=${GITCOMMIT}" +LDFLAGS=-ldflags "-w -s -X main.version=${VERSION}" all: check-diff -.PHONY: check-diff +.PHONY: check-diff lint check linux check-diff: *.go go build $(LDFLAGS) -o check-diff diff --git a/check.go b/check.go new file mode 100644 index 0000000..45011e3 --- /dev/null +++ b/check.go @@ -0,0 +1,97 @@ +package main + +import ( + "bytes" + "crypto/sha256" + "fmt" + "os" + "os/exec" + "os/user" + "path/filepath" + "strings" + + "github.com/mackerelio/checkers" + "github.com/monitoring-forge/saferio" +) + +func (opt *Opt) cmd(file *os.File) error { + cmd := exec.Command(opt.Command, opt.Args...) + var stderr bytes.Buffer + cmd.Stdout = file + cmd.Stderr = &stderr + if err := cmd.Start(); err != nil { + return err + } + err := cmd.Wait() + if err != nil { + return fmt.Errorf("%w - %s", err, stderr.String()) + } + return nil +} + +func (opt *Opt) check() *checkers.Checker { + + hasher := sha256.New() + fmt.Fprintf(hasher, "%s\x00%s\x00%s", opt.Identifier, opt.Command, strings.Join(opt.Args, "\x00")) + + curUser, err := user.Current() + if err != nil { + return checkers.Critical(err.Error()) + } + + prevFileName := fmt.Sprintf("check-diff-%s-%x", curUser.Uid, hasher.Sum(nil)) + newFile, err := os.CreateTemp(opt.Workdir, "check-diff-") + if err != nil { + return checkers.Critical(err.Error()) + } + + defer func() { + _ = newFile.Close() + _ = os.Remove(newFile.Name()) + }() + + err = opt.cmd(newFile) + if err != nil { + return checkers.Critical(err.Error()) + } + + err = newFile.Close() + if err != nil { + return checkers.Critical(err.Error()) + } + + if !saferio.FileExists(opt.Workdir, prevFileName) { + err = os.Rename(newFile.Name(), filepath.Join(opt.Workdir, prevFileName)) + if err != nil { + return checkers.Critical(err.Error()) + } + if len(opt.Args) > 0 { + return checkers.Ok(fmt.Sprintf("first time execution command: '%s %s'", opt.Command, strings.Join(opt.Args, " "))) + } + return checkers.Ok(fmt.Sprintf("first time execution command: '%s'", opt.Command)) + } + + diff, err := diff(filepath.Join(opt.Workdir, prevFileName), newFile.Name()) + if err != nil { + return checkers.Critical(err.Error()) + } + + err = os.Rename(newFile.Name(), filepath.Join(opt.Workdir, prevFileName)) + if err != nil { + return checkers.Critical(err.Error()) + } + + if diff == "" { + msg, err := buildNoDifferenceMsg(filepath.Join(opt.Workdir, prevFileName)) + if err != nil { + return checkers.Critical(err.Error()) + } + return checkers.Ok(msg) + } + + diffMsg := buildDiffMsg(diff) + if opt.Warn { + return checkers.Warning(diffMsg) + } + return checkers.Critical(diffMsg) +} diff --git a/main_test.go b/check_test.go similarity index 92% rename from main_test.go rename to check_test.go index 220f1bb..86751c0 100644 --- a/main_test.go +++ b/check_test.go @@ -84,10 +84,10 @@ func createTempFile(t *testing.T, content string) (*os.File, func()) { } } -func TestRun(t *testing.T) { +func TestCheck(t *testing.T) { opt, file := getCmdOpt(t, "echo", []string{"Hello, World!"}) defer file.Close() - ckr := opt.run() + ckr := opt.check() if ckr.Status != checkers.OK { t.Errorf("Expected OK status, got %v", ckr.Status) } @@ -95,8 +95,8 @@ func TestRun(t *testing.T) { t.Errorf("Expected first time execution message, got %q", ckr.Message) } - // Run again to check for no difference - ckr = opt.run() + // Check again to check for no difference + ckr = opt.check() if ckr.Status != checkers.OK { t.Errorf("Expected OK status, got %v", ckr.Status) } @@ -105,16 +105,16 @@ func TestRun(t *testing.T) { } } -func TestRunWithDifference(t *testing.T) { +func TestCheckWithDifference(t *testing.T) { opt, file := getCmdOpt(t, "date", []string{}) defer file.Close() - // First run to create the initial state - opt.run() + // First check to create the initial state + opt.check() // Change the command to produce a different output time.Sleep(2 * time.Second) // Ensure the date command produces a different output opt.Command = "date" - ckr := opt.run() + ckr := opt.check() if ckr.Status != checkers.CRITICAL { t.Errorf("Expected CRITICAL status, got %v", ckr.Status) } diff --git a/diff.go b/diff.go index 0422ee9..14bdba6 100644 --- a/diff.go +++ b/diff.go @@ -2,15 +2,17 @@ package main import ( "bufio" + "errors" "fmt" "io" "strings" "github.com/cubicdaiya/gonp" + "github.com/monitoring-forge/saferio" ) func getLines(filename string) ([]string, error) { - file, err := openRD(filename) + file, err := saferio.OpenRD(filename) if err != nil { return nil, err } @@ -29,23 +31,23 @@ func getLines(filename string) ([]string, error) { } func diff(prev, current string) (string, error) { - prevLines, err := getLines(prev) - if err != nil { - return "", err - } - newLines, err := getLines(current) - if err != nil { - return "", err - } + prevLines, err := getLines(prev) + if err != nil { + return "", err + } + newLines, err := getLines(current) + if err != nil { + return "", err + } - d := gonp.New(prevLines, newLines) - d.Compose() + d := gonp.New(prevLines, newLines) + d.Compose() - return d.SprintUniHunks(d.UnifiedHunks()), nil + return d.SprintUniHunks(d.UnifiedHunks()), nil } func buildNoDifferenceMsg(filename string) (string, error) { - file, err := openRD(filename) + file, err := saferio.OpenRD(filename) if err != nil { return "", err } @@ -57,7 +59,7 @@ func buildNoDifferenceMsg(filename string) (string, error) { } b := make([]byte, 128) count, err := file.Read(b) - if err != nil && err != io.EOF { + if err != nil && !errors.Is(err, io.EOF) { return "", err } o := string(strings.TrimRight(string(b[0:count]), "\r\n")) diff --git a/go.mod b/go.mod index 4493079..fe51142 100644 --- a/go.mod +++ b/go.mod @@ -11,5 +11,7 @@ require github.com/cubicdaiya/gonp v1.0.4 require ( github.com/mackerelio/golib v1.2.2 + github.com/monitoring-forge/flagrun v0.0.7 + github.com/monitoring-forge/saferio v0.0.2 golang.org/x/sys v0.47.0 // indirect ) diff --git a/go.sum b/go.sum index 3ff45c5..e3872e2 100644 --- a/go.sum +++ b/go.sum @@ -6,5 +6,9 @@ github.com/mackerelio/checkers v0.2.1 h1:vRXzAT+CEyaLjSrHVxKUBjyWq1kfo8QcI8ZoSKZ github.com/mackerelio/checkers v0.2.1/go.mod h1:CW3k/5bvHhxDrfKgWvMvNH0R51zco141ZVxlI7o/KAc= github.com/mackerelio/golib v1.2.2 h1:fQfW1o2KvPyIsAH/7+M2J0gpf2jLu+NyYpDCN79zxW4= github.com/mackerelio/golib v1.2.2/go.mod h1:itcSOfrsT5XvkzflFPebT43RrliaKDz2T6YGYt3jclw= +github.com/monitoring-forge/flagrun v0.0.7 h1:6UpM8waZzN0KiZJJ2xHP64TncdmMKHE6W2HBxJwhAZA= +github.com/monitoring-forge/flagrun v0.0.7/go.mod h1:yAwxGIEx81wU4JOMp6hz7SdSYWgT/Hr4JXEdHlEn1mw= +github.com/monitoring-forge/saferio v0.0.2 h1:DJ4D4e7tBuMze4Ez7N7l7pGPAJ/oFsC0mwh+dg5UkwE= +github.com/monitoring-forge/saferio v0.0.2/go.mod h1:N2nfAMrgYrax2CdwKIIWIjy5tIrlQFtJJWzKess30KY= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/main.go b/main.go index d13fc97..a4dfe7c 100644 --- a/main.go +++ b/main.go @@ -1,30 +1,14 @@ package main import ( - "bytes" - "crypto/sha256" - "fmt" "os" - "os/exec" - "os/user" - "path/filepath" - "runtime" - "strings" - "github.com/jessevdk/go-flags" "github.com/mackerelio/checkers" "github.com/mackerelio/golib/pluginutil" + "github.com/monitoring-forge/flagrun" ) var version string -var commit string - -const ( - OK = iota - WARNING - CRITICAL - UNKNOWN -) type Opt struct { Args []string @@ -35,125 +19,7 @@ type Opt struct { Version bool `short:"v" long:"version" description:"Show version"` } -func (opt *Opt) cmd(file *os.File) error { - cmd := exec.Command(opt.Command, opt.Args...) - var stderr bytes.Buffer - cmd.Stdout = file - cmd.Stderr = &stderr - if err := cmd.Start(); err != nil { - return err - } - err := cmd.Wait() - if err != nil { - return fmt.Errorf("%w - %s", err, stderr.String()) - } - return nil -} - -func (opt *Opt) run() *checkers.Checker { - - identifier := strings.Join([]string{ - opt.Identifier, - opt.Command, - strings.Join(opt.Args, "-"), - }, "-") - hasher := sha256.New() - hasher.Write([]byte(identifier)) - - curUser, err := user.Current() - if err != nil { - return checkers.Critical(err.Error()) - } - - prevPath := filepath.Join(opt.Workdir, fmt.Sprintf("check-diff-%s-%x", curUser.Uid, hasher.Sum(nil))) - newFile, err := os.CreateTemp(opt.Workdir, "check-diff-") - if err != nil { - return checkers.Critical(err.Error()) - } - - err = opt.cmd(newFile) - if err != nil { - newFile.Close() - _ = os.Remove(newFile.Name()) - return checkers.Critical(err.Error()) - } - - err = newFile.Close() - if err != nil { - return checkers.Critical(err.Error()) - } - - if !fileExists(prevPath) { - err = os.Rename(newFile.Name(), prevPath) - if err != nil { - return checkers.Critical(err.Error()) - } - if len(opt.Args) > 0 { - return checkers.Ok(fmt.Sprintf("first time execution command: '%s %s'", opt.Command, strings.Join(opt.Args, " "))) - } - return checkers.Ok(fmt.Sprintf("first time execution command: '%s'", opt.Command)) - } - - diff, err := diff(prevPath, newFile.Name()) - if err != nil { - return checkers.Critical(err.Error()) - } - - err = os.Rename(newFile.Name(), prevPath) - if err != nil { - return checkers.Critical(err.Error()) - } - - if diff == "" { - msg, err := buildNoDifferenceMsg(prevPath) - if err != nil { - return checkers.Critical(err.Error()) - } - return checkers.Ok(msg) - } - - diffMsg := buildDiffMsg(diff) - if opt.Warn { - return checkers.Warning(diffMsg) - } - return checkers.Critical(diffMsg) -} - -func fileExists(filename string) bool { - _, err := os.Stat(filename) - return err == nil -} - -func main() { - opt := &Opt{} - psr := flags.NewParser(opt, flags.HelpFlag|flags.PassDoubleDash) - psr.Usage = "[OPTIONS] -- command args1 args2" - args, err := psr.Parse() - if opt.Version { - if commit == "" { - commit = "dev" - } - fmt.Printf( - "%s-%s\n%s/%s, %s, %s\n", - filepath.Base(os.Args[0]), - version, - runtime.GOOS, - runtime.GOARCH, - runtime.Version(), - commit) - os.Exit(OK) - } else if flags.WroteHelp(err) { - fmt.Fprintf(os.Stdout, "%v\n", err) - os.Exit(OK) - } else if err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) - os.Exit(UNKNOWN) - } else if len(args) == 0 { - fmt.Fprintf(os.Stderr, "command is required\n") - psr.WriteHelp(os.Stderr) - os.Exit(UNKNOWN) - } - +func (opt *Opt) Run(args []string) *checkers.Checker { opt.Args = []string{} opt.Command = args[0] if len(args) > 1 { @@ -164,7 +30,9 @@ func main() { opt.Workdir = pluginutil.PluginWorkDir() } - ckr := opt.run() - ckr.Name = "check-diff" - ckr.Exit() + return opt.check() +} + +func main() { + os.Exit(flagrun.Check(&Opt{}, flagrun.Version(version), flagrun.ArgsRequired())) } diff --git a/open_unix.go b/open_unix.go deleted file mode 100644 index d4ff433..0000000 --- a/open_unix.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build !windows -// +build !windows - -package main - -import ( - "os" - "syscall" -) - -// openRD opens a file for reading with O_NOFOLLOW flag to prevent symlink attacks. -// This function is specific to Unix-like systems where syscall.O_NOFOLLOW is available. -func openRD(filename string) (*os.File, error) { - return os.OpenFile(filename, os.O_RDONLY|syscall.O_NOFOLLOW, 0) -} diff --git a/open_windows.go b/open_windows.go deleted file mode 100644 index 1f6d94d..0000000 --- a/open_windows.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build windows -// +build windows - -package main - -import "os" - -// openRD opens a file for reading with appropriate flags for Windows. -// Since Windows doesn't support O_NOFOLLOW, we simply open the file with O_RDONLY. -func openRD(filename string) (*os.File, error) { - return os.OpenFile(filename, os.O_RDONLY, 0) -}