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