Added include configuration support

A relatively straight forward change that adds "include" key support in
the main configuration file.

The config parser uses the list under this key to open the included
configuration files parse them as configs and append their jobs to the
main config file.
This commit is contained in:
Zeyad Tamimi 2025-02-22 11:51:05 -08:00
parent 6e125bae97
commit c92c141b51
5 changed files with 131 additions and 1 deletions

View File

@ -5,6 +5,8 @@ import (
"log/syslog"
"os"
"time"
"path/filepath"
pathpkg "path"
"github.com/pkg/errors"
"github.com/robfig/cron/v3"
@ -24,6 +26,7 @@ const (
type Config struct {
Jobs []JobEnum `yaml:"jobs,optional"`
Global *Global `yaml:"global,optional,fromdefaults"`
Include []string `yaml:"include,optional"`
}
func (c *Config) Job(name string) (*JobEnum, error) {
@ -657,6 +660,7 @@ var ConfigFileDefaultLocations = []string{
func ParseConfig(path string) (i *Config, err error) {
// Parse main configuration file
if path == "" {
// Try default locations
for _, l := range ConfigFileDefaultLocations {
@ -679,7 +683,71 @@ func ParseConfig(path string) (i *Config, err error) {
return
}
return ParseConfigBytes(bytes)
i, err = ParseConfigBytes(bytes)
if err != nil {
return nil, err
}
err = ExpandConfigInclude(path, i)
if err != nil {
return nil, err
}
return i, err
}
func ExpandConfigInclude(configPath string, config *Config) (err error) {
if config == nil {
return nil
}
var includeConfigPaths []string
for _, path := range config.Include {
if configPath[0] != '/' {
path = pathpkg.Join(pathpkg.Dir(configPath), path)
}
stat, statErr := os.Stat(path)
if statErr != nil {
return errors.Errorf("Could not open included configuration path: %s", path)
}
if stat.Mode().IsDir() {
directoryPaths, err := filepath.Glob(path + "/*.yml")
if err != nil {
return err
}
includeConfigPaths = append(includeConfigPaths, directoryPaths...)
} else if stat.Mode().IsRegular() {
if extention := filepath.Ext(path); extention != ".yml" {
return errors.Errorf("Only .yml files can be included: %s", path)
}
includeConfigPaths = append(includeConfigPaths, path)
} else {
return errors.Errorf("Only directories or .yml files can be included: %s", path)
}
}
for _, path := range includeConfigPaths {
var bytes []byte
if bytes, err = os.ReadFile(path); err != nil {
return err
}
includedConfig, err := ParseConfigBytes(bytes)
if err != nil {
return err
}
if len(includedConfig.Include) > 0 {
return errors.Errorf("Included configuration files cannot include other files: %s", path)
}
config.Jobs = append(config.Jobs, includedConfig.Jobs...)
}
return nil
}
func ParseConfigBytes(bytes []byte) (*Config, error) {
@ -687,6 +755,7 @@ func ParseConfigBytes(bytes []byte) (*Config, error) {
if err := yaml.UnmarshalStrict(bytes, &c); err != nil {
return nil, err
}
if c != nil {
return c, nil
}

View File

@ -0,0 +1,43 @@
package config
import (
"testing"
"github.com/kr/pretty"
"github.com/stretchr/testify/require"
)
func TestIncludeSingle(t *testing.T) {
path := "./samples/include.yml"
t.Run(path, func(t *testing.T) {
config, err := ParseConfig(path)
if err != nil {
t.Errorf("error parsing %s:\n%+v", path, err)
}
require.NotNil(t, config)
require.NotNil(t, config.Global)
require.NotEmpty(t, config.Jobs)
t.Logf("file: %s", path)
t.Log(pretty.Sprint(config))
})
}
func TestIncludeDirectory(t *testing.T) {
path := "./samples/include_directory.yml"
t.Run(path, func(t *testing.T) {
config, err := ParseConfig(path)
if err != nil {
t.Errorf("error parsing %s:\n%+v", path, err)
}
require.NotNil(t, config)
require.NotNil(t, config.Global)
require.NotEmpty(t, config.Jobs)
t.Logf("file: %s", path)
t.Log(pretty.Sprint(config))
})
}

View File

@ -0,0 +1,14 @@
jobs:
- name: snapjob
type: snap
filesystems: {
"tank/frequently_changed<": true,
}
snapshotting:
type: periodic
interval: 2m
prefix: zrepl_snapjob_
pruning:
keep:
- type: last_n
count: 60

View File

@ -0,0 +1,2 @@
include:
- ./snap.yml

View File

@ -0,0 +1,2 @@
include:
- ./include.d