diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..993d2f7 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,29 @@ +name: golangci-lint +on: + push: + branches: + - master + - main + pull_request: + +permissions: + contents: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: '1.21' + cache: false + # Install libgit2-dev 1.5 for git2go + - run: sudo apt-get install -qqy wget + - run: wget http://archive.ubuntu.com/ubuntu/pool/universe/libg/libgit2/libgit2-{1.5,dev}_1.5.1+ds-1ubuntu1_amd64.deb + - run: sudo apt-get install -qqy ./*.deb + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.54 diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..c7666d9 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,121 @@ +--- +linters: + enable-all: true + disable: + # Disabled to get codebase to pass the linter. + # We can enable these one at a time. + - cyclop + - forcetypeassert + - funlen + - gochecknoglobals + - gochecknoinits + - godox + - revive + - stylecheck + - wrapcheck + # Disabled permanently + - exhaustruct # structs may be uninitialized + - nlreturn # covered by wsl cuddle rules + - paralleltest # tests are acceptable in sequence + - goimports # conflicts with GCI + - depguard # manage using go.mod for now + - nonamedreturns # named returns are acceptable in short functions + # Deprecated + - exhaustivestruct + - scopelint + - interfacer + - maligned + - golint + - structcheck + - varcheck + - deadcode + - nosnakecase + - ifshort + +severity: + default-severity: major + +issues: + fast: false + max-issues-per-linter: 0 + max-same-issues: 0 + exclude-use-default: false + exclude-case-sensitive: true + exclude-rules: + - path: _test\.go + linters: + - gochecknoglobals + - errcheck + - wrapcheck + - gosec + - goerr113 + +linters-settings: + exhaustive: + default-signifies-exhaustive: true + + errcheck: + exclude-functions: + - (*os.File).Close + + gci: + sections: + - standard + - default + - prefix(github.com/getsolus/usysconf) + + gomnd: + ignored-numbers: ['2', '4', '8', '16', '32', '64', '10'] + + gosec: + excludes: [] + + govet: + enable-all: true + disable: + - fieldalignment # misalignment is accepted + + revive: + enable-all-rules: false + rules: # see https://github.com/mgechev/revive#recommended-configuration + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: error-return + - name: error-strings + - name: error-naming + - name: exported + - name: if-return + - name: increment-decrement + - name: var-naming + - name: var-declaration + - name: package-comments + - name: range + - name: receiver-naming + - name: time-naming + - name: unexported-return + - name: indent-error-flow + - name: errorf + - name: empty-block + - name: superfluous-else + - name: unused-parameter + - name: unreachable-code + - name: redefines-builtin-id + + stylecheck: + checks: [all] + + tagalign: + order: + - zero + - short + - long + - desc + - cmd + - arg + - aliases + - help + + varnamelen: + min-name-length: 1 diff --git a/Makefile b/Makefile index 19d1379..f67cbce 100644 --- a/Makefile +++ b/Makefile @@ -54,16 +54,7 @@ uninstall: $(RMDIR_IF_EMPTY) $(BINDIR) check: - $(GO) get -u golang.org/x/lint/golint - $(GO) get -u github.com/securego/gosec/cmd/gosec - $(GO) get -u honnef.co/go/tools/cmd/staticcheck - $(GO) get -u gitlab.com/opennota/check/cmd/aligncheck - $(GO) fmt -x ./... - $(GO) vet ./... - golint -set_exit_status `go list ./... | grep -v vendor` - gosec -exclude=G204 ./... - staticcheck ./... - aligncheck ./... + $(shell $(GO) env GOPATH)/bin/golangci-lint run $(GO) test -cover ./... vendor: check clean diff --git a/cli/cli.go b/cli/cli.go index 3c3cc6d..9ed5a66 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -23,22 +23,23 @@ var Version string = "unknown" // GlobalFlags contains the flags for all commands. type GlobalFlags struct { - Debug bool `short:"d" long:"debug" help:"Run in debug mode."` - Chroot bool `short:"c" long:"chroot" help:"Specify that command is being run from a chrooted environment."` - Live bool `short:"l" long:"live" help:"Specify that command is being run from a live medium."` - Version kong.VersionFlag `short:"v" long:"version" help:"Print version and exit."` + Debug bool `short:"d" long:"debug" help:"Run in debug mode."` + Chroot bool `short:"c" long:"chroot" help:"Specify that command is being run from a chrooted environment."` //nolint:lll + Live bool `short:"l" long:"live" help:"Specify that command is being run from a live medium."` + Version kong.VersionFlag `short:"v" long:"version" help:"Print version and exit."` } type arguments struct { GlobalFlags - Run run `cmd:"" aliases:"r" help:"Run specified trigger(s) to update the system configuration."` + Run run `cmd:"" aliases:"r" help:"Run specified trigger(s) to update the system configuration."` List list `cmd:"" aliases:"ls" help:"List available triggers to run (user-specific)."` - Graph graph `cmd:"" aliases:"g" help:"Print the dependencies for all available triggers."` + Graph graph `cmd:"" aliases:"g" help:"Print the dependencies for all available triggers."` } func Parse() (*kong.Context, GlobalFlags) { var args arguments ctx := kong.Parse(&args, kong.Vars{"version": Version}) + return ctx, args.GlobalFlags } diff --git a/cli/graph.go b/cli/graph.go index d7ff48c..edbd205 100644 --- a/cli/graph.go +++ b/cli/graph.go @@ -22,12 +22,14 @@ import ( type graph struct{} -// GraphDepsRun prints the usage for the requested command +// GraphDepsRun prints the usage for the requested command. func (g graph) Run(flags GlobalFlags) error { tm, err := config.LoadAll() if err != nil { return fmt.Errorf("failed to load triggers: %w", err) } + tm.Graph(flags.Chroot, flags.Live).Print() + return nil } diff --git a/cli/list.go b/cli/list.go index fc81632..9a41a9c 100644 --- a/cli/list.go +++ b/cli/list.go @@ -28,7 +28,9 @@ func (l list) Run(flags GlobalFlags) error { if err != nil { return fmt.Errorf("failed to load triggers: %w", err) } + slog.Info("Available triggers:") tm.Print(flags.Chroot, flags.Live) + return nil } diff --git a/cli/run.go b/cli/run.go index 4efe3ce..b51cd8a 100644 --- a/cli/run.go +++ b/cli/run.go @@ -26,18 +26,22 @@ import ( type run struct { Force bool `short:"f" long:"force" help:"Force run the configuration regardless if it should be skipped."` - DryRun bool `short:"n" long:"dry-run" help:"Test the configuration files without executing the specified binaries and arguments."` + DryRun bool `short:"n" long:"dry-run" help:"Test the configuration files without executing the specified binaries and arguments."` //nolint:lll Triggers []string `arg:"" help:"Names of the triggers to run." optional:""` } +var errNeedRoot = errors.New("you must have root privileges to run triggers") + func (r run) Run(flags GlobalFlags) error { if os.Geteuid() != 0 { - return errors.New("you must have root privileges to run triggers") + return errNeedRoot } + if util.IsChroot() { flags.Chroot = true } + if util.IsLive() { flags.Live = true } @@ -64,5 +68,6 @@ func (r run) Run(flags GlobalFlags) error { } // Run triggers. tm.Run(s, n) + return nil } diff --git a/config/load.go b/config/load.go index 1c5d48b..dd7cc1e 100644 --- a/config/load.go +++ b/config/load.go @@ -35,36 +35,47 @@ func Load(path string) (triggers.Map, error) { logger.Debug("Directory not found") return nil, nil } + return nil, fmt.Errorf("failed to read triggers: %w", err) } + tm := make(triggers.Map, len(entries)) + logger.Debug("Scanning directory") + for _, entry := range entries { if entry.IsDir() { continue } + name := entry.Name() if !strings.HasSuffix(name, ".toml") { continue } + t := triggers.Trigger{ Name: strings.TrimSuffix(name, ".toml"), Path: filepath.Clean(filepath.Join(path, name)), } logger.Debug("Trigger found", "name", t.Name) + err = t.Load(t.Path) if err != nil { return nil, err } + err = t.Validate() if err != nil { return nil, fmt.Errorf("failed to read %s from %s: %w", name, path, err) } + tm[t.Name] = t } + if len(tm) == 0 { logger.Debug("No triggers found") } + return tm, nil } @@ -76,6 +87,7 @@ func LoadAll() (triggers.Map, error) { if p, err := os.UserHomeDir(); err != nil { paths = append(paths, p) } + if os.Getuid() == 0 { uname := os.Getenv("SUDO_USER") if uname != "" && uname != "root" { @@ -89,13 +101,17 @@ func LoadAll() (triggers.Map, error) { } tm := make(triggers.Map) + for _, path := range paths { trig, err := Load(path) if err != nil { return nil, err } + tm.Merge(trig) } + slog.Info("Total triggers", "count", len(tm)) + return tm, nil } diff --git a/config/paths.go b/config/paths.go index b109a5f..1ee2325 100644 --- a/config/paths.go +++ b/config/paths.go @@ -15,8 +15,8 @@ package config var ( - // UsrDir is the path defined during build (Makefile) i.e. /usr/share/defaults/usysconf.d + // UsrDir is the path defined during build (Makefile) i.e. `/usr/share/defaults/usysconf.d`. UsrDir string - // SysDir is the path defined during build (Makefile) i.e. /etc/usysconf.d + // SysDir is the path defined during build (Makefile) i.e. `/etc/usysconf.d`. SysDir string ) diff --git a/deps/graph.go b/deps/graph.go index 50737f9..dace1cf 100644 --- a/deps/graph.go +++ b/deps/graph.go @@ -21,31 +21,33 @@ import ( "strings" ) -// Graph represents the dependencies shared between triggers +// Graph represents the dependencies shared between triggers. type Graph map[string][]string -// Insert sets the dependencies for a given trigger +// Insert sets the dependencies for a given trigger. func (g Graph) Insert(name string, deps []string) { g[name] = append(g[name], deps...) } -// Validate checks the graph for any potential issues +// Validate checks the graph for any potential issues. func (g Graph) Validate(triggers []string) { g.CheckCircular() g.CheckMissing(triggers) } -// CheckMissing checks for any missign triggers and prints warnings +// CheckMissing checks for any missign triggers and prints warnings. func (g Graph) CheckMissing(triggers []string) { for name, deps := range g { for _, dep := range deps { found := false + for _, trigger := range triggers { if dep == trigger { found = true break } } + if !found { slog.Warn("Dependency does not exist", "parent", name, "child", dep) } @@ -53,7 +55,7 @@ func (g Graph) CheckMissing(triggers []string) { } } -// CheckCircular checks for circular dependencies +// CheckCircular checks for circular dependencies. func (g Graph) CheckCircular() { var visited []string for name := range g { @@ -63,6 +65,7 @@ func (g Graph) CheckCircular() { if next == last { break } + found = found[1:] } // TODO: Return an error instead of panicking. @@ -81,6 +84,7 @@ func (g Graph) circular(name string, visited []string) (found []string) { return } } + if len(found) == 0 { found = g.circular(dep, visited) if len(found) != 0 { @@ -88,42 +92,50 @@ func (g Graph) circular(name string, visited []string) (found []string) { } } } + return } -// prune all references to things not in the list +// prune all references to things not in the list. func (g Graph) prune(names []string) { for k := range g { found := false + for _, name := range names { if k == name { found = true break } } + if !found { delete(g, k) } } + for k, deps := range g { var next []string + for _, dep := range deps { found := false + for _, name := range names { if dep == name { found = true break } } + if found { next = append(next, dep) } } + g[k] = next } } -// traverse performs a breadth-first traversal of a graph +// traverse performs a breadth-first traversal of a graph. func (g Graph) traverse(todo []string) (order, remaining []string) { for _, name := range todo { deps := g[name] @@ -133,51 +145,67 @@ func (g Graph) traverse(todo []string) (order, remaining []string) { remaining = append(remaining, name) } } + for _, name := range remaining { var next []string + for _, dep := range g[name] { found := false + for _, prev := range order { if dep == prev { found = true break } } + if !found { next = append(next, dep) } } + g[name] = next } + sort.Strings(order) - return + + return order, remaining } -// Resolve finds the ideal ordering for a list of triggers +// Resolve finds the ideal ordering for a list of triggers. func (g Graph) Resolve(todo []string) (order []string) { g.prune(todo) + var partial []string for len(todo) > 0 { partial, todo = g.traverse(todo) order = append(order, partial...) } + return } -// Print renders this graph to a "dot" format +// Print renders this graph to a "dot" format. +// +//nolint:forbidigo func (g Graph) Print() { - var names []string + names := make([]string, 0, len(g)) + for name := range g { names = append(names, name) } + sort.Strings(names) fmt.Println("digraph {") + for _, name := range names { deps := g[name] sort.Strings(deps) + for _, dep := range deps { fmt.Printf("\t\"%s\" -> \"%s\";\n", name, dep) } } + fmt.Println("}") } diff --git a/main.go b/main.go index f33fbae..cb70cc0 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,7 @@ func main() { if flags.Debug { logOpt.Level = slog.LevelDebug } + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, logOpt))) err := ctx.Run(flags) diff --git a/state/map.go b/state/map.go index 8122205..3a2b5fa 100644 --- a/state/map.go +++ b/state/map.go @@ -26,13 +26,15 @@ import ( "github.com/fxamacker/cbor/v2" ) -// Path is the location of the serialized system state directory +const stateDirPermissions = 0o750 + +// Path is the location of the serialized system state directory. var Path string -// Map contains a list files and their modification times +// Map contains a list files and their modification times. type Map map[string]time.Time -// Load reads in the state if it exists and deserializes it +// Load reads in the state if it exists and deserializes it. func Load() (Map, error) { m := make(Map) sFile, err := os.Open(filepath.Clean(Path)) @@ -58,86 +60,103 @@ func Load() (Map, error) { return m, nil } -// Save writes out the current state for future runs +// Save writes out the current state for future runs. func (m Map) Save() error { - if err := os.MkdirAll(filepath.Dir(Path), 0750); err != nil { + if err := os.MkdirAll(filepath.Dir(Path), stateDirPermissions); err != nil { return err } + sFile, err := os.Create(filepath.Clean(Path)) if err != nil { return err } + enc := cbor.NewEncoder(sFile) err = enc.Encode(m) _ = sFile.Close() + return err } -// Merge combines two Maps into one +// Merge combines two Maps into one. func (m Map) Merge(other Map) { for k, v := range other { m[k] = v } } -// Diff finds all of the Files which were modified or deleted between states +// Diff finds all of the Files which were modified or deleted between states. func (m Map) Diff(curr Map) Map { diff := make(Map) // Check for new or newer for currKey, currVal := range curr { found := false + for prevKey, prevVal := range m { if currKey == prevKey { found = true + if currVal.After(prevVal) { diff[currKey] = currVal } + break } } + if !found { diff[currKey] = currVal } } + return diff } -// Search finds all of the matching files in a Map +// Search finds all of the matching files in a Map. func (m Map) Search(paths []string) Map { match := make(Map) + for _, path := range paths { search := strings.ReplaceAll(path, "*", ".*") search = "^" + strings.ReplaceAll(search, string(filepath.Separator), "\\"+string(filepath.Separator)) + regex, err := regexp.Compile(search) if err != nil { slog.Warn("Could not convert to regex", "path", path) continue } + for k, v := range m { if regex.MatchString(k) { match[k] = v } } } + return match } -// Exclude removes keys from the Map if they match certain patterns +// Exclude removes keys from the Map if they match certain patterns. func (m Map) Exclude(patterns []string) Map { match := make(Map) for k, v := range m { match[k] = v } - var regexes []*regexp.Regexp + + regexes := make([]*regexp.Regexp, 0, len(patterns)) + for _, pattern := range patterns { exclude := strings.ReplaceAll(pattern, "*", ".*") + regex, err := regexp.Compile(exclude) if err != nil { slog.Warn("Could not convert to regex", "pattern", pattern) continue } + regexes = append(regexes, regex) } + for k := range m { for _, regex := range regexes { if regex.MatchString(k) { @@ -146,41 +165,47 @@ func (m Map) Exclude(patterns []string) Map { } } } + return match } -// IsEmpty checkes if the Map has nothing in it +// IsEmpty checkes if the Map has nothing in it. func (m Map) IsEmpty() bool { return len(m) == 0 } -// Strings gets a list of files from the keys +// Strings gets a list of files from the keys. func (m Map) Strings() (strs []string) { for k := range m { strs = append(strs, k) } + return } -// Scan goes over a set of paths and imports them and their contents to the map +// Scan goes over a set of paths and imports them and their contents to the map. func Scan(filters []string) (m Map, err error) { m = make(Map) + var matches []string + for _, filter := range filters { if matches, err = filepath.Glob(filter); err != nil { - err = fmt.Errorf("unable to glob path: %s", filter) - return + return m, fmt.Errorf("unable to glob path %q: %w", filter, err) } + if len(matches) == 0 { continue } + for _, match := range matches { err = filepath.Walk(filepath.Clean(match), func(path string, info os.FileInfo, err error) error { if err != nil { - err = fmt.Errorf("failed to check path: %s", path) - return err + return fmt.Errorf("failed to check path %q: %w", path, err) } + m[filepath.Join(path, info.Name())] = info.ModTime() + return nil }) if err != nil { @@ -188,9 +213,11 @@ func Scan(filters []string) (m Map, err error) { err = nil continue } - return + + return m, err } } } - return + + return m, err } diff --git a/triggers/bin.go b/triggers/bin.go index f7432ee..aa43569 100644 --- a/triggers/bin.go +++ b/triggers/bin.go @@ -31,9 +31,10 @@ type Bin struct { Replace *Replace `toml:"replace"` } -// ExecuteBins generates and runs all of the necesarry Bin commands +// ExecuteBins generates and runs all of the necesarry Bin commands. func (t *Trigger) ExecuteBins(s Scope) { var bins []Bin + var outputs []Output // Generate for _, b := range t.Bins { @@ -47,10 +48,11 @@ func (t *Trigger) ExecuteBins(s Scope) { outputs[i].Status = out.Status outputs[i].Message = out.Message } + t.Output = append(t.Output, outputs...) } -// Execute the binary from the confuration +// Execute the binary from the configuration. func (b *Bin) Execute(s Scope, env map[string]string) Output { out := Output{Status: Success} // if the norun flag is present do not execute the configuration @@ -58,8 +60,10 @@ func (b *Bin) Execute(s Scope, env map[string]string) Output { out.Status = Success return out } + // Create command - cmd := exec.Command(b.Bin, b.Args...) + cmd := exec.Command(b.Bin, b.Args...) //#nosec:G204 // the command from the config is tainted + // Setup environment for k, v := range env { cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v)) @@ -73,6 +77,7 @@ func (b *Bin) Execute(s Scope, env map[string]string) Output { out.Status = Failure out.Message = fmt.Sprintf("error executing '%s %v': %s\n%s", b.Bin, b.Args, err.Error(), buff.String()) } + return out } @@ -80,23 +85,25 @@ func (b *Bin) Execute(s Scope, env map[string]string) Output { // in the arguments and creating separate binaries to be executed. func (b Bin) FanOut() (nbins []Bin, outputs []Output) { phIndex := -1 + for i, arg := range b.Args { if arg == "***" { phIndex = i break } } + if phIndex == -1 { - nbins = append(nbins, b) - out := Output{Name: b.Task} - outputs = append(outputs, out) - return + return append(nbins, b), append(outputs, Output{Name: b.Task}) } + if b.Replace == nil { slog.Error("Placeholder found, but [bins.replaces] is missing") - return + return nil, nil } + slog.Debug("Replace string exists", "argument", phIndex) + paths := util.FilterPaths(b.Replace.Paths, b.Replace.Exclude) for _, path := range paths { out := Output{ @@ -107,5 +114,6 @@ func (b Bin) FanOut() (nbins []Bin, outputs []Output) { nbins = append(nbins, b) outputs = append(outputs, out) } - return + + return nbins, outputs } diff --git a/triggers/check.go b/triggers/check.go index 6289efd..a33f457 100644 --- a/triggers/check.go +++ b/triggers/check.go @@ -27,13 +27,15 @@ type Check struct { Paths []string `toml:"paths"` } -// CheckMatch will glob the paths and if the path does not exist in the system, an error is returned +// CheckMatch will glob the paths and if the path does not exist in the system, an error is returned. func (t *Trigger) CheckMatch() (m state.Map, ok bool) { ok = true + if t.Check == nil { slog.Debug("No check paths for trigger", "name", t.Name) return } + m, err := state.Scan(t.Check.Paths) if err != nil { out := Output{ @@ -42,7 +44,9 @@ func (t *Trigger) CheckMatch() (m state.Map, ok bool) { } t.Output = append(t.Output, out) ok = false + return } + return } diff --git a/triggers/config.go b/triggers/config.go index 5cb9f81..415bef3 100644 --- a/triggers/config.go +++ b/triggers/config.go @@ -15,37 +15,43 @@ package triggers import ( + "errors" "fmt" - "github.com/BurntSushi/toml" - "io/ioutil" "os" "path/filepath" + + "github.com/BurntSushi/toml" ) -// Load reads a Trigger configuration from a file and parses it +// Load reads a Trigger configuration from a file and parses it. func (t *Trigger) Load(path string) error { // Check if this is a valid file path if _, err := os.Stat(path); os.IsNotExist(err) { return err } // Read the configuration into the program - cfg, err := ioutil.ReadFile(filepath.Clean(path)) + cfg, err := os.ReadFile(filepath.Clean(path)) if err != nil { - return fmt.Errorf("unable to read config file located at %s", path) + return fmt.Errorf("unable to read config file located at %q: %w", path, err) } // Save the configuration into the content structure if err := toml.Unmarshal(cfg, t); err != nil { - return fmt.Errorf("unable to read config file located at %s due to %s", path, err.Error()) + return fmt.Errorf("unable to read config file located at %q: %w", path, err) } + return nil } -// Validate checks for errors in a Trigger configuration +// ErrNoBin is returned when a trigger does not contain a binary to execute. +var ErrNoBin = errors.New("triggers must contain at least one [[bin]]") + +// Validate checks for errors in a Trigger configuration. func (t *Trigger) Validate() error { // Verify that there is at least one binary to execute, otherwise there // is no need to continue if len(t.Bins) == 0 { - return fmt.Errorf("triggers must contain at least one [[bin]]") + return ErrNoBin } + return nil } diff --git a/triggers/deps.go b/triggers/deps.go index e9187e1..14abcf6 100644 --- a/triggers/deps.go +++ b/triggers/deps.go @@ -14,7 +14,7 @@ package triggers -// Deps contains a description of the dependencies between triggers +// Deps contains a description of the dependencies between triggers. type Deps struct { After []string `toml:"after"` } diff --git a/triggers/map.go b/triggers/map.go index 3802fc5..ef5cc9a 100644 --- a/triggers/map.go +++ b/triggers/map.go @@ -23,29 +23,37 @@ import ( "github.com/getsolus/usysconf/state" ) -// Map relates the name of trigger to its definition +// Map relates the name of trigger to its definition. type Map map[string]Trigger -// Merge combines two Maps by copying from right to left +// Merge combines two Maps by copying from right to left. func (tm Map) Merge(tm2 Map) { for k, v := range tm2 { tm[k] = v } } -// Print renders a Map in a human-readable format +// Print renders a Map in a human-readable format. +// +//nolint:forbidigo func (tm Map) Print(chroot, live bool) { - var keys []string + keys := make([]string, 0, len(tm)) max := 0 + for k := range tm { keys = append(keys, k) + if len(k) > max { max = len(k) } } + max += 4 + sort.Strings(keys) + f := fmt.Sprintf("%%%ds - %%s\n", max) + for _, key := range keys { t := tm[key] if t.Skip != nil { @@ -53,34 +61,40 @@ func (tm Map) Print(chroot, live bool) { continue } } + fmt.Printf(f, t.Name, t.Description) } + fmt.Println() } -// Graph generates a dependency graph +// Graph generates a dependency graph. func (tm Map) Graph(chroot, live bool) (g deps.Graph) { g = make(deps.Graph) - var names []string + names := make([]string, 0, len(tm)) + for _, t := range tm { if t.Skip != nil { if (t.Skip.Chroot && chroot) || (t.Skip.Live && live) { continue } } + if t.Deps != nil { g.Insert(t.Name, t.Deps.After) } + names = append(names, t.Name) } + g.Validate(names) + return } -// Run executes a list of triggers, where available +// Run executes a list of triggers, where available. func (tm Map) Run(s Scope, names []string) { prev, err := state.Load() - if err != nil { slog.Error("Failed to read state file", "reason", err) return @@ -101,6 +115,7 @@ func (tm Map) Run(s Scope, names []string) { // Run Trigger t.Run(s, prev, next) } + if !s.DryRun { // Save new State for next run if err := next.Save(); err != nil { diff --git a/triggers/remove.go b/triggers/remove.go index 7ca65f6..eb68233 100644 --- a/triggers/remove.go +++ b/triggers/remove.go @@ -28,24 +28,27 @@ type Remove struct { Exclude []string `toml:"exclude"` } -// Remove glob the paths and if it exists it will remove it from the system +// Remove glob the paths and if it exists it will remove it from the system. func (t *Trigger) Remove(s Scope) bool { if s.DryRun { slog.Debug("No Paths will be removed during a dry-run") } + if len(t.Removals) == 0 { slog.Debug("No Paths to remove") return true } + for _, remove := range t.Removals { if !t.removeOne(s, remove) { return false } } + return true } -// removeOne carries out removals for a single Remove entry +// removeOne carries out removals for a single Remove entry. func (t *Trigger) removeOne(s Scope, remove Remove) bool { matches, err := state.Scan(remove.Paths) if err != nil { @@ -54,22 +57,28 @@ func (t *Trigger) removeOne(s Scope, remove Remove) bool { Message: fmt.Sprintf("Failed to remove paths for '%s', reason: %s\n", t.Name, err), } t.Output = append(t.Output, out) + return false } + matches = matches.Exclude(remove.Exclude) for path := range matches { slog.Debug("Removing", "path", path) + if s.DryRun { continue } + if err := os.Remove(path); err != nil { out := Output{ Status: Failure, Message: fmt.Sprintf("Failed to remove path '%s', reason: %s\n", path, err), } t.Output = append(t.Output, out) + return false } } + return true } diff --git a/triggers/scope.go b/triggers/scope.go index c37f2f1..0410115 100644 --- a/triggers/scope.go +++ b/triggers/scope.go @@ -14,7 +14,7 @@ package triggers -// Scope sets limits of execution for a trigger +// Scope sets limits of execution for a trigger. type Scope struct { Chroot bool Debug bool diff --git a/triggers/skip.go b/triggers/skip.go index 3096849..8a576a8 100644 --- a/triggers/skip.go +++ b/triggers/skip.go @@ -16,18 +16,20 @@ package triggers import ( "fmt" + "github.com/getsolus/usysconf/state" ) -// Skip contains details for when the configuration will not be executed, due to existing paths, or possible flags passed. -// This supports globbing. +// Skip contains details for when the configuration will not be executed due to existing paths, +// or possible flags passed. This supports globbing. type Skip struct { Chroot bool `toml:"chroot,omitempty"` Live bool `toml:"live,omitempty"` Paths []string `toml:"paths"` } -// ShouldSkip will process the skip and check elements of the configuration and see if it should not be executed. +// ShouldSkip will process the skip and check elements of the configuration and see if +// it should not be executed. func (t *Trigger) ShouldSkip(s Scope, check, diff state.Map) bool { out := Output{ Status: Skipped, @@ -41,6 +43,7 @@ func (t *Trigger) ShouldSkip(s Scope, check, diff state.Map) bool { if s.Forced { return false } + if t.Skip == nil { return false } @@ -59,7 +62,9 @@ func (t *Trigger) ShouldSkip(s Scope, check, diff state.Map) bool { for k := range matches { out.Message = fmt.Sprintf("path '%s' found", k) t.Output = append(t.Output, out) + return true } + return false } diff --git a/triggers/trigger.go b/triggers/trigger.go index 2193334..8310016 100644 --- a/triggers/trigger.go +++ b/triggers/trigger.go @@ -22,9 +22,9 @@ import ( // Trigger contains all the information for a configuration to be executed and output to the user. type Trigger struct { - Name string - Path string - Output []Output + Name string `toml:"-"` + Path string `toml:"-"` + Output []Output `toml:"-"` Description string `toml:"description"` Check *Check `toml:"check,omitempty"` @@ -58,6 +58,7 @@ func (t *Trigger) Run(s Scope, prev, next state.Map) (ok bool) { t.ExecuteBins(s) FINISH: t.Finish(s) + return } diff --git a/util/chroot.go b/util/chroot.go index 5703236..03cf5b7 100644 --- a/util/chroot.go +++ b/util/chroot.go @@ -24,11 +24,14 @@ import ( "syscall" ) -// IsChroot detects if the current process is running in a chroot environment +// IsChroot detects if the current process is running in a chroot environment. func IsChroot() bool { var raw []byte + var root, chroot *syscall.Stat_t + var rootDir, chrootDir os.FileInfo + var pid int // Try to check for access to the root partition of PID1 (shell?) if _, err := os.Stat("/proc/1/root"); err != nil { @@ -41,32 +44,40 @@ func IsChroot() bool { slog.Warn("Failed to access", "path", "/proc/mounts", "reason", err) goto FALLBACK } + raw, err = io.ReadAll(mounts) if err != nil { slog.Warn("Failed to read", "path", "/proc/mounts", "reason", err) goto FALLBACK } + _ = mounts.Close() + if strings.Contains(string(raw), "overlay / overlay") { slog.Debug("Overlayfs for '/' found, assuming chroot") return true } FALLBACK: slog.Debug("Falling back to rigorous check for chroot") + rootDir, err = os.Stat("/") if err != nil { slog.Error("Failed to access", "path", "/", "reason", err) // TODO: Return error instead of panicking. panic("Failed to access") } + pid = os.Getpid() + chrootDir, err = os.Stat(filepath.Join("/", "proc", strconv.Itoa(pid), "root")) if err != nil { slog.Error("Failed to access", "path", "/", "reason", err) // TODO: Return error instead of panicking. panic("Failed to access") } + root = rootDir.Sys().(*syscall.Stat_t) chroot = chrootDir.Sys().(*syscall.Stat_t) + return root.Dev != chroot.Dev || root.Ino != chroot.Ino } diff --git a/util/filter_paths.go b/util/filter_paths.go index 3ea08f1..129ac6e 100644 --- a/util/filter_paths.go +++ b/util/filter_paths.go @@ -18,19 +18,22 @@ import ( "path/filepath" ) -// get a list of files that match the provided filters +// get a list of files that match the provided filters. func match(filters []string) (matches []string) { for _, filter := range filters { partial, err := filepath.Glob(filter) if err != nil { continue } + matches = append(matches, partial...) } + return } -// FilterPaths will process through globbed paths and remove any paths from the resulting slice if they are present in the excludes slice. +// FilterPaths will process through globbed paths and remove any paths from the resulting slice +// if they are present in the excludes slice. func FilterPaths(includes []string, excludes []string) (paths []string) { excludePaths := match(excludes) for _, includePath := range match(includes) { @@ -39,7 +42,9 @@ func FilterPaths(includes []string, excludes []string) (paths []string) { break } } + paths = append(paths, includePath) } + return } diff --git a/util/live.go b/util/live.go index 6674b03..5f553f3 100644 --- a/util/live.go +++ b/util/live.go @@ -19,18 +19,19 @@ import ( "os" ) -// IsLive checks is this process is running in a Live install +// IsLive checks is this process is running in a Live install. func IsLive() bool { var err error if _, err = os.Stat("/run/initramfs/livedev"); err == nil { slog.Debug("Live session detected") return true } + if os.IsNotExist(err) { return false } + slog.Error("Could not check for live session", "reason", err) // TODO: Return error instead of panicking. panic("Could not check for live session") - //return false }