initial commit

This commit is contained in:
Gibheer 2020-04-21 20:12:47 +02:00
commit b5395df086
7 changed files with 490 additions and 0 deletions

43
exec/main.go Normal file
View File

@ -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)
}

67
file/content_test.go Normal file
View File

@ -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")
}
}

161
file/main.go Normal file
View File

@ -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
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.zero-knowledge.org/gibheer/ensure
go 1.14

19
main.go Normal file
View File

@ -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
}
)

101
runner.go Normal file
View File

@ -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
}

96
runner_test.go Normal file
View File

@ -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
}