GithubHelp home page GithubHelp logo

gogrlx / grlx Goto Github PK

View Code? Open in Web Editor NEW
106.0 4.0 4.0 1.27 MB

Effective Fleet Configuration Management

Home Page: https://grlx.dev

License: BSD Zero Clause License

Go 97.53% Makefile 2.12% Dockerfile 0.28% Shell 0.08%
devops golang management orchestration configuration configuration-management go grlx garlic

grlx's Introduction

grlx - Effective Fleet Configuration Management

License 0BSD Go Report Card GoDoc CodeQL govulncheck GitHub commit activity (branch) Twitter Discord

grlx (pronounced like "garlic") is a pure-Go DevOps automation engine designed to use few system resources and keep your application front and center.

Quick Start

Want to get up and running as quickly as possible to see what all the fuss is about? Use our bootstrap scripts!

  1. Download and initialize the command line utility from our releases to your dev machine.
# replace 'linux' with darwin if you're on macOS
curl -L https://releases.grlx.dev/linux/amd64/latest/grlx > grlx && chmod +x grlx
./grlx init

You'll be asked some questions, such as which interface the farmer is listening on, and which ports to use for communication. Set the interface to the domain name or IP address of the farmer. Once configured, the CLI prints out your administrator public key, which you'll need for the next step! It's recommended you now add grlx somewhere in your $PATH.

  1. On your control server, you'll need to install the farmer.
# or, just run as root instead of sudo
curl -L https://bootstrap.grlx.dev/latest/farmer | sudo bash

You'll be asked several questions about the interface to listen on, which ports to use, etc. For the quick start, it's recommended to use the default ports (make sure there's no firewall in the way!). You'll be prompted for an admin public key, which you should have gotten from the prior step, and a certificate host name(s). Make sure the certificate host name matches the external-facing interface (a domain or IP address) as it will be used for TLS validation!

  1. On all of your fleet nodes, you'll need to install the sprout.
# or, just run as root instead of sudo
# FARMER_BUS_PORT and FARMER_API_PORT variables are available in case you chose
# to use different ports.
curl -L https://bootstrap.grlx.dev/latest/sprout | FARMERINTERFACE=localhost sudo -E bash

Once the sprout is up and running, return to the CLI.

  1. If all is well, you're ready to cook! Accept the TLS cert and the sprout keys when prompted.
grlx version
grlx keys accept -A
sleep 15;
grlx -T \* test ping
grlx -T \* cmd run whoami
grlx -T \* cmd run --out json -- uname -a

Documentation

Please see the official docs site for complete documentation.

Why grlx?

Our team started out using competing solutions, and we ran into scalability issues. Python is a memory hog and is interpreted to boot. Many systems struggle with installing Python dependencies properly, and with so many moving parts, the probability of something going wrong increases.

Architecture

grlx is made up of three components: the farmer, one or many sprouts, and a CLI utility, grlx. The farmer binary runs as a daemon on a management server (referred to as the 'farmer'), and is controlled via the grlx cli. grlx can be run both locally on the management server or remotely over a secure-by-default, TLS-encrypted API. The sprout binary should be installed as a daemon on systems that are to be managed. Managed systems are referred to as 'sprouts.'

Batteries Included

farmer contains an embedded messaging Pub-Sub server (NATS), and an api server. Nodes running sprout subscribe to messages over the bus. Both the API server and the messaging bus use TLS encryption (elliptic curve by default), and sprouts authenticate using public-key cryptography.

Jobs can be created with the grlx command-line interface and typically come in the form of stateful targets called 'recipes'. Recipes are yaml documents which describe the desired state of a sprout after the recipe is applied (cooked). Because the farmer exposes an API, grlx is by no means the only way to create or manage jobs, but it is the only supported method at the beginning.

Sponsors

A big thank you to all of grlx's sponsors. If you're a small company or individual user and you'd like to donate to grlx's development, you can donate to individual developers using the GitHub Sponsors button.

For prioritized and commercial support, we have partnered with ADAtomic, Inc., to offer official, on-call hours. For more information, please contact the team via email.

Founders Club

Early Adopters

If you or your company use grlx and you'd like to be added to this list, Create an Issue.

License

Dependencies may carry their own license agreements. To see the licenses of dependencies, please view DEPENDENCIES.md.

Unless otherwise noted, the grlx source files are distributed under the 0BSD license found in the LICENSE file.

All grlx logos are Copyright 2021 Tai Groot and Licensed under CC BY 3.0.

grlx's People

Contributors

dependabot[bot] avatar ethanholz avatar github-actions[bot] avatar jimt avatar taigrr avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

grlx's Issues

add Close() call

or an error if the file cannot be opened

https://api.github.com/gogrlx/grlx/blob/4d5c1c2110c18ff55022ca9e36ae01ab060e6e35/ingredients/file/hashers/hashers.go#L15

package hashers

import (
	"crypto/md5"
	"crypto/sha1"
	"crypto/sha512"
	"errors"
	"fmt"
	"hash/crc32"
	"io"
	"os"
	"sync"
)

// TODO add Close() call
type HashFunc func(io.ReadCloser, string) (string, bool, error)

var (
	hashFuncs             map[string]HashFunc
	hfTex                 sync.Mutex
	ErrHashFuncExists     = fmt.Errorf("hasher already exists")
	ErrorHashFuncNotFound = fmt.Errorf("hasher not found")
)

func init() {
	hfTex.Lock()
	hashFuncs = make(map[string]HashFunc)
	hashFuncs["md5"] = MD5
	hashFuncs["sha1"] = SHA1
	hashFuncs["sha256"] = SHA256
	hashFuncs["sha512"] = SHA512
	hashFuncs["crc"] = CRC32
	hfTex.Unlock()
}

func Register(id string, function HashFunc) error {
	hfTex.Lock()
	defer hfTex.Unlock()
	if _, ok := hashFuncs[id]; ok {
		return errors.Join(ErrHashFuncExists, fmt.Errorf("hasher %s already exists", id))
	}
	hashFuncs[id] = function
	return nil
}

func GetHashFunc(hashType string) (HashFunc, error) {
	hfTex.Lock()
	defer hfTex.Unlock()
	if hf, ok := hashFuncs[hashType]; ok {
		return hf, nil
	}
	return nil, errors.Join(ErrorHashFuncNotFound, fmt.Errorf("hasher %s not found", hashType))
}

// Given a filename, return a reader for the file
// or an error if the file cannot be opened
func FileToReader(file string) (io.Reader, error) {
	f, err := os.Open(file)
	if err != nil {
		return nil, err
	}
	return f, nil
}

func MD5(file io.ReadCloser, expected string) (string, bool, error) {
	var actual string
	md5 := md5.New()
	if _, err := io.Copy(md5, file); err != nil {
		return actual, false, err
	}
	actual = fmt.Sprintf("%x", md5.Sum(nil))
	return actual, actual == expected, nil
}

func SHA1(file io.ReadCloser, expected string) (string, bool, error) {
	var actual string
	sha1 := sha1.New()
	if _, err := io.Copy(sha1, file); err != nil {
		return actual, false, err
	}
	actual = fmt.Sprintf("%x", sha1.Sum(nil))
	return actual, actual == expected, nil
}

func SHA256(file io.ReadCloser, expected string) (string, bool, error) {
	var actual string
	sha1 := sha1.New()
	if _, err := io.Copy(sha1, file); err != nil {
		return actual, false, err
	}
	actual = fmt.Sprintf("%x", sha1.Sum(nil))

	return actual, actual == expected, nil
}

func SHA512(file io.ReadCloser, expected string) (string, bool, error) {
	var actual string
	sha512 := sha512.New()
	if _, err := io.Copy(sha512, file); err != nil {
		return actual, false, err
	}
	actual = fmt.Sprintf("%x", sha512.Sum(nil))

	return actual, actual == expected, nil
}

func CRC32(file io.ReadCloser, expected string) (string, bool, error) {
	var actual string
	table := crc32.IEEETable
	crc := crc32.New(table)
	if _, err := io.Copy(crc, file); err != nil {
		return actual, false, err
	}
	actual = fmt.Sprintf("%x", crc.Sum(nil))

	return actual, actual == expected, nil
}

make sure envelope.Test is set in grlx and farmer

so don't try to access res.Changed or res.Changes

https://api.github.com/gogrlx/grlx/blob/8d33df33f701e0d4d99bad5d3aa6beda9fa9d687/cook/actuallycook.go#L102

				// all requisites are met, so start the step in a goroutine
				go func(step types.Step) {
					defer wg.Done()
					// use the ingredient package to load and cook the step
					ingredient, err := ingredients.NewRecipeCooker(step.ID, step.Ingredient, step.Method, step.Properties)
					if err != nil {
						completionChan <- StepCompletion{
							ID:               step.ID,
							CompletionStatus: Failed,
						}
					}
					var res types.Result
					// TODO allow for cancellation
					bgCtx := context.Background()
					// TODO make sure envelope.Test is set in grlx and farmer
					if envelope.Test {
						res, err = ingredient.Test(bgCtx)
					} else {
						res, err = ingredient.Apply(bgCtx)
					}
					if res.Succeeded {
						completionChan <- StepCompletion{
							ID:               step.ID,
							CompletionStatus: Completed,
							ChangesMade:      res.Changed,
							Changes:          res.Changes,
						}
					} else if res.Failed {
						completionChan <- StepCompletion{
							ID:               step.ID,
							CompletionStatus: Failed,
							ChangesMade:      res.Changed,
							Changes:          res.Changes,
						}
					} else if err != nil {
						completionChan <- StepCompletion{
							ID:               step.ID,
							CompletionStatus: Completed,
							Error:            err,
							// res might be nil if err is not nil
							// so don't try to access res.Changed or res.Changes
						}
					}
				}(stepMap[id])

make changes a proper stringer

https://api.github.com/gogrlx/grlx/blob/185ddbfe19c3b1b0496656ac67ae06312898752b/ingredients/file/file.go#L99

		if test {
			return types.Result{
				Succeeded: true, Failed: false,
				// TODO: make changes a proper stringer
				Changed: true, Changes: []fmt.Stringer{types.SimpleChange(fmt.Sprintf("%v", fp))},
			}, nil
		} else {
			err = fp.Download(ctx)
			if err != nil {
				return types.Result{Succeeded: false, Failed: true}, err
			}
			return types.Result{Succeeded: true, Failed: false, Changed: true, Changes: []fmt.Stringer{types.SimpleChange(fmt.Sprintf("%v", fp))}}, nil
		}
	}
	return types.Result{Succeeded: true, Failed: false, Changed: false}, nil
}

func (f File) directory(ctx context.Context, test bool) (types.Result, error) {
	changes := []fmt.Stringer{}
	type dir struct {
		user           string
		group          string

here broadcast the error on the bus

https://api.github.com/gogrlx/grlx/blob/a79315bf625eb31aaf7620098b3cab52db51e96b/cook/actuallycook.go#L59

			ChangesMade:      false,
		}
	}
	stepMap := map[types.StepID]types.Step{}
	for _, step := range envelope.Steps {
		stepMap[step.ID] = step
	}
	// create a wait group and a channel to receive step completions
	wg := sync.WaitGroup{}
	wg.Add(len(envelope.Steps))
	completionChan := make(chan StepCompletion, 5)
	ctx, cancel := context.WithCancel(context.Background())
	// spawn a goroutine to wait for all steps to complete and then cancel the context
	go func() {
		wg.Wait()
		cancel()
	}()
	for {
		select {
		// each time a step completes, check if any other steps can be started
		case completion := <-completionChan:
			completionMap[completion.ID] = completion
			for id, step := range completionMap {
				if step.CompletionStatus != NotStarted {
					continue
				}
				requisitesMet, err := RequisitesAreMet(stepMap[id], completionMap)
				if err != nil {
					// TODO here broadcast the error on the bus
					completionChan <- StepCompletion{
						ID:               id,
						CompletionStatus: Failed,
					}
					wg.Done()
					continue
				}
				if !requisitesMet {
					continue
				}
				// all requisites are met, so start the step in a goroutine
				go func(step types.Step) {
					// TODO here use the ingredient package to load and cook the step
					wg.Done()
					completionChan <- StepCompletion{
						ID:               step.ID,
						CompletionStatus: Completed,
					}
				}(stepMap[id])
			}
		// All steps are done, so context will be cancelled and we'll exit
		case <-ctx.Done():
			return nil
		}
	}
}

// RequisitesAreMet returns true if all of the requisites for the given step are met
// TODO this is a stub
func RequisitesAreMet(step types.Step, completionMap map[types.StepID]StepCompletion) (bool, error) {
	return false, nil
}

here broadcast the error on the bus and mark the step as failed

https://api.github.com/gogrlx/grlx/blob/5e9b01623c34ff47a11908492dc316ff2394558c/cook/actuallycook.go#L56

package cook

import (
	"context"
	"sync"

	"github.com/gogrlx/grlx/types"
)

type CompletionStatus int

const (
	NotStarted CompletionStatus = iota
	InProgress
	Completed
	Failed
)

type StepCompletion struct {
	ID               types.StepID
	CompletionStatus CompletionStatus
	ChangesMade      bool
}

func CookRecipeEnvelope(envelope types.RecipeEnvelope) error {
	completionMap := map[types.StepID]StepCompletion{}
	stepMap := map[types.StepID]types.Step{}
	for _, step := range envelope.Steps {
		stepMap[step.ID] = step
	}
	for _, step := range envelope.Steps {
		completionMap[step.ID] = StepCompletion{
			ID:               step.ID,
			CompletionStatus: NotStarted,
			ChangesMade:      false,
		}
	}
	wg := sync.WaitGroup{}
	wg.Add(len(envelope.Steps))
	completionChan := make(chan StepCompletion, 5)
	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		wg.Wait()
		cancel()
	}()
	for {
		select {
		case completion := <-completionChan:
			completionMap[completion.ID] = completion
			for id, step := range completionMap {
				if step.CompletionStatus != NotStarted {
					continue
				}
				requisitesMet, err := RequisitesAreMet(stepMap[id])
				if err != nil {
					// TODO here broadcast the error on the bus and mark the step as failed
					wg.Done()
				}
				if !requisitesMet {
					continue
				}
				go func() {
					// TODO here use the ingredient package to load and cook the step
					wg.Done()
				}()
			}
		case <-ctx.Done():
			return nil
		}
	}
}

func RequisitesAreMet(step types.Step) (bool, error) {
	return false, nil
}

check for Test v Apply

https://api.github.com/gogrlx/grlx/blob/cd27501d919f074ddeefcc100c227d841545a258/cook/actuallycook.go#L89

				if !requisitesMet {
					continue
				}
				// mark the step as in progress
				t := completionMap[id]
				t.CompletionStatus = InProgress
				completionMap[id] = t
				// all requisites are met, so start the step in a goroutine
				go func(step types.Step) {
					defer wg.Done()
					// TODO here use the ingredient package to load and cook the step
					ingredient, err := ingredients.NewRecipeCooker(step.ID, step.Ingredient, step.Method, step.Properties)
					if err != nil {
						// TODO here broadcast the error on the bus
						completionChan <- StepCompletion{
							ID:               step.ID,
							CompletionStatus: Failed,
						}
					}
					// TODO allow for cancellation
					// TODO check for Test v Apply
					res, err := ingredient.Apply(context.Background())
					if err != nil {
						completionChan <- StepCompletion{
							ID:               step.ID,
							CompletionStatus: Failed,
						}
					}
					if res.Succeeded {
						completionChan <- StepCompletion{
							ID:               step.ID,
							CompletionStatus: Completed,
						}
					} else if res.Failed {
						completionChan <- StepCompletion{
							ID:               step.ID,
							CompletionStatus: Failed,
						}
					} else {
						completionChan <- StepCompletion{
							ID:               step.ID,
							CompletionStatus: Completed,
						}
					}
				}(stepMap[id])
			}

join with an error type for missing params

https://api.github.com/gogrlx/grlx/blob/f52ddd715a11c92c20318d1c929062943d6d0313/ingredients/file/fileCached.go#L16

package file

import (
	"context"
	"errors"
	"fmt"
	"path/filepath"

	"github.com/gogrlx/grlx/config"
	"github.com/gogrlx/grlx/types"
)

func (f File) cached(ctx context.Context, test bool) (types.Result, error) {
	source, ok := f.params["source"].(string)
	if !ok || source == "" {
		// TODO join with an error type for missing params
		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("source not defined")
	}
	// TODO allow for skip_verify here
	hash, ok := f.params["hash"].(string)
	if !ok || hash == "" {
		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("hash not defined")
	}
	// TODO determine filename scheme for skip_verify downloads
	cacheDest := filepath.Join(config.CacheDir, hash)
	fp, err := NewFileProvider(f.id, source, cacheDest, hash, f.params)
	if err != nil {
		return types.Result{Succeeded: false, Failed: true}, err
	}
	// TODO allow for skip_verify here
	valid, errVal := fp.Verify(ctx)
	if errVal != nil && !errors.Is(errVal, types.ErrFileNotFound) {
		return types.Result{Succeeded: false, Failed: true}, errVal
	}
	if !valid {
		if test {
			return types.Result{
				Succeeded: true, Failed: false,
				// TODO: make changes a proper stringer
				Changed: true, Notes: []fmt.Stringer{types.SimpleNote(fmt.Sprintf("%v", fp))},
			}, nil
		} else {
			err = fp.Download(ctx)
			if err != nil {
				return types.Result{Succeeded: false, Failed: true}, err
			}
			return types.Result{Succeeded: true, Failed: false, Changed: true, Notes: []fmt.Stringer{types.SimpleNote(fmt.Sprintf("%v", fp))}}, nil
		}
	}
	return types.Result{Succeeded: true, Failed: false, Changed: false}, nil
}

determine filename scheme for skip_verify downloads

https://api.github.com/gogrlx/grlx/blob/ef9ee84ca03797c48e3c828b395b5352054c71ff/ingredients/file/fileCached.go#L24

package file

import (
	"context"
	"errors"
	"fmt"
	"path/filepath"

	"github.com/gogrlx/grlx/config"
	"github.com/gogrlx/grlx/types"
)

func (f File) cached(ctx context.Context, test bool) (types.Result, error) {
	source, ok := f.params["source"].(string)
	if !ok || source == "" {
		// TODO join with an error type for missing params
		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("source not defined")
	}
	// TODO allow for skip_verify here
	hash, ok := f.params["hash"].(string)
	if !ok || hash == "" {
		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("hash not defined")
	}
	// TODO determine filename scheme for skip_verify downloads
	cacheDest := filepath.Join(config.CacheDir, hash)
	fp, err := NewFileProvider(f.id, source, cacheDest, hash, f.params)
	if err != nil {
		return types.Result{Succeeded: false, Failed: true}, err
	}
	// TODO allow for skip_verify here
	valid, errVal := fp.Verify(ctx)
	if errVal != nil && !errors.Is(errVal, types.ErrFileNotFound) {
		return types.Result{Succeeded: false, Failed: true}, errVal
	}
	if !valid {
		if test {
			return types.Result{
				Succeeded: true, Failed: false,
				// TODO: make changes a proper stringer
				Changed: true, Notes: []fmt.Stringer{types.SimpleNote(fmt.Sprintf("%v", fp))},
			}, nil
		} else {
			err = fp.Download(ctx)
			if err != nil {
				return types.Result{Succeeded: false, Failed: true}, err
			}
			return types.Result{Succeeded: true, Failed: false, Changed: true, Notes: []fmt.Stringer{types.SimpleNote(fmt.Sprintf("%v", fp))}}, nil
		}
	}
	return types.Result{Succeeded: true, Failed: false, Changed: false}, nil
}

here use the ingredient package to load and cook the step

https://api.github.com/gogrlx/grlx/blob/5e9b01623c34ff47a11908492dc316ff2394558c/cook/actuallycook.go#L63

package cook

import (
	"context"
	"sync"

	"github.com/gogrlx/grlx/types"
)

type CompletionStatus int

const (
	NotStarted CompletionStatus = iota
	InProgress
	Completed
	Failed
)

type StepCompletion struct {
	ID               types.StepID
	CompletionStatus CompletionStatus
	ChangesMade      bool
}

func CookRecipeEnvelope(envelope types.RecipeEnvelope) error {
	completionMap := map[types.StepID]StepCompletion{}
	stepMap := map[types.StepID]types.Step{}
	for _, step := range envelope.Steps {
		stepMap[step.ID] = step
	}
	for _, step := range envelope.Steps {
		completionMap[step.ID] = StepCompletion{
			ID:               step.ID,
			CompletionStatus: NotStarted,
			ChangesMade:      false,
		}
	}
	wg := sync.WaitGroup{}
	wg.Add(len(envelope.Steps))
	completionChan := make(chan StepCompletion, 5)
	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		wg.Wait()
		cancel()
	}()
	for {
		select {
		case completion := <-completionChan:
			completionMap[completion.ID] = completion
			for id, step := range completionMap {
				if step.CompletionStatus != NotStarted {
					continue
				}
				requisitesMet, err := RequisitesAreMet(stepMap[id])
				if err != nil {
					// TODO here broadcast the error on the bus and mark the step as failed
					wg.Done()
				}
				if !requisitesMet {
					continue
				}
				go func() {
					// TODO here use the ingredient package to load and cook the step
					wg.Done()
				}()
			}
		case <-ctx.Done():
			return nil
		}
	}
}

func RequisitesAreMet(step types.Step) (bool, error) {
	return false, nil
}

join with an error type for missing params

https://api.github.com/gogrlx/grlx/blob/7e6aa1b717ddff84c9bdee90a188b012fe01a448/ingredients/file/file.go#L56

}

func New(id, method string, params map[string]interface{}) (File, error) {
	return File{id: id, method: method, params: params}, nil
}

func (f File) Test(ctx context.Context) (types.Result, error) {
	switch f.method {
	case "absent":
		return f.absent(ctx, true)
	case "append":
		fallthrough
	case "contains":
		fallthrough
	case "content":
		fallthrough
	case "managed":
		fallthrough
	case "present":
		fallthrough
	case "symlink":
		fallthrough
	default:
		// TODO define error type
		return types.Result{Succeeded: false, Failed: true, Changed: false, Changes: nil}, fmt.Errorf("method %s undefined", f.method)

	}
}

func (f File) absent(ctx context.Context, test bool) (types.Result, error) {
	name, ok := f.params["name"].(string)
	if !ok {
		// TODO join with an error type for missing params
		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("name not defined")
	}
	name = filepath.Clean(name)
	if name == "" {
		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("name not defined")
	}
	if name == "/" {
		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("refusing to delete root")
	}
	_, err := os.Stat(name)
	if errors.Is(err, os.ErrNotExist) {
		return types.Result{Succeeded: true, Failed: false, Changed: false, Changes: nil}, nil
	}
	if err != nil {
		return types.Result{Succeeded: false, Failed: true}, err
	}
	if test {
		return types.Result{Succeeded: true, Failed: false, Changed: true, Changes: nil}, nil
	}
	err = os.Remove(name)
	if err != nil {
		return types.Result{Succeeded: false, Failed: true}, err
	}
	return types.Result{Succeeded: true, Failed: false, Changed: true, Changes: struct{ Removed []string }{Removed: []string{name}}}, nil
}

func (f File) Apply(ctx context.Context) (types.Result, error) {
	switch f.method {
	case "absent":
		return f.absent(ctx, false)
	case "append":
		fallthrough
	case "contains":

add test apply path here

https://api.github.com/gogrlx/grlx/blob/b1e651999b27bbe0a0c4c42ab79a8835dce8357d/ingredients/file/file.go#L207

			}
		}
	}
	// chmod the directory to the named dirmode if it is defined
	// TODO add test apply path here
	{
		if val, ok := f.params["dir_mode"].(string); ok {
			d.dirMode = val
			modeVal, _ := strconv.ParseUint(d.dirMode, 8, 32)
			// "dir_mode": "string", "file_mode": "string"
			//"clean": "bool", "follow_symlinks": "bool", "force": "bool", "backupname": "string", "allow_symlink": "bool",
			err := os.Chmod(name, os.FileMode(modeVal))
			if err != nil {
				return types.Result{Succeeded: false, Failed: true}, err
			}
		}
	}
	// chmod the directory to the named dirmode if it is defined
	// TODO add test apply path here
	{
		if val, ok := f.params["file_mode"].(string); ok {
			d.fileMode = val
			modeVal, _ := strconv.ParseUint(d.fileMode, 8, 32)
			// "makedirs": "bool",
			//"clean": "bool", "follow_symlinks": "bool", "force": "bool", "backupname": "string", "allow_symlink": "bool",
			err := os.Chmod(name, os.FileMode(modeVal))
			if err != nil {
				return types.Result{Succeeded: false, Failed: true}, err
			}
		}
	} // recurse the file_mode if it is defined
	// TODO add test apply path here
	{
		if val, ok := f.params["group"].(string); ok {
			d.group = val
			group, lookupErr := user.LookupGroup(d.group)
			if lookupErr != nil {
				return types.Result{Succeeded: false, Failed: true}, lookupErr
			}
			gid, parseErr := strconv.ParseUint(group.Gid, 10, 32)
			if parseErr != nil {
				return types.Result{Succeeded: false, Failed: true}, parseErr
			}
			chownErr := os.Chown(name, -1, int(gid))
			if chownErr != nil {
				return types.Result{Succeeded: false, Failed: true}, chownErr
			}
			if val, ok := f.params["recurse"].(bool); ok && val {
				walkErr := filepath.WalkDir(name, func(path string, d fs.DirEntry, err error) error {
					return os.Chown(path, -1, int(gid))
				})
				if walkErr != nil {
					return types.Result{Succeeded: false, Failed: true}, walkErr
				}
			}
		}
	}

	return f.undef()
}

configure expiration time for job data on the sprout and farmer

https://api.github.com/gogrlx/grlx/blob/f5f667d4c62669313e1f61a7d4fc8a9df43dcc8a/cook/jobs.go#L14

package cook

// This code will subscribe to the jobs topic and record the jobs
// to flat files in the jobs directory.  The files will be named
// with the job id and will contain the job data in jsonL format.

// The jobs directory will be created if it does not exist.

// Jobs are stored in triplicate: in the jobs directory on the farmer,
// in the jobs directory on the sprout, and in the jobs directory on
// the cli user's machine.
// Jobs can be retrieved from the farmer with the grlx job command.

// TODO configure expiration time for job data on the sprout and farmer

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"time"

	"github.com/gogrlx/grlx/types"
)

// Job represents a job

type Job struct {
	Id      string         `json:"id"`
	Results []types.Result `json:"results"`
	Sprout  string         `json:"sprout"`
	Summary types.Summary  `json:"summary"`
}

func logJobs() {
	// Create the jobs directory if it does not exist
	if _, err := os.Stat("jobs"); os.IsNotExist(err) {
		os.Mkdir("jobs", 0o755)
	}

	// Subscribe to the jobs topic
	sub, err := natsConn.SubscribeSync("jobs")
	if err != nil {
		log.Fatal(err)
	}

	// Read messages from the jobs topic
	for {
		msg, err := sub.NextMsg(10 * time.Second)
		if err != nil {
			log.Fatal(err)
		}

		// Get the job data
		var job Job
		err = json.Unmarshal(msg.Data, &job)
		if err != nil {
			log.Fatal(err)
		}

		// Create the job file
		jobFile := filepath.Join("jobs", fmt.Sprintf("%s.jsonl", job.Id))
		f, err := os.Create(jobFile)
		if err != nil {
			log.Fatal(err)
		}

		// Write the job data to the file
		b, err := json.Marshal(job)
		if err != nil {
			log.Fatal(err)
		}
		_, err = f.Write(b)
		if err != nil {
			log.Fatal(err)
		}
		_, err = f.WriteString("\n")
		if err != nil {
			log.Fatal(err)
		}

		// Close the file
		err = f.Close()
		if err != nil {
			log.Fatal(err)
		}

		// Log the job
		log.Printf("Job %s received\n", job.Id)

		// Acknowledge the message
		err = msg.Ack()
		if err != nil {
			log.Fatal(err)
		}
	}
}

allow for skip_verify here

https://api.github.com/gogrlx/grlx/blob/dc84e51242741f4ef9a64477401d0e1f8128f964/ingredients/file/file.go#L73

	}
}

func (f File) cached(ctx context.Context, test bool) (types.Result, error) {
	source, ok := f.params["source"].(string)
	if !ok || source == "" {
		// TODO join with an error type for missing params
		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("source not defined")
	}
	// TODO allow for skip_verify here
	hash, ok := f.params["hash"].(string)
	if !ok || hash == "" {
		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("hash not defined")
	}
	// TODO determine filename scheme for skip_verify downloads
	cacheDest := filepath.Join(config.CacheDir(), hash)
	fp, err := NewFileProvider(f.id, source, cacheDest, hash, f.params)
	if err != nil {
		return types.Result{Succeeded: false, Failed: true}, err
	}
	// TODO allow for skip_verify here
	valid, errVal := fp.Verify(ctx)
	if errVal != nil {
		return types.Result{Succeeded: false, Failed: true}, errVal
	}
	if !valid {
		if test {
			return types.Result{
				Succeeded: true, Failed: false,
				Changed: true, Changes: fp,
			}, nil
		} else {
			err = fp.Download(ctx)
			if err != nil {
				return types.Result{Succeeded: false, Failed: true}, err
			}
			return types.Result{Succeeded: true, Failed: false, Changed: true, Changes: fp}, nil
		}
	}
	return types.Result{Succeeded: true, Failed: false, Changed: false}, nil
}

func (f File) absent(ctx context.Context, test bool) (types.Result, error) {
	name, ok := f.params["name"].(string)
	if !ok {

look into effects of sorting vs not sorting this slice

https://api.github.com/gogrlx/grlx/blob/5f20c40f32c7794aff76fdba85b36c7cc8485a09/ingredients/file/file.go#L875

			lines = append(lines, scanner.Text())
		}
	}
	file, err := os.Open(name)
	if err != nil {
		return types.Result{
			Succeeded: false, Failed: true,
			Changed: false, Notes: []fmt.Stringer{
				types.SimpleNote(fmt.Sprintf("failed to open %s", name)),
			},
		}, lines, err
	}
	// TODO look into effects of sorting vs not sorting this slice
	sort.Strings(lines)
	contents := []string{}
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		contents = append(contents, scanner.Text())
	}
	file.Close()
	sort.Strings(contents)
	isSubset, missing := stringSliceIsSubset(lines, contents)
	if isSubset {
		return types.Result{Succeeded: true, Failed: false}, []string{}, nil
	}
	return types.Result{
		Succeeded: false, Failed: true,
		Changed: false, Notes: []fmt.Stringer{
			types.SimpleNote(fmt.Sprintf("file %s does not contain all specified content", name)),
		},
	}, missing, types.ErrMissingContent
}

func stringSliceIsSubset(a, b []string) (bool, []string) {
	missing := []string{}
	for len(a) > 0 {
		switch {
		case len(b) == 0:
			missing = append(missing, a...)
			return len(missing) == 0, missing
		case a[0] == b[0]:
			a = a[1:]
			b = b[1:]
		case a[0] < b[0]:
			missing = append(missing, a[0])
			if len(a) == 1 {
				return len(missing) == 0, missing
			}
			a = a[1:]
			b = b[1:]
		case a[0] > b[0]:
			b = b[1:]
		}
	}
	return len(missing) == 0, missing
}

func (f File) content(ctx context.Context, test bool) (types.Result, error) {

define error type

https://api.github.com/gogrlx/grlx/blob/7e6aa1b717ddff84c9bdee90a188b012fe01a448/ingredients/file/file.go#L47

}

func New(id, method string, params map[string]interface{}) (File, error) {
	return File{id: id, method: method, params: params}, nil
}

func (f File) Test(ctx context.Context) (types.Result, error) {
	switch f.method {
	case "absent":
		return f.absent(ctx, true)
	case "append":
		fallthrough
	case "contains":
		fallthrough
	case "content":
		fallthrough
	case "managed":
		fallthrough
	case "present":
		fallthrough
	case "symlink":
		fallthrough
	default:
		// TODO define error type
		return types.Result{Succeeded: false, Failed: true, Changed: false, Changes: nil}, fmt.Errorf("method %s undefined", f.method)

	}
}

func (f File) absent(ctx context.Context, test bool) (types.Result, error) {
	name, ok := f.params["name"].(string)
	if !ok {
		// TODO join with an error type for missing params
		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("name not defined")
	}
	name = filepath.Clean(name)
	if name == "" {
		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("name not defined")
	}
	if name == "/" {
		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("refusing to delete root")
	}
	_, err := os.Stat(name)
	if errors.Is(err, os.ErrNotExist) {
		return types.Result{Succeeded: true, Failed: false, Changed: false, Changes: nil}, nil
	}
	if err != nil {
		return types.Result{Succeeded: false, Failed: true}, err
	}
	if test {
		return types.Result{Succeeded: true, Failed: false, Changed: true, Changes: nil}, nil
	}
	err = os.Remove(name)
	if err != nil {
		return types.Result{Succeeded: false, Failed: true}, err
	}
	return types.Result{Succeeded: true, Failed: false, Changed: true, Changes: struct{ Removed []string }{Removed: []string{name}}}, nil
}

func (f File) Apply(ctx context.Context) (types.Result, error) {
	switch f.method {
	case "absent":
		return f.absent(ctx, false)
	case "append":
		fallthrough
	case "contains":

remove undef func

https://api.github.com/gogrlx/grlx/blob/44c91d08d74ad1a6359b03accfbf8a11e476b27f/ingredients/file/file.go#L28

// TODO error check, set id, properly parse
func (f File) Parse(id, method string, params map[string]interface{}) (types.RecipeCooker, error) {
	return File{id: id, method: method, params: params}, nil
}

// this is a helper func to replace fallthroughs so I can keep the
// cases sorted alphabetically. It's not exported and won't stick around.
// TODO remove undef func
func (f File) undef() (types.Result, error) {
	return types.Result{Succeeded: false, Failed: true, Changed: false, Changes: nil}, fmt.Errorf("method %s undefined", f.method)
}

func (f File) Test(ctx context.Context) (types.Result, error) {

define error type

https://api.github.com/gogrlx/grlx/blob/7e6aa1b717ddff84c9bdee90a188b012fe01a448/ingredients/file/file.go#L123

		fallthrough
	default:
		// TODO define error type
		return types.Result{Succeeded: false, Failed: true, Changed: false, Changes: nil}, fmt.Errorf("method %s undefined", f.method)

	}
}

func (f File) PropertiesForMethod(method string) (map[string]string, error) {
	switch f.method {
	case "absent":
		return map[string]string{"name": "string"}, nil
	case "append":
		fallthrough
	case "contains":
		fallthrough
	case "content":
		fallthrough
	case "managed":
		fallthrough
	case "present":
		fallthrough
	case "symlink":
		fallthrough
	default:
		// TODO define error type
		return nil, fmt.Errorf("method %s undefined", f.method)

	}
}

func (f File) Methods() (string, []string) {

add test apply path here

https://api.github.com/gogrlx/grlx/blob/b1e651999b27bbe0a0c4c42ab79a8835dce8357d/ingredients/file/file.go#L207

			}
		}
	}
	// chmod the directory to the named dirmode if it is defined
	// TODO add test apply path here
	{
		if val, ok := f.params["dir_mode"].(string); ok {
			d.dirMode = val
			modeVal, _ := strconv.ParseUint(d.dirMode, 8, 32)
			// "dir_mode": "string", "file_mode": "string"
			//"clean": "bool", "follow_symlinks": "bool", "force": "bool", "backupname": "string", "allow_symlink": "bool",
			err := os.Chmod(name, os.FileMode(modeVal))
			if err != nil {
				return types.Result{Succeeded: false, Failed: true}, err
			}
		}
	}
	// chmod the directory to the named dirmode if it is defined
	// TODO add test apply path here
	{
		if val, ok := f.params["file_mode"].(string); ok {
			d.fileMode = val
			modeVal, _ := strconv.ParseUint(d.fileMode, 8, 32)
			// "makedirs": "bool",
			//"clean": "bool", "follow_symlinks": "bool", "force": "bool", "backupname": "string", "allow_symlink": "bool",
			err := os.Chmod(name, os.FileMode(modeVal))
			if err != nil {
				return types.Result{Succeeded: false, Failed: true}, err
			}
		}
	} // recurse the file_mode if it is defined
	// TODO add test apply path here
	{
		if val, ok := f.params["group"].(string); ok {
			d.group = val
			group, lookupErr := user.LookupGroup(d.group)
			if lookupErr != nil {
				return types.Result{Succeeded: false, Failed: true}, lookupErr
			}
			gid, parseErr := strconv.ParseUint(group.Gid, 10, 32)
			if parseErr != nil {
				return types.Result{Succeeded: false, Failed: true}, parseErr
			}
			chownErr := os.Chown(name, -1, int(gid))
			if chownErr != nil {
				return types.Result{Succeeded: false, Failed: true}, chownErr
			}
			if val, ok := f.params["recurse"].(bool); ok && val {
				walkErr := filepath.WalkDir(name, func(path string, d fs.DirEntry, err error) error {
					return os.Chown(path, -1, int(gid))
				})
				if walkErr != nil {
					return types.Result{Succeeded: false, Failed: true}, walkErr
				}
			}
		}
	}

	return f.undef()
}

add a signal on the sprout side to indicate that the cook is complete

complete <- struct{}{}

https://api.github.com/gogrlx/grlx/blob/f1aa1b7e72ff749d1203a48a833bf21e39885793/pkg/grlx/cmd/cook.go#L68

				log.Panic(err)
			}
		}
		// topic: grlx.cook."+envelope.JobID+"."+pki.GetSproutID()
		jid := results.JID
		nc, err := client.NewNatsClient()
		if err != nil {
			log.Fatal(err)
		}
		ec, err := nats.NewEncodedConn(nc, nats.JSON_ENCODER)
		if err != nil {
			log.Fatal(err)
		}
		complete := make(chan struct{})
		sub, err := ec.Subscribe("grlx.cook."+jid+".>", func(msg *nats.Msg) {
			printTex.Lock()
			defer printTex.Unlock()
			fmt.Println(msg.Subject)
			fmt.Println(string(msg.Data))
			// TODO add a signal on the sprout side to indicate that the cook is complete
			// complete <- struct{}{}
		})
		if err != nil {
			log.Fatal(err)
		}
		defer sub.Unsubscribe()
		defer nc.Flush()
		<-complete

		switch outputMode {
		case "json":
			jw, _ := json.Marshal(results)

this is a stub

https://api.github.com/gogrlx/grlx/blob/a79315bf625eb31aaf7620098b3cab52db51e96b/cook/actuallycook.go#L88

			ChangesMade:      false,
		}
	}
	stepMap := map[types.StepID]types.Step{}
	for _, step := range envelope.Steps {
		stepMap[step.ID] = step
	}
	// create a wait group and a channel to receive step completions
	wg := sync.WaitGroup{}
	wg.Add(len(envelope.Steps))
	completionChan := make(chan StepCompletion, 5)
	ctx, cancel := context.WithCancel(context.Background())
	// spawn a goroutine to wait for all steps to complete and then cancel the context
	go func() {
		wg.Wait()
		cancel()
	}()
	for {
		select {
		// each time a step completes, check if any other steps can be started
		case completion := <-completionChan:
			completionMap[completion.ID] = completion
			for id, step := range completionMap {
				if step.CompletionStatus != NotStarted {
					continue
				}
				requisitesMet, err := RequisitesAreMet(stepMap[id], completionMap)
				if err != nil {
					// TODO here broadcast the error on the bus
					completionChan <- StepCompletion{
						ID:               id,
						CompletionStatus: Failed,
					}
					wg.Done()
					continue
				}
				if !requisitesMet {
					continue
				}
				// all requisites are met, so start the step in a goroutine
				go func(step types.Step) {
					// TODO here use the ingredient package to load and cook the step
					wg.Done()
					completionChan <- StepCompletion{
						ID:               step.ID,
						CompletionStatus: Completed,
					}
				}(stepMap[id])
			}
		// All steps are done, so context will be cancelled and we'll exit
		case <-ctx.Done():
			return nil
		}
	}
}

// RequisitesAreMet returns true if all of the requisites for the given step are met
// TODO this is a stub
func RequisitesAreMet(step types.Step, completionMap map[types.StepID]StepCompletion) (bool, error) {
	return false, nil
}

also collect the results of the step and store them into a log folder by JID

https://api.github.com/gogrlx/grlx/blob/8d33df33f701e0d4d99bad5d3aa6beda9fa9d687/cook/actuallycook.go#L65

		select {
		// each time a step completes, check if any other steps can be started
		case completion := <-completionChan:
			ec.Publish("grlx.cook."+envelope.JobID+"."+pki.GetSproutID(), completion)
			log.Noticef("Step %s completed with status %v", completion.ID, completion)
			// TODO also collect the results of the step and store them into a log folder by JID
			completionMap[completion.ID] = completion
			for id, step := range completionMap {
				if step.CompletionStatus != NotStarted {

add notes for each changed timestamp if changed

https://api.github.com/gogrlx/grlx/blob/a6ef44fb8b45d263c0b28c6234fcbd09c77b060d/ingredients/file/file.go#L499

}

func (f File) prepend(ctx context.Context, test bool) (types.Result, error) {
	// TODO
	// "name": "string", "text": "[]string", "makedirs": "bool",
	// "source": "string", "source_hash": "string",
	// "template": "bool", "sources": "[]string",
	// "source_hashes": "[]string", "ignore_whitespace": "bool",
	return f.undef()
}

func (f File) touch(ctx context.Context, test bool) (types.Result, error) {
	// TODO
	return f.undef()
	name, ok := f.params["name"].(string)
	if !ok {
		return types.Result{Succeeded: false, Failed: true}, types.ErrMissingName
	}

	// "name": "string", "atime": "string",
	// "mtime": "string", "makedirs": "bool",
	atime := time.Now()
	mtime := time.Now()
	{
		// parse atime
		atimeStr, ok := f.params["atime"].(string)
		if ok && atimeStr != "" {
			at, err := time.Parse(time.RFC3339, atimeStr)
			if err != nil {
				return types.Result{Succeeded: false, Failed: true, Changed: false, Notes: []fmt.Stringer{types.SimpleNote("")}}, err
			}
			atime = at
		}
	}
	{
		// parse mtime
		mtimeStr, ok := f.params["mtime"].(string)
		if ok && mtimeStr != "" {
			at, err := time.Parse(time.RFC3339, mtimeStr)
			if err != nil {
				return types.Result{Succeeded: false, Failed: true, Changed: false, Notes: []fmt.Stringer{types.SimpleNote("")}}, err
			}
			atime = at
		}
	}
	mkdirs := false
	{
		mkd, ok := f.params["makedirs"].(bool)
		if ok && mkd {
			mkdirs = true
		}
	}

	name = filepath.Clean(name)
	if name == "" {
		return types.Result{Succeeded: false, Failed: true}, types.ErrMissingName
	}
	if name == "/" {
		return types.Result{Succeeded: false, Failed: true}, types.ErrModifyRoot
	}
	_, err := os.Stat(name)
	if errors.Is(err, os.ErrNotExist) {
		needsMkdirs := false
		fileDir := filepath.Dir(name)
		_, dirErr := os.Stat(fileDir)
		if errors.Is(dirErr, os.ErrNotExist) {
			needsMkdirs = true
		}
		if !mkdirs && needsMkdirs {
			return types.Result{
				Succeeded: false, Failed: true,
				Changed: false, Notes: []fmt.Stringer{
					types.SimpleNote(fmt.Sprintf("filepath `%s` is missing and `makedirs` is false", fileDir)),
				},
			}, types.ErrPathNotFound
		}
		if needsMkdirs {
			if test {
				return types.Result{
					Succeeded: true, Failed: false,
					Changed: true, Notes: []fmt.Stringer{
						types.SimpleNote(fmt.Sprintf("file `%s` to be created with provided timestamps", name)),
					},
				}, nil
			}
			dirErr = os.MkdirAll(fileDir, 0o755)
			if dirErr != nil {
				return types.Result{
					Succeeded: false, Failed: true,
					Changed: false, Notes: []fmt.Stringer{
						types.SimpleNote(fmt.Sprintf("failed to create parent directory %s", fileDir)),
					},
				}, dirErr
			}
		}
		f, errCreate := os.Create(name)
		if errCreate != nil {
			return types.Result{
				Succeeded: false, Failed: true,
				Changed: false, Notes: []fmt.Stringer{
					types.SimpleNote(fmt.Sprintf("failed to create file %s", name)),
				},
			}, errCreate
		}
		f.Close()
	}
	if test {
		// TODO add notes for each changed timestamp if changed
		return types.Result{
			Succeeded: true, Failed: false,
			Changed: true, Notes: nil,
		}, nil
	}

	err = os.Chtimes(name, atime, mtime)
	if err != nil {
		return types.Result{Succeeded: false, Failed: true}, err
	}
	return types.Result{
		Succeeded: true, Failed: false,
		Changed: true, Notes: nil,
	}, nil
}

func (f File) contains(ctx context.Context, test bool) (types.Result, error) {
	// TODO
	// "name": "string", "text": "[]string",
	// "makedirs": "bool", "source": "string",
	// "source_hash": "string", "template": "bool",
	// "sources": "[]string", "source_hashes": "[]string",

	return f.undef()
}

func (f File) content(ctx context.Context, test bool) (types.Result, error) {
	// TODO
	// "name": "string", "text": "[]string",
	// "makedirs": "bool", "source": "string",
	// "source_hash": "string", "template": "bool",
	// "sources": "[]string", "source_hashes": "[]string",

	return f.undef()
}

func (f File) managed(ctx context.Context, test bool) (types.Result, error) {
	// TODO
	// "name": "string", "source": "string", "source_hash": "string", "user": "string",
	// "group": "string", "mode": "string", "attrs": "string", "template": "bool",
	// "makedirs": "bool", "dir_mode": "string", "replace": "bool", "backup": "string", "show_changes": "bool",
	// "create":          "bool",
	// "follow_symlinks": "bool", "skip_verify": "bool",

	return f.undef()
}

func (f File) symlink(ctx context.Context, test bool) (types.Result, error) {
	// "name": "string", "target": "string", "force": "bool", "backupname": "string",
	// "makedirs": "bool", "user": "string", "group": "string", "mode": "string",
	return f.undef()
}

add test apply path here

https://api.github.com/gogrlx/grlx/blob/b1e651999b27bbe0a0c4c42ab79a8835dce8357d/ingredients/file/file.go#L207

			}
		}
	}
	// chmod the directory to the named dirmode if it is defined
	// TODO add test apply path here
	{
		if val, ok := f.params["dir_mode"].(string); ok {
			d.dirMode = val
			modeVal, _ := strconv.ParseUint(d.dirMode, 8, 32)
			// "dir_mode": "string", "file_mode": "string"
			//"clean": "bool", "follow_symlinks": "bool", "force": "bool", "backupname": "string", "allow_symlink": "bool",
			err := os.Chmod(name, os.FileMode(modeVal))
			if err != nil {
				return types.Result{Succeeded: false, Failed: true}, err
			}
		}
	}
	// chmod the directory to the named dirmode if it is defined
	// TODO add test apply path here
	{
		if val, ok := f.params["file_mode"].(string); ok {
			d.fileMode = val
			modeVal, _ := strconv.ParseUint(d.fileMode, 8, 32)
			// "makedirs": "bool",
			//"clean": "bool", "follow_symlinks": "bool", "force": "bool", "backupname": "string", "allow_symlink": "bool",
			err := os.Chmod(name, os.FileMode(modeVal))
			if err != nil {
				return types.Result{Succeeded: false, Failed: true}, err
			}
		}
	} // recurse the file_mode if it is defined
	// TODO add test apply path here
	{
		if val, ok := f.params["group"].(string); ok {
			d.group = val
			group, lookupErr := user.LookupGroup(d.group)
			if lookupErr != nil {
				return types.Result{Succeeded: false, Failed: true}, lookupErr
			}
			gid, parseErr := strconv.ParseUint(group.Gid, 10, 32)
			if parseErr != nil {
				return types.Result{Succeeded: false, Failed: true}, parseErr
			}
			chownErr := os.Chown(name, -1, int(gid))
			if chownErr != nil {
				return types.Result{Succeeded: false, Failed: true}, chownErr
			}
			if val, ok := f.params["recurse"].(bool); ok && val {
				walkErr := filepath.WalkDir(name, func(path string, d fs.DirEntry, err error) error {
					return os.Chown(path, -1, int(gid))
				})
				if walkErr != nil {
					return types.Result{Succeeded: false, Failed: true}, walkErr
				}
			}
		}
	}

	return f.undef()
}

determine filename scheme for skip_verify downloads

https://api.github.com/gogrlx/grlx/blob/dc84e51242741f4ef9a64477401d0e1f8128f964/ingredients/file/file.go#L78

	}
}

func (f File) cached(ctx context.Context, test bool) (types.Result, error) {
	source, ok := f.params["source"].(string)
	if !ok || source == "" {
		// TODO join with an error type for missing params
		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("source not defined")
	}
	// TODO allow for skip_verify here
	hash, ok := f.params["hash"].(string)
	if !ok || hash == "" {
		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("hash not defined")
	}
	// TODO determine filename scheme for skip_verify downloads
	cacheDest := filepath.Join(config.CacheDir(), hash)
	fp, err := NewFileProvider(f.id, source, cacheDest, hash, f.params)
	if err != nil {
		return types.Result{Succeeded: false, Failed: true}, err
	}
	// TODO allow for skip_verify here
	valid, errVal := fp.Verify(ctx)
	if errVal != nil {
		return types.Result{Succeeded: false, Failed: true}, errVal
	}
	if !valid {
		if test {
			return types.Result{
				Succeeded: true, Failed: false,
				Changed: true, Changes: fp,
			}, nil
		} else {
			err = fp.Download(ctx)
			if err != nil {
				return types.Result{Succeeded: false, Failed: true}, err
			}
			return types.Result{Succeeded: true, Failed: false, Changed: true, Changes: fp}, nil
		}
	}
	return types.Result{Succeeded: true, Failed: false, Changed: false}, nil
}

func (f File) absent(ctx context.Context, test bool) (types.Result, error) {
	name, ok := f.params["name"].(string)
	if !ok {

make this properties nil check in other places

https://api.github.com/gogrlx/grlx/blob/88e17fa5077e6c70626f50be9c8a7f7e513edf7d/ingredients/file/local/provider.go#L54

}

func (lf LocalFile) Parse(id, source, destination, hash string, properties map[string]interface{}) (types.FileProvider, error) {
	// TODO make this properties nil check in other places
	if properties == nil {
		properties = make(map[string]interface{})
	}
	return LocalFile{ID: id, Source: source, Destination: destination, Hash: hash, Props: properties}, nil
}

allow for cancellation

https://api.github.com/gogrlx/grlx/blob/cd27501d919f074ddeefcc100c227d841545a258/cook/actuallycook.go#L88

				if !requisitesMet {
					continue
				}
				// mark the step as in progress
				t := completionMap[id]
				t.CompletionStatus = InProgress
				completionMap[id] = t
				// all requisites are met, so start the step in a goroutine
				go func(step types.Step) {
					defer wg.Done()
					// TODO here use the ingredient package to load and cook the step
					ingredient, err := ingredients.NewRecipeCooker(step.ID, step.Ingredient, step.Method, step.Properties)
					if err != nil {
						// TODO here broadcast the error on the bus
						completionChan <- StepCompletion{
							ID:               step.ID,
							CompletionStatus: Failed,
						}
					}
					// TODO allow for cancellation
					// TODO check for Test v Apply
					res, err := ingredient.Apply(context.Background())
					if err != nil {
						completionChan <- StepCompletion{
							ID:               step.ID,
							CompletionStatus: Failed,
						}
					}
					if res.Succeeded {
						completionChan <- StepCompletion{
							ID:               step.ID,
							CompletionStatus: Completed,
						}
					} else if res.Failed {
						completionChan <- StepCompletion{
							ID:               step.ID,
							CompletionStatus: Failed,
						}
					} else {
						completionChan <- StepCompletion{
							ID:               step.ID,
							CompletionStatus: Completed,
						}
					}
				}(stepMap[id])
			}

add test apply path here

https://api.github.com/gogrlx/grlx/blob/b1e651999b27bbe0a0c4c42ab79a8835dce8357d/ingredients/file/file.go#L152

		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("refusing to delete root")
	}
	d := dir{}
	// create the directory if it doesn't exist
	{
		// create the dir if "makeDirs" is true or not defined
		if val, ok := f.params["makedirs"].(bool); ok && val || !ok {
			d.makeDirs = true
			errCreate := os.MkdirAll(name, 0o755)
			if errCreate != nil {
				return types.Result{Succeeded: false, Failed: true}, errCreate
			}

		}
	}
	// chown the directory to the named user
	// TODO add test apply path here
	{
		if val, ok := f.params["user"].(string); ok {
			d.user = val

allow for skip_verify here

https://api.github.com/gogrlx/grlx/blob/dc84e51242741f4ef9a64477401d0e1f8128f964/ingredients/file/file.go#L73

	}
}

func (f File) cached(ctx context.Context, test bool) (types.Result, error) {
	source, ok := f.params["source"].(string)
	if !ok || source == "" {
		// TODO join with an error type for missing params
		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("source not defined")
	}
	// TODO allow for skip_verify here
	hash, ok := f.params["hash"].(string)
	if !ok || hash == "" {
		return types.Result{Succeeded: false, Failed: true}, fmt.Errorf("hash not defined")
	}
	// TODO determine filename scheme for skip_verify downloads
	cacheDest := filepath.Join(config.CacheDir(), hash)
	fp, err := NewFileProvider(f.id, source, cacheDest, hash, f.params)
	if err != nil {
		return types.Result{Succeeded: false, Failed: true}, err
	}
	// TODO allow for skip_verify here
	valid, errVal := fp.Verify(ctx)
	if errVal != nil {
		return types.Result{Succeeded: false, Failed: true}, errVal
	}
	if !valid {
		if test {
			return types.Result{
				Succeeded: true, Failed: false,
				Changed: true, Changes: fp,
			}, nil
		} else {
			err = fp.Download(ctx)
			if err != nil {
				return types.Result{Succeeded: false, Failed: true}, err
			}
			return types.Result{Succeeded: true, Failed: false, Changed: true, Changes: fp}, nil
		}
	}
	return types.Result{Succeeded: true, Failed: false, Changed: false}, nil
}

func (f File) absent(ctx context.Context, test bool) (types.Result, error) {
	name, ok := f.params["name"].(string)
	if !ok {

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.