engine: resources: Add a Purge option to the file resource
This adds a "purge" parameter to the file resource. To do this, we have to add the API hooks so the file resource can query other resources in the graph to know if they are present, and as a result whether they should be excluded from the purge or not. This is useful for when we have a managed directory with some managed contents. If a managed file is removed from the directory, then it will be removed by the file (directory) resource if it has Purge set. Alternatively, you can use the Reverse meta param, which is sometimes preferable for this use case and sometimes not. This will be discussed elsewhere. This also adds a bunch of tests for this feature. This also makes a few somewhat related cleanups in the file code.
This commit is contained in:
@@ -116,6 +116,12 @@ The force property is required if we want the file resource to be able to change
|
||||
a file into a directory or vice-versa. If such a change is needed, but the force
|
||||
property is not set to `true`, then this file resource will error.
|
||||
|
||||
### Purge
|
||||
|
||||
The purge property is used when this file represents a directory, and we'd like
|
||||
to remove any unmanaged files from within it. Please note that any unmanaged
|
||||
files in a directory with this flag set will be irreversibly deleted.
|
||||
|
||||
## Group
|
||||
|
||||
The group resource manages the system groups from `/etc/group`.
|
||||
|
||||
@@ -61,6 +61,7 @@ const (
|
||||
type FileRes struct {
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Edgeable
|
||||
traits.GraphQueryable // allow others to query this res in the res graph
|
||||
//traits.Groupable // TODO: implement this
|
||||
traits.Recvable
|
||||
traits.Reversible
|
||||
@@ -86,7 +87,20 @@ type FileRes struct {
|
||||
// parameters.
|
||||
Content *string `lang:"content" yaml:"content"`
|
||||
// Source specifies the source contents for the file resource. It cannot
|
||||
// be combined with the Content or Fragments parameters.
|
||||
// be combined with the Content or Fragments parameters. It must be an
|
||||
// absolute path, and it can point to a file or a directory. If it
|
||||
// points to a file, then that will will be copied throuh directly. If
|
||||
// it points to a directory, then it will copy the directory "rsync
|
||||
// style" onto the file destination. As a result, if this is a file,
|
||||
// then the main file res must be a file, and if it is a directory, then
|
||||
// this must be a directory. To meaningfully copy a full directory, you
|
||||
// also need to specify the Recurse parameter, which is currently
|
||||
// required. If you want an existing dir to be turned into a file (or
|
||||
// vice-versa) instead of erroring, then you'll also need to specify the
|
||||
// Force parameter. If source is undefined and the file path is a
|
||||
// directory, then a directory will be created. If left undefined, and
|
||||
// combined with the Purge option too, then any unmanaged file in this
|
||||
// dir will be removed.
|
||||
Source string `lang:"source" yaml:"source"`
|
||||
// Fragments specifies that the file is built from a list of individual
|
||||
// files. If one of the files is a directory, then the list of files in
|
||||
@@ -114,6 +128,11 @@ type FileRes struct {
|
||||
Mode string `lang:"mode" yaml:"mode"`
|
||||
Recurse bool `lang:"recurse" yaml:"recurse"`
|
||||
Force bool `lang:"force" yaml:"force"`
|
||||
// Purge specifies that when true, any unmanaged file in this file
|
||||
// directory will be removed. As a result, this file resource must be a
|
||||
// directory. This isn't particularly meaningful if you don't also set
|
||||
// Recurse to true. This doesn't work with Content or Fragments.
|
||||
Purge bool `lang:"purge" yaml:"purge"`
|
||||
|
||||
sha256sum string
|
||||
}
|
||||
@@ -198,10 +217,29 @@ func (obj *FileRes) Validate() error {
|
||||
return fmt.Errorf("can't specify file Content, Source, or Fragments when State is %s", FileStateAbsent)
|
||||
}
|
||||
|
||||
// The path and Source must either both be dirs or both not be.
|
||||
srcIsDir := strings.HasSuffix(obj.Source, "/")
|
||||
if isSrc && (obj.isDir() != srcIsDir) {
|
||||
return fmt.Errorf("the path and Source must either both be dirs or both not be")
|
||||
}
|
||||
|
||||
if obj.isDir() && (isContent || isFrag) { // makes no sense
|
||||
return fmt.Errorf("can't specify Content or Fragments when creating a Dir")
|
||||
}
|
||||
|
||||
// TODO: is this really a requirement that we want to enforce?
|
||||
if isSrc && obj.isDir() && srcIsDir && !obj.Recurse {
|
||||
return fmt.Errorf("you'll want to Recurse when you have a Source dir to copy")
|
||||
}
|
||||
// TODO: do we want to enforce this sort of thing?
|
||||
if obj.Purge && !obj.Recurse {
|
||||
return fmt.Errorf("you'll want to Recurse when you have a Purge to do")
|
||||
}
|
||||
|
||||
if isSrc && !obj.isDir() && !srcIsDir && obj.Recurse {
|
||||
return fmt.Errorf("you can't recurse when copying a single file")
|
||||
}
|
||||
|
||||
for _, frag := range obj.Fragments {
|
||||
// absolute paths begin with a slash
|
||||
if !strings.HasPrefix(frag, "/") {
|
||||
@@ -209,6 +247,14 @@ func (obj *FileRes) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if obj.Purge && (isContent || isFrag) {
|
||||
return fmt.Errorf("can't combine Purge with Content or Fragments")
|
||||
}
|
||||
// XXX: should this work with obj.Purge && obj.Source != "" or not?
|
||||
//if obj.Purge && obj.Source != "" {
|
||||
// return fmt.Errorf("can't Purge when Source is specified")
|
||||
//}
|
||||
|
||||
// TODO: should we silently ignore these errors or include them?
|
||||
//if obj.State == FileStateAbsent && obj.Owner != "" {
|
||||
// return fmt.Errorf("can't specify Owner for an absent file")
|
||||
@@ -244,11 +290,6 @@ func (obj *FileRes) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: should this specify that we create an empty directory instead?
|
||||
//if obj.Source == "" && obj.isDir() {
|
||||
// return fmt.Errorf("can't specify an empty source when creating a Dir.")
|
||||
//}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -617,11 +658,14 @@ func (obj *FileRes) dirCheckApply(apply bool) (bool, error) {
|
||||
// syncCheckApply is the CheckApply operation for a source and destination dir.
|
||||
// It is recursive and can create directories directly, and files via the usual
|
||||
// fileCheckApply method. It returns checkOK and error as is normally expected.
|
||||
func (obj *FileRes) syncCheckApply(apply bool, src, dst string) (bool, error) {
|
||||
// If excludes is specified, none of those files there will be deleted by this,
|
||||
// with the exception that a sync *can* convert a file to a dir, or vice-versa.
|
||||
func (obj *FileRes) syncCheckApply(apply bool, src, dst string, excludes []string) (bool, error) {
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("syncCheckApply: %s -> %s", src, dst)
|
||||
}
|
||||
if src == "" || dst == "" {
|
||||
// an src of "" is now supported, if dst is a dir
|
||||
if dst == "" {
|
||||
return false, fmt.Errorf("the src and dst must not be empty")
|
||||
}
|
||||
|
||||
@@ -631,11 +675,14 @@ func (obj *FileRes) syncCheckApply(apply bool, src, dst string) (bool, error) {
|
||||
srcIsDir := strings.HasSuffix(src, "/")
|
||||
dstIsDir := strings.HasSuffix(dst, "/")
|
||||
|
||||
if srcIsDir != dstIsDir {
|
||||
if srcIsDir != dstIsDir && src != "" {
|
||||
return false, fmt.Errorf("the src and dst must be both either files or directories")
|
||||
}
|
||||
if src == "" && !dstIsDir {
|
||||
return false, fmt.Errorf("dst must be a dir if we have an empty src")
|
||||
}
|
||||
|
||||
if !srcIsDir && !dstIsDir {
|
||||
if !srcIsDir && !dstIsDir && src != "" {
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("syncCheckApply: %s -> %s", src, dst)
|
||||
}
|
||||
@@ -656,18 +703,23 @@ func (obj *FileRes) syncCheckApply(apply bool, src, dst string) (bool, error) {
|
||||
}
|
||||
|
||||
// else: if srcIsDir && dstIsDir
|
||||
srcFiles, err := ReadDir(src) // if src does not exist...
|
||||
if err != nil && !os.IsNotExist(err) { // an empty map comes out below!
|
||||
return false, err
|
||||
|
||||
smartSrc := make(map[string]FileInfo)
|
||||
if src != "" {
|
||||
srcFiles, err := ReadDir(src) // if src does not exist...
|
||||
if err != nil && !os.IsNotExist(err) { // an empty map comes out below!
|
||||
return false, err
|
||||
}
|
||||
smartSrc = mapPaths(srcFiles)
|
||||
obj.init.Logf("syncCheckApply: srcFiles: %v", srcFiles)
|
||||
}
|
||||
|
||||
dstFiles, err := ReadDir(dst)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return false, err
|
||||
}
|
||||
//obj.init.Logf("syncCheckApply: srcFiles: %v", srcFiles)
|
||||
//obj.init.Logf("syncCheckApply: dstFiles: %v", dstFiles)
|
||||
smartSrc := mapPaths(srcFiles)
|
||||
smartDst := mapPaths(dstFiles)
|
||||
obj.init.Logf("syncCheckApply: dstFiles: %v", dstFiles)
|
||||
|
||||
for relPath, fileInfo := range smartSrc {
|
||||
absSrc := fileInfo.AbsPath // absolute path
|
||||
@@ -713,7 +765,7 @@ func (obj *FileRes) syncCheckApply(apply bool, src, dst string) (bool, error) {
|
||||
obj.init.Logf("syncCheckApply: recurse: %s -> %s", absSrc, absDst)
|
||||
}
|
||||
if obj.Recurse {
|
||||
if c, err := obj.syncCheckApply(apply, absSrc, absDst); err != nil { // recurse
|
||||
if c, err := obj.syncCheckApply(apply, absSrc, absDst, excludes); err != nil { // recurse
|
||||
return false, errwrap.Wrapf(err, "syncCheckApply: recurse failed")
|
||||
} else if !c { // don't let subsequent passes make this true
|
||||
checkOK = false
|
||||
@@ -728,6 +780,19 @@ func (obj *FileRes) syncCheckApply(apply bool, src, dst string) (bool, error) {
|
||||
if !apply && len(smartDst) > 0 { // we know there are files to remove!
|
||||
return false, nil // so just exit now
|
||||
}
|
||||
|
||||
// isExcluded specifies if the path is part of an excluded path. For
|
||||
// example, if we exclude /tmp/foo/bar from deletion, then we don't want
|
||||
// to delete /tmp/foo/bar *or* /tmp/foo/ *or* /tmp/ b/c they're parents.
|
||||
isExcluded := func(p string) bool {
|
||||
for _, x := range excludes {
|
||||
if util.HasPathPrefix(x, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// any files that now remain in smartDst need to be removed...
|
||||
for relPath, fileInfo := range smartDst {
|
||||
absSrc := src + relPath // absolute dest (should not exist!)
|
||||
@@ -743,6 +808,9 @@ func (obj *FileRes) syncCheckApply(apply bool, src, dst string) (bool, error) {
|
||||
// think the symmetry is more elegant and correct here for now
|
||||
// Avoiding this is also useful if we had a recurse limit arg!
|
||||
if true { // switch
|
||||
if isExcluded(absDst) { // skip removing excluded files
|
||||
continue
|
||||
}
|
||||
obj.init.Logf("syncCheckApply: removing: %s", absCleanDst)
|
||||
if apply {
|
||||
if err := os.RemoveAll(absCleanDst); err != nil { // dangerous ;)
|
||||
@@ -754,11 +822,14 @@ func (obj *FileRes) syncCheckApply(apply bool, src, dst string) (bool, error) {
|
||||
}
|
||||
_ = absSrc
|
||||
//obj.init.Logf("syncCheckApply: recurse rm: %s -> %s", absSrc, absDst)
|
||||
//if c, err := obj.syncCheckApply(apply, absSrc, absDst); err != nil {
|
||||
//if c, err := obj.syncCheckApply(apply, absSrc, absDst, excludes); err != nil {
|
||||
// return false, errwrap.Wrapf(err, "syncCheckApply: recurse rm failed")
|
||||
//} else if !c { // don't let subsequent passes make this true
|
||||
// checkOK = false
|
||||
//}
|
||||
//if isExcluded(absDst) { // skip removing excluded files
|
||||
// continue
|
||||
//}
|
||||
//obj.init.Logf("syncCheckApply: removing: %s", absCleanDst)
|
||||
//if apply { // safety
|
||||
// if err := os.Remove(absCleanDst); err != nil {
|
||||
@@ -869,11 +940,48 @@ func (obj *FileRes) sourceCheckApply(apply bool) (bool, error) {
|
||||
obj.init.Logf("sourceCheckApply(%t)", apply)
|
||||
|
||||
// source is not defined, leave it alone...
|
||||
if obj.Source == "" {
|
||||
if obj.Source == "" && !obj.Purge {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
checkOK, err := obj.syncCheckApply(apply, obj.Source, obj.getPath())
|
||||
excludes := []string{}
|
||||
|
||||
// If we're running a purge, do it here.
|
||||
if obj.Purge {
|
||||
graph, err := obj.init.FilteredGraph()
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "can't read filtered graph")
|
||||
}
|
||||
for _, vertex := range graph.Vertices() {
|
||||
res, ok := vertex.(engine.Res)
|
||||
if !ok {
|
||||
// programming error
|
||||
return false, fmt.Errorf("not a Res")
|
||||
}
|
||||
if res.Kind() != "file" {
|
||||
continue // only interested in files
|
||||
}
|
||||
if res.Name() == obj.Name() {
|
||||
continue // skip me!
|
||||
}
|
||||
fileRes, ok := res.(*FileRes)
|
||||
if !ok {
|
||||
// programming error
|
||||
return false, fmt.Errorf("not a FileRes")
|
||||
}
|
||||
p := fileRes.getPath() // if others use it, make public!
|
||||
if !util.HasPathPrefix(p, obj.getPath()) {
|
||||
continue
|
||||
}
|
||||
excludes = append(excludes, p)
|
||||
}
|
||||
}
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("syncCheckApply: excludes: %+v", excludes)
|
||||
}
|
||||
|
||||
// XXX: should this work with obj.Purge && obj.Source != "" or not?
|
||||
checkOK, err := obj.syncCheckApply(apply, obj.Source, obj.getPath(), excludes)
|
||||
if err != nil {
|
||||
obj.init.Logf("syncCheckApply: error: %v", err)
|
||||
return false, err
|
||||
@@ -1144,6 +1252,9 @@ func (obj *FileRes) Cmp(r engine.Res) error {
|
||||
if obj.Force != res.Force {
|
||||
return fmt.Errorf("the Force option differs")
|
||||
}
|
||||
if obj.Purge != res.Purge {
|
||||
return fmt.Errorf("the Purge option differs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1334,6 +1445,7 @@ func (obj *FileRes) Copy() engine.CopyableRes {
|
||||
Mode: obj.Mode,
|
||||
Recurse: obj.Recurse,
|
||||
Force: obj.Force,
|
||||
Purge: obj.Purge,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1446,6 +1558,18 @@ func (obj *FileRes) Reversed() (engine.ReversibleRes, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GraphQueryAllowed returns nil if you're allowed to query the graph. This
|
||||
// function accepts information about the requesting resource so we can
|
||||
// determine the access with some form of fine-grained control.
|
||||
func (obj *FileRes) GraphQueryAllowed(opts ...engine.GraphQueryableOption) error {
|
||||
options := &engine.GraphQueryableOptions{} // default options
|
||||
options.Apply(opts...) // apply the options
|
||||
if options.Kind != "file" {
|
||||
return fmt.Errorf("only other files can access my information")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// smartPath adds a trailing slash to the path if it is a directory.
|
||||
func smartPath(fileInfo os.FileInfo) string {
|
||||
smartPath := fileInfo.Name() // absolute path
|
||||
|
||||
@@ -23,12 +23,14 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
@@ -585,6 +587,29 @@ func TestResources2(t *testing.T) {
|
||||
cleanup func() error // function to run as cleanup
|
||||
}
|
||||
|
||||
type initOptions struct {
|
||||
// graph is the graph that should be passed in with Init
|
||||
graph *pgraph.Graph
|
||||
// TODO: add more options if needed
|
||||
|
||||
// logf specifies the log function for Init to pass through...
|
||||
logf func(format string, v ...interface{})
|
||||
}
|
||||
|
||||
type initOption func(*initOptions)
|
||||
|
||||
addGraph := func(graph *pgraph.Graph) initOption {
|
||||
return func(io *initOptions) {
|
||||
io.graph = graph
|
||||
}
|
||||
}
|
||||
|
||||
addLogf := func(logf func(format string, v ...interface{})) initOption {
|
||||
return func(io *initOptions) {
|
||||
io.logf = logf
|
||||
}
|
||||
}
|
||||
|
||||
// resValidate runs Validate on the res.
|
||||
resValidate := func(res engine.Res) func() error {
|
||||
// run Close
|
||||
@@ -593,9 +618,18 @@ func TestResources2(t *testing.T) {
|
||||
}
|
||||
}
|
||||
// resInit runs Init on the res.
|
||||
resInit := func(res engine.Res) func() error {
|
||||
resInit := func(res engine.Res, opts ...initOption) func() error {
|
||||
|
||||
io := &initOptions{} // defaults
|
||||
for _, optionFunc := range opts { // apply the options
|
||||
optionFunc(io)
|
||||
}
|
||||
|
||||
logf := func(format string, v ...interface{}) {
|
||||
// noop for now
|
||||
if io.logf == nil {
|
||||
return
|
||||
}
|
||||
io.logf(fmt.Sprintf("test: ")+format+"\n", v...)
|
||||
}
|
||||
init := &engine.Init{
|
||||
//Debug: debug,
|
||||
@@ -611,15 +645,18 @@ func TestResources2(t *testing.T) {
|
||||
|
||||
// Copied from state.go
|
||||
FilteredGraph: func() (*pgraph.Graph, error) {
|
||||
graph, err := pgraph.NewGraph("filtered")
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "could not create graph")
|
||||
}
|
||||
//graph, err := pgraph.NewGraph("filtered")
|
||||
//if err != nil {
|
||||
// return nil, errwrap.Wrapf(err, "could not create graph")
|
||||
//}
|
||||
// Hack: We just add ourself as allowed since
|
||||
// we're just a one-vertex test suite...
|
||||
graph.AddVertex(res) // hack!
|
||||
|
||||
return graph, nil // we return in a func so it's fresh!
|
||||
//graph.AddVertex(res) // hack!
|
||||
//return graph, nil // we return in a func so it's fresh!
|
||||
if io.graph == nil {
|
||||
return nil, fmt.Errorf("use addGraph to add one here")
|
||||
}
|
||||
return io.graph, nil
|
||||
},
|
||||
}
|
||||
// run Init
|
||||
@@ -1229,10 +1266,326 @@ func TestResources2(t *testing.T) {
|
||||
timeline: timeline,
|
||||
expect: func() error { return nil },
|
||||
startup: func() error { return nil },
|
||||
cleanup: func() error { return nil },
|
||||
cleanup: func() error { return os.Remove(p) },
|
||||
})
|
||||
}
|
||||
{
|
||||
//file "/tmp/somefile" {
|
||||
// state => "exists",
|
||||
// source => "/tmp/somefiletocopy",
|
||||
//}
|
||||
r1 := makeRes("file", "r1")
|
||||
res := r1.(*FileRes) // if this panics, the test will panic
|
||||
p := "/tmp/somefile"
|
||||
p2 := "/tmp/somefiletocopy"
|
||||
content := "hello this is some file to copy\n"
|
||||
res.Path = p
|
||||
res.State = "exists"
|
||||
res.Source = p2
|
||||
|
||||
timeline := []func() error{
|
||||
fileAbsent(p), // ensure it's absent
|
||||
fileWrite(p2, content),
|
||||
resValidate(r1),
|
||||
resInit(r1),
|
||||
resCheckApply(r1, false), // changed
|
||||
fileExpect(p, content), // should be created like this
|
||||
fileExpect(p2, content), // should not change
|
||||
resCheckApply(r1, true), // it's already good
|
||||
fileExpect(p, content), // should already be like this
|
||||
fileExpect(p2, content), // should not change either
|
||||
resClose(r1),
|
||||
}
|
||||
|
||||
testCases = append(testCases, test{
|
||||
name: "copy file with source",
|
||||
timeline: timeline,
|
||||
expect: func() error { return nil },
|
||||
startup: func() error { return nil },
|
||||
cleanup: func() error { return os.Remove(p) },
|
||||
})
|
||||
}
|
||||
{
|
||||
//file "/tmp/somedir/" {
|
||||
// state => "exists",
|
||||
//}
|
||||
r1 := makeRes("file", "r1")
|
||||
res := r1.(*FileRes) // if this panics, the test will panic
|
||||
p := "/tmp/somedir/"
|
||||
res.Path = p
|
||||
res.State = "exists"
|
||||
|
||||
timeline := []func() error{
|
||||
fileAbsent(p), // ensure it's absent
|
||||
resValidate(r1),
|
||||
resInit(r1),
|
||||
resCheckApply(r1, false), // changed
|
||||
fileExists(p, true), // ensure it's a dir
|
||||
resCheckApply(r1, true), // it's already good
|
||||
fileExists(p, true), // ensure it's a dir
|
||||
resClose(r1),
|
||||
}
|
||||
|
||||
testCases = append(testCases, test{
|
||||
name: "make empty directory",
|
||||
timeline: timeline,
|
||||
expect: func() error { return nil },
|
||||
startup: func() error { return nil },
|
||||
cleanup: func() error { return os.RemoveAll(p) },
|
||||
})
|
||||
}
|
||||
{
|
||||
//file "/tmp/somedir/" {
|
||||
// state => "exists",
|
||||
// source => /tmp/somedirtocopy/,
|
||||
// recurse => true,
|
||||
//}
|
||||
r1 := makeRes("file", "r1")
|
||||
res := r1.(*FileRes) // if this panics, the test will panic
|
||||
p := "/tmp/somedir/"
|
||||
p2 := "/tmp/somedirtocopy/"
|
||||
res.Path = p
|
||||
res.State = "exists"
|
||||
res.Source = p2
|
||||
res.Recurse = true
|
||||
|
||||
f1 := path.Join(p, "f1")
|
||||
f2 := path.Join(p, "f2")
|
||||
d1 := path.Join(p, "d1/")
|
||||
d2 := path.Join(p, "d2/")
|
||||
d1f1 := path.Join(p, "d1/f1")
|
||||
d1f2 := path.Join(p, "d1/f2")
|
||||
d2f1 := path.Join(p, "d2/f1")
|
||||
d2f2 := path.Join(p, "d2/f2")
|
||||
d2f3 := path.Join(p, "d2/f3")
|
||||
|
||||
xf1 := path.Join(p2, "f1")
|
||||
xf2 := path.Join(p2, "f2")
|
||||
xd1 := path.Join(p2, "d1/")
|
||||
xd2 := path.Join(p2, "d2/")
|
||||
xd1f1 := path.Join(p2, "d1/f1")
|
||||
xd1f2 := path.Join(p2, "d1/f2")
|
||||
xd2f1 := path.Join(p2, "d2/f1")
|
||||
xd2f2 := path.Join(p2, "d2/f2")
|
||||
xd2f3 := path.Join(p2, "d2/f3")
|
||||
|
||||
timeline := []func() error{
|
||||
fileMkdir(p2, true),
|
||||
fileWrite(xf1, "f1\n"),
|
||||
fileWrite(xf2, "f2\n"),
|
||||
fileMkdir(xd1, true),
|
||||
fileMkdir(xd2, true),
|
||||
fileWrite(xd1f1, "d1f1\n"),
|
||||
fileWrite(xd1f2, "d1f2\n"),
|
||||
fileWrite(xd2f1, "d2f1\n"),
|
||||
fileWrite(xd2f2, "d2f2\n"),
|
||||
fileWrite(xd2f3, "d2f3\n"),
|
||||
resValidate(r1),
|
||||
resInit(r1),
|
||||
resCheckApply(r1, false), // changed
|
||||
fileExists(p, true), // ensure it's a dir
|
||||
fileExists(f1, false), // ensure it's a file
|
||||
fileExists(f2, false),
|
||||
fileExists(d1, true), // ensure it's a dir
|
||||
fileExists(d2, true),
|
||||
fileExists(d1f1, false),
|
||||
fileExists(d1f2, false),
|
||||
fileExists(d2f1, false),
|
||||
fileExists(d2f2, false),
|
||||
fileExists(d2f3, false),
|
||||
resCheckApply(r1, true), // it's already good
|
||||
resClose(r1),
|
||||
}
|
||||
|
||||
testCases = append(testCases, test{
|
||||
name: "source dir copy",
|
||||
timeline: timeline,
|
||||
expect: func() error { return nil },
|
||||
startup: func() error { return nil },
|
||||
cleanup: func() error { return os.RemoveAll(p) },
|
||||
})
|
||||
}
|
||||
{
|
||||
//file "/tmp/somedir/" {
|
||||
// state => "exists",
|
||||
// recurse => true,
|
||||
// purge => true,
|
||||
//}
|
||||
r1 := makeRes("file", "r1")
|
||||
res := r1.(*FileRes) // if this panics, the test will panic
|
||||
p := "/tmp/somedir/"
|
||||
res.Path = p
|
||||
res.State = "exists"
|
||||
res.Recurse = true
|
||||
res.Purge = true
|
||||
|
||||
f1 := path.Join(p, "f1")
|
||||
f2 := path.Join(p, "f2")
|
||||
d1 := path.Join(p, "d1/")
|
||||
d2 := path.Join(p, "d2/")
|
||||
d1f1 := path.Join(p, "d1/f1")
|
||||
d1f2 := path.Join(p, "d1/f2")
|
||||
d2f1 := path.Join(p, "d2/f1")
|
||||
d2f2 := path.Join(p, "d2/f2")
|
||||
d2f3 := path.Join(p, "d2/f3")
|
||||
|
||||
graph, err := pgraph.NewGraph("test")
|
||||
if err != nil {
|
||||
panic("can't make graph")
|
||||
}
|
||||
graph.AddVertex(res) // add self
|
||||
|
||||
timeline := []func() error{
|
||||
fileMkdir(p, true),
|
||||
fileWrite(f1, "f1\n"),
|
||||
fileWrite(f2, "f2\n"),
|
||||
fileMkdir(d1, true),
|
||||
fileMkdir(d2, true),
|
||||
fileWrite(d1f1, "d1f1\n"),
|
||||
fileWrite(d1f2, "d1f2\n"),
|
||||
fileWrite(d2f1, "d2f1\n"),
|
||||
fileWrite(d2f2, "d2f2\n"),
|
||||
fileWrite(d2f3, "d2f3\n"),
|
||||
resValidate(r1),
|
||||
resInit(r1, addGraph(graph)),
|
||||
resCheckApply(r1, false), // changed
|
||||
fileExists(p, true), // ensure it's a dir
|
||||
fileAbsent(f1), // ensure it's absent
|
||||
fileAbsent(f2),
|
||||
fileAbsent(d1),
|
||||
fileAbsent(d2),
|
||||
fileAbsent(d1f1),
|
||||
fileAbsent(d1f2),
|
||||
fileAbsent(d2f1),
|
||||
fileAbsent(d2f2),
|
||||
fileAbsent(d2f3),
|
||||
resCheckApply(r1, true), // it's already good
|
||||
resClose(r1),
|
||||
}
|
||||
|
||||
testCases = append(testCases, test{
|
||||
name: "dir purge",
|
||||
timeline: timeline,
|
||||
expect: func() error { return nil },
|
||||
startup: func() error { return nil },
|
||||
cleanup: func() error { return os.RemoveAll(p) },
|
||||
})
|
||||
}
|
||||
{
|
||||
//file "/tmp/somedir/" {
|
||||
// state => "exists",
|
||||
// recurse => true,
|
||||
// purge => true,
|
||||
//}
|
||||
// TODO: should State be required for these to not delete them?
|
||||
//file "/tmp/somedir/hello" {
|
||||
//}
|
||||
//file "/tmp/somedir/nested-dir/" {
|
||||
//}
|
||||
//file "/tmp/somedir/nested-dir/nestedfileindir" {
|
||||
//}
|
||||
r1 := makeRes("file", "r1")
|
||||
res := r1.(*FileRes) // if this panics, the test will panic
|
||||
p := "/tmp/somedir/"
|
||||
res.Path = p
|
||||
res.State = "exists"
|
||||
res.Recurse = true
|
||||
res.Purge = true
|
||||
|
||||
f1 := path.Join(p, "f1")
|
||||
f2 := path.Join(p, "f2")
|
||||
d1 := path.Join(p, "d1/")
|
||||
d2 := path.Join(p, "d2/")
|
||||
d1f1 := path.Join(p, "d1/f1")
|
||||
d1f2 := path.Join(p, "d1/f2")
|
||||
d2f1 := path.Join(p, "d2/f1")
|
||||
d2f2 := path.Join(p, "d2/f2")
|
||||
d2f3 := path.Join(p, "d2/f3")
|
||||
|
||||
r2 := makeRes("file", "r2")
|
||||
res2 := r2.(*FileRes)
|
||||
p2 := path.Join(p, "hello")
|
||||
res2.Path = p2
|
||||
p2c := "i am a hello file\n"
|
||||
// TODO: should State be required for this to not delete it?
|
||||
|
||||
r3 := makeRes("file", "r3")
|
||||
res3 := r3.(*FileRes)
|
||||
p3 := path.Join(p, "nested-dir/")
|
||||
res3.Path = p3
|
||||
// TODO: should State be required for this to not delete it?
|
||||
|
||||
r4 := makeRes("file", "r4")
|
||||
res4 := r4.(*FileRes)
|
||||
p4 := path.Join(p3, "nestedfileindir")
|
||||
res4.Path = p4
|
||||
p4c := "i am a nested file\n"
|
||||
// TODO: should State be required for this to not delete it?
|
||||
|
||||
graph, err := pgraph.NewGraph("test")
|
||||
if err != nil {
|
||||
panic("can't make graph")
|
||||
}
|
||||
graph.AddVertex(res, res2, res3, res4)
|
||||
|
||||
timeline := []func() error{
|
||||
fileMkdir(p, true),
|
||||
fileWrite(f1, "f1\n"),
|
||||
fileWrite(f2, "f2\n"),
|
||||
fileMkdir(d1, true),
|
||||
fileMkdir(d2, true),
|
||||
fileWrite(d1f1, "d1f1\n"),
|
||||
fileWrite(d1f2, "d1f2\n"),
|
||||
fileWrite(d2f1, "d2f1\n"),
|
||||
fileWrite(d2f2, "d2f2\n"),
|
||||
fileWrite(d2f3, "d2f3\n"),
|
||||
fileWrite(p2, p2c),
|
||||
fileMkdir(p3, true),
|
||||
fileWrite(p4, p4c),
|
||||
|
||||
resValidate(r2),
|
||||
resInit(r2),
|
||||
//resCheckApply(r2, false), // not really needed in test
|
||||
resClose(r2),
|
||||
|
||||
resValidate(r3),
|
||||
resInit(r3),
|
||||
//resCheckApply(r3, false), // not really needed in test
|
||||
resClose(r3),
|
||||
|
||||
resValidate(r4),
|
||||
resInit(r4),
|
||||
//resCheckApply(r4, false), // not really needed in test
|
||||
resClose(r4),
|
||||
|
||||
resValidate(r1),
|
||||
resInit(r1, addGraph(graph), addLogf(nil)), // show the full graph
|
||||
resCheckApply(r1, false), // changed
|
||||
fileExists(p, true), // ensure it's a dir
|
||||
fileAbsent(f1), // ensure it's absent
|
||||
fileAbsent(f2),
|
||||
fileAbsent(d1),
|
||||
fileAbsent(d2),
|
||||
fileAbsent(d1f1),
|
||||
fileAbsent(d1f2),
|
||||
fileAbsent(d2f1),
|
||||
fileAbsent(d2f2),
|
||||
fileAbsent(d2f3),
|
||||
fileExists(p2, false), // ensure it's a file XXX !!!
|
||||
fileExists(p3, true), // ensure it's a dir
|
||||
fileExists(p4, false),
|
||||
resCheckApply(r1, true), // it's already good
|
||||
resClose(r1),
|
||||
}
|
||||
|
||||
testCases = append(testCases, test{
|
||||
name: "dir purge with others inside",
|
||||
timeline: timeline,
|
||||
expect: func() error { return nil },
|
||||
startup: func() error { return nil },
|
||||
cleanup: func() error { return os.RemoveAll(p) },
|
||||
})
|
||||
}
|
||||
names := []string{}
|
||||
for index, tc := range testCases { // run all the tests
|
||||
if tc.name == "" {
|
||||
|
||||
19
examples/lang/purge1.mcl
Normal file
19
examples/lang/purge1.mcl
Normal file
@@ -0,0 +1,19 @@
|
||||
file "/tmp/some_dir/" {
|
||||
state => "exists",
|
||||
#source => "", # this default means empty directory
|
||||
recurse => true,
|
||||
purge => true,
|
||||
}
|
||||
|
||||
file "/tmp/some_dir/fileA" {
|
||||
state => "exists",
|
||||
content => "i am fileA\n",
|
||||
}
|
||||
file "/tmp/some_dir/fileB" {
|
||||
state => "exists",
|
||||
content => "i am fileB\n",
|
||||
}
|
||||
file "/tmp/some_dir/fileC" {
|
||||
state => "exists",
|
||||
content => "i am fileC\n",
|
||||
}
|
||||
Reference in New Issue
Block a user