initial commit
This commit is contained in:
commit
b5395df086
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue