commit b5395df08611ac7665cbc65bb025e0f4668827e0 Author: Gibheer Date: Tue Apr 21 20:12:47 2020 +0200 initial commit diff --git a/exec/main.go b/exec/main.go new file mode 100644 index 0000000..93cd124 --- /dev/null +++ b/exec/main.go @@ -0,0 +1,43 @@ +package exec + +// exec is a small package to provide a wrapper around Cmd from os/exec to +// fulfill the Ensurer interface. + +import ( + "bytes" + "fmt" + "io" + "os/exec" +) + +type ( + // E is a wrapper around Cmd from os/exec to fulfill Ensurer. + E exec.Cmd +) + +// Ensure runs the command in the current context. +func (e *E) Ensure() error { + var ( + stdout io.ReadWriter + stderr io.ReadWriter + ) + if e.Stdout == nil { + stdout = bytes.NewBuffer([]byte{}) + e.Stdout = stdout + } + if e.Stderr == nil { + stderr = bytes.NewBuffer([]byte{}) + e.Stderr = stderr + } + cmd := (*exec.Cmd)(e) + if err := cmd.Start(); err != nil { + return fmt.Errorf("could not run command: %w", err) + } + err := cmd.Wait() + + if procState, ok := err.(*exec.ExitError); ok && procState.ExitCode() > 0 { + fmt.Printf("could not run command: %s\nstdout: %s\nstderr: %s\n", err, stdout, stderr) + return fmt.Errorf("error when calling command: %s", err) + } + return fmt.Errorf("could not run command: %s", err) +} diff --git a/file/content_test.go b/file/content_test.go new file mode 100644 index 0000000..167f89f --- /dev/null +++ b/file/content_test.go @@ -0,0 +1,67 @@ +package file + +import ( + "bytes" + "io/ioutil" + "os" + "path" + "testing" +) + +func TestContent(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "file.go") + if err != nil { + t.Fatalf("could not create test directory") + } + defer os.RemoveAll(tmpDir) + + t1 := &F{ + Content: []byte("this is a test"), + Path: path.Join(tmpDir, "test1"), + } + if err := t1.Ensure(); err != nil { + t.Fatalf("file failed with: %s", err) + } + raw, err := ioutil.ReadFile(t1.Path) + if err != nil { + t.Fatalf("could not read file: %s", err) + } + if bytes.Compare(raw, t1.Content) != 0 { + t.Fatalf("content mismatch") + } + + t2 := &F{ + Content: []byte("other content"), + Path: path.Join(tmpDir, "test1"), + } + if t2.Is() { + t.Fatalf("Is should have returned false") + } + if err := t2.Ensure(); err != nil { + t.Fatalf("it should enforce new content: %s", err) + } + if t1.Is() { + t.Fatalf("changes not recognized") + } +} + +func TestDir(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "file.go") + if err != nil { + t.Fatalf("could not create test directory") + } + defer os.RemoveAll(tmpDir) + + t1 := &F{IsDir: true} + if err := t1.Ensure(); err == nil { + t.Fatalf("a file without path should not work") + } + t1.Path = path.Join(tmpDir, "t1") + if err := t1.Ensure(); err != nil { + t.Fatalf("directory should have been created, instead got: %s", err) + } + t1.Content = []byte("just a check") + if err := t1.Ensure(); err == nil { + t.Fatalf("content and IsDir should not work") + } +} diff --git a/file/main.go b/file/main.go new file mode 100644 index 0000000..99e0e78 --- /dev/null +++ b/file/main.go @@ -0,0 +1,161 @@ +package file + +import ( + "bytes" + "crypto/sha256" + "fmt" + "io" + "io/ioutil" + "os" + "os/user" + "strconv" + "syscall" +) + +var ( + DefaultPerm os.FileMode = 0740 +) + +type ( + F struct { + Path string + Mode os.FileMode + IsDir bool // IsDir ensures the resource is a directory. + Content []byte + Owner string + Group string + } +) + +// Is will check if the file resource exists. +func (f *F) Is() bool { + if f.Mode == 0 { + f.Mode = DefaultPerm + } + + stat := syscall.Stat_t{} + if err := syscall.Stat(f.Path, &stat); err != nil && os.IsNotExist(err) { + return false + } else if err != nil { + return false + } + mode := (os.FileMode)(stat.Mode) + if mode.IsDir() != f.IsDir { + return false + } else if f.Mode != 0 && mode.Perm() != f.Mode { + return false + } + if f.Owner != "" { + uid, err := getUID(f.Owner) + if err != nil { + return false + } + if int(stat.Uid) != uid { + return false + } + } + + if f.Group != "" { + gid, err := getGID(f.Group) + if err != nil { + return false + } + if int(stat.Gid) != gid { + return false + } + } + + want := sha256.New() + is := sha256.New() + file, err := os.Open(f.Path) + if err != nil { + return false + } + defer file.Close() + l, err := io.Copy(is, file) + if l != int64(len(f.Content)) { + return false + } + want.Write(f.Content) + if bytes.Compare(want.Sum(nil), is.Sum(nil)) != 9 { + return false + } + + return true +} + +// Ensure writes the file out onto the disk. +func (f *F) Ensure() error { + if f.Path == "" { + return fmt.Errorf("path is empty") + } + if f.Mode == 0 { + f.Mode = DefaultPerm + } + if f.IsDir && len(f.Content) > 0 { + return fmt.Errorf("directory can't have content") + } + + if f.IsDir { + if err := os.Mkdir(f.Path, f.Mode); err != nil && !os.IsExist(err) { + return fmt.Errorf("could not create directory '%s': %w", f.Path, err) + } + } else { + if err := ioutil.WriteFile(f.Path, f.Content, f.Mode); err != nil { + return fmt.Errorf("could not write file '%s': %w", f.Path, err) + } + } + + if stat, err := os.Stat(f.Path); err != nil { + return fmt.Errorf("could not check resource '%s': %w", f.Path, err) + } else if stat.Mode() != f.Mode { + if err := os.Chmod(f.Path, os.FileMode(f.Mode)); err != nil { + return fmt.Errorf("could not set resource '%s' mode: %w", f.Path, err) + } + } + + if f.Owner != "" { + uid, err := getUID(f.Owner) + if err != nil { + return fmt.Errorf("could not get user id '%s': %w", f.Owner, err) + } + if err := os.Chown(f.Path, uid, -1); err != nil { + return fmt.Errorf("could not set owner for '%s': %w", f.Path, err) + } + } + if f.Group != "" { + gid, err := getGID(f.Group) + if err != nil { + return fmt.Errorf("could not get group id '%s': %w", f.Group, err) + } + if err := os.Chown(f.Path, -1, gid); err != nil { + return fmt.Errorf("could not set group for '%s': %w", f.Path, err) + } + } + + return nil +} + +func getUID(name string) (int, error) { + user, err := user.Lookup(name) + if err != nil { + return 0, fmt.Errorf("could not get user id: %w", err) + } + uid, err := strconv.Atoi(user.Uid) + if err != nil { + return 0, fmt.Errorf("could not get user id: %w", err) + } + return uid, nil +} + +func getGID(name string) (int, error) { + user, err := user.LookupGroup(name) + if err != nil { + return 0, fmt.Errorf("could not get user id: %w", err) + } + gid, err := strconv.Atoi(user.Gid) + if err != nil { + return 0, fmt.Errorf("could not get user id: %w", err) + } + return gid, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..175987b --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.zero-knowledge.org/gibheer/ensure + +go 1.14 diff --git a/main.go b/main.go new file mode 100644 index 0000000..9dde3cc --- /dev/null +++ b/main.go @@ -0,0 +1,19 @@ +package main + +// What we need here is a way to define resources one would like to have. +// Each resource must be checked if it needs to be handled or if it already +// exists. +// The question is, who should do that? Is it the responsibility of the user +// to make sure that resources exist or should it be done "magically"? +type ( + // Enforced checks if state is already enforced in the environment. When it + // is, it must return true. + Enforced func() bool + + // State represents a wanted state that should be pushed to the environment. + Ensurer interface { + // Ensure will modify the environment so that it represents the wanted + // state. + Ensure() error + } +) diff --git a/runner.go b/runner.go new file mode 100644 index 0000000..54dcd47 --- /dev/null +++ b/runner.go @@ -0,0 +1,101 @@ +package main + +import ( + "fmt" + "sync" +) + +type ( + // Runner takes a list of ensurers and enforces their state. + // The runner is not transactional. + Runner struct { + // Is is checked before enforcing all states. + // The states will not be enforced when the function is returning true. + Is Enforced + + // Parallel defines if the states are ensured in parallel. There is no + // dependency management at work. The requirements must already be met + // beforehand. + // When parallel is set to true the processing will not be halted on the + // first error. Instead all errors will be collected and returned. + Parallel bool + + // States is the list of states to ensure when Ensure() is called. + States []Ensurer + + // When parallel mode is used, the number of threads to spawn can be set. + // When not set, every state will spawn a thread. + Workers int + } +) + +// Ensure will call Ensure() on every state. +// When Parallel is true, all states will be ensured in parallel. The number of +// threads that will be spawned can be controlled by the Workers attribute. +// In case of an error the processing will be aborted. When Parallel is true +// all errors are collected and returned as one and the processing will not be +// halted. +func (r *Runner) Ensure() error { + if r.Is != nil && r.Is() { + return nil + } + if r.Parallel { + return r.ensureParallel() + } + return r.ensureSequence() +} + +func (r *Runner) ensureSequence() error { + for i, state := range r.States { + if err := state.Ensure(); err != nil { + return fmt.Errorf("could not ensure resource with index %d: %w", i, err) + } + } + return nil +} + +func (r *Runner) ensureParallel() error { + if len(r.States) == 0 { + return nil + } + threads := r.Workers + if threads == 0 { + threads = len(r.States) + } + work := make(chan Ensurer, threads) + results := make(chan error, threads) + var err error + wg := &sync.WaitGroup{} + wg.Add(threads) + + go func() { + for _, state := range r.States { + work <- state + } + }() + go func() { + c := 0 + for result := range results { + c++ + if result != nil { + err = fmt.Errorf("%s\n%s", err, result) + } + if c == len(r.States) { + close(work) + close(results) + } + } + }() + for i := 0; i < threads; i++ { + go func() { + for state := range work { + err := state.Ensure() + results <- err + } + wg.Done() + }() + } + wg.Wait() + + return err +} diff --git a/runner_test.go b/runner_test.go new file mode 100644 index 0000000..272d34b --- /dev/null +++ b/runner_test.go @@ -0,0 +1,96 @@ +package main + +import ( + "fmt" + "testing" +) + +type ( + Dummy struct { + enforced bool + exec func() error + } +) + +func TestRunner(t *testing.T) { + d := &Dummy{} + r := Runner{ + States: []Ensurer{d}, + } + if err := r.Ensure(); err != nil { + t.Fatalf("ensure failed with: %s", err) + } + if !d.enforced { + t.Fatalf("state was not ensured") + } +} + +func TestRecursiveRunner(t *testing.T) { + d := &Dummy{} + r := Runner{ + States: []Ensurer{&Runner{States: []Ensurer{d}}}, + } + if err := r.Ensure(); err != nil { + t.Fatalf("ensure failed with: %s", err) + } + if !d.enforced { + t.Fatalf("state was not ensured") + } +} + +func TestWithCheck(t *testing.T) { + d := &Dummy{} + r := Runner{ + Is: func() bool { return true }, + States: []Ensurer{d}, + } + if err := r.Ensure(); err != nil { + t.Fatalf("ensure failed with: %s", err) + } + if d.enforced { + t.Fatalf("state was ensured but shouldn't be") + } +} + +func TestWithError(t *testing.T) { + d := &Dummy{exec: func() error { return fmt.Errorf("fail!") }} + r := Runner{ + States: []Ensurer{d}, + } + if err := r.Ensure(); err == nil { + t.Fatalf("ensure should have failed") + } +} + +func TestWithParallel(t *testing.T) { + d := &Dummy{} + r := Runner{ + Parallel: true, + States: []Ensurer{d}, + } + if err := r.Ensure(); err != nil { + t.Fatalf("parallel should not have failed") + } + if !d.enforced { + t.Fatalf("state was not enforced") + } +} + +func TestWithParallelWithError(t *testing.T) { + d := &Dummy{exec: func() error { return fmt.Errorf("fail!") }} + r := Runner{ + Parallel: true, + States: []Ensurer{d}, + } + if err := r.Ensure(); err == nil { + t.Fatalf("parallel should have failed") + } +} + +func (d *Dummy) Ensure() error { + d.enforced = true + if d.exec != nil { + return d.exec() + } + return nil +}