Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New schedule section for configuration v2 #146

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,13 @@ func getScheduleJobs(c *config.Config, flags commandLineFlags) (schedule.Schedul
return nil, nil, nil, fmt.Errorf("cannot load profile '%s': %w", flags.name, err)
}

return schedule.NewSchedulerConfig(global), profile, profile.Schedules(), nil
schedules := profile.Schedules()
// schedulesConfig := make([]*config.ScheduleConfig, len(schedules))
// for index, schedule := range schedules {
// schedulesConfig[index] = schedule.GetScheduleConfig()
// }

return schedule.NewSchedulerConfig(global), profile, schedules, nil
}

func requireScheduleJobs(schedules []*config.Schedule, flags commandLineFlags) error {
Expand Down Expand Up @@ -585,13 +591,11 @@ func getRemovableScheduleJobs(c *config.Config, flags commandLineFlags) (schedul
}

func preRunSchedule(ctx *Context) error {
if len(ctx.request.arguments) < 1 {
return errors.New("run-schedule command expects one argument: schedule name")
if len(ctx.request.arguments) != 1 {
return errors.New("run-schedule command expects one argument (only): schedule name")
}
scheduleName := ctx.request.arguments[0]
// temporarily allow v2 configuration to run v1 schedules
// if ctx.config.GetVersion() < config.Version02
{
if ctx.config.GetVersion() < config.Version02 {
// schedule name is in the form "command@profile"
commandName, profileName, ok := strings.Cut(scheduleName, "@")
if !ok {
Expand Down
26 changes: 19 additions & 7 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -657,18 +657,30 @@ func (c *Config) GetSchedules() ([]*Schedule, error) {
if c.GetVersion() <= Version01 {
return c.getSchedulesV1()
}
return nil, nil

schedules := make([]*Schedule, 0)

if section := c.viper.Sub(constants.SectionConfigurationSchedules); section != nil {
for sectionKey := range section.AllSettings() {
schedule, err := c.getSchedule(sectionKey)
if err != nil {
continue
}
schedules = append(schedules, schedule.GetSchedule())
}
}
return schedules, nil
}

// GetScheduleSections returns a list of schedules
func (c *Config) GetScheduleSections() (schedules map[string]Schedule, err error) {
func (c *Config) GetScheduleSections() (schedules map[string]*ScheduleSection, err error) {
c.requireMinVersion(Version02)

schedules = map[string]Schedule{}
schedules = map[string]*ScheduleSection{}

if section := c.viper.Sub(constants.SectionConfigurationSchedules); section != nil {
for sectionKey := range section.AllSettings() {
var schedule Schedule
var schedule *ScheduleSection
schedule, err = c.getSchedule(sectionKey)
if err != nil {
break
Expand All @@ -680,9 +692,9 @@ func (c *Config) GetScheduleSections() (schedules map[string]Schedule, err error
return
}

func (c *Config) getSchedule(key string) (Schedule, error) {
schedule := Schedule{}
err := c.unmarshalKey(c.flatKey(constants.SectionConfigurationSchedules, key), &schedule)
func (c *Config) getSchedule(key string) (*ScheduleSection, error) {
schedule := NewScheduleSection(c, key)
err := c.unmarshalKey(c.flatKey(constants.SectionConfigurationSchedules, key), schedule)
if err != nil {
return schedule, err
}
Expand Down
89 changes: 89 additions & 0 deletions config/config_schedule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
require.NoError(t, err)
assert.NotEmpty(t, schedules)
assert.Equal(t, []string{"value"}, schedules["sname"].Profiles)
assert.Equal(t, []string{"daily"}, schedules["sname"].Schedules)

Check failure on line 50 in config/config_schedule_test.go

View workflow job for this annotation

GitHub Actions / Analyze (go)

schedules["sname"].Schedules undefined (type *ScheduleSection has no field or method Schedules)

Check failure on line 50 in config/config_schedule_test.go

View workflow job for this annotation

GitHub Actions / Build and test (1.22, ubuntu-latest)

schedules["sname"].Schedules undefined (type *ScheduleSection has no field or method Schedules)

Check failure on line 50 in config/config_schedule_test.go

View workflow job for this annotation

GitHub Actions / Build and test (1.22, macos-14)

schedules["sname"].Schedules undefined (type *ScheduleSection has no field or method Schedules)
})
}
}
Expand All @@ -56,3 +56,92 @@
c := newConfig("toml")
assert.Panics(t, func() { c.GetScheduleSections() })
}

func TestGetEmptySchedules(t *testing.T) {
fixtures := []testTemplate{
{FormatTOML, `version = "1"`},
{FormatJSON, `{"version": "1"}`},
{FormatYAML, `version: "1"`},
{FormatTOML, `version = "2"`},
{FormatJSON, `{"version": "2"}`},
{FormatYAML, `version: "2"`},
}

for _, testItem := range fixtures {
format := testItem.format
testConfig := testItem.config
t.Run(format, func(t *testing.T) {
c, err := Load(bytes.NewBufferString(testConfig), format)
require.NoError(t, err)

schedules, err := c.GetSchedules()
require.NoError(t, err)
assert.Empty(t, schedules)
})
}
}

func TestGetSchedules(t *testing.T) {
fixtures := []testTemplate{
{FormatTOML, `version = "1"
[profile1]
[profile1.backup]
schedule = "daily"
[profile2]
[profile2.backup]
schedule = "weekly"
`},
{FormatJSON, `{"version": "1", "profile1": {"backup": {"schedule": "daily"}}, "profile2": {"backup": {"schedule": "weekly"}}}`},
{FormatYAML, `version: "1"
profile1:
backup:
schedule: "daily"
profile2:
backup:
schedule: "weekly"
`},
{FormatTOML, `version = "2"
[schedules]
[schedules.schedule1]
profiles="profile1"
schedule="daily"
[schedules.schedule2]
profiles="profile2"
schedule="weekly"
`},
{FormatJSON, `{"version": "2", "schedules": {"schedule1": {"profiles": "profile1", "schedule": "daily"}, "schedule2": {"profiles": "profile2", "schedule": "weekly"}}}`},
{FormatYAML, `version: "2"
schedules:
schedule1:
profiles: profile1
schedule: daily
schedule2:
profiles: profile2
schedule: weekly
`},
}

for _, testItem := range fixtures {
format := testItem.format
testConfig := testItem.config
t.Run(format, func(t *testing.T) {
c, err := Load(bytes.NewBufferString(testConfig), format)
require.NoError(t, err)

schedules, err := c.GetSchedules()
require.NoError(t, err)
require.Len(t, schedules, 2)
for _, schedule := range schedules {
assertSchedule(t, schedule)
}
})
}
}

func assertSchedule(t *testing.T, schedule *Schedule) {
t.Helper()

assert.Len(t, schedule.Profiles, 1)
assert.Len(t, schedule.Schedules, 1)
assert.NotEmpty(t, schedule.ProfileName)

Check failure on line 146 in config/config_schedule_test.go

View workflow job for this annotation

GitHub Actions / Analyze (go)

schedule.ProfileName undefined (type *Schedule has no field or method ProfileName)

Check failure on line 146 in config/config_schedule_test.go

View workflow job for this annotation

GitHub Actions / Build and test (1.22, ubuntu-latest)

schedule.ProfileName undefined (type *Schedule has no field or method ProfileName)

Check failure on line 146 in config/config_schedule_test.go

View workflow job for this annotation

GitHub Actions / Build and test (1.22, macos-14)

schedule.ProfileName undefined (type *Schedule has no field or method ProfileName)
}
2 changes: 1 addition & 1 deletion config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ two:
}
}

func TestGetSchedules(t *testing.T) {
func TestGetSchedulesV1(t *testing.T) {
content := `---
profile:
backup:
Expand Down
11 changes: 11 additions & 0 deletions config/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,7 @@ var infoTypes struct {
mixins,
mixinUse,
profile,
schedule,
genericSection reflect.Type
genericSectionNames []string
}
Expand All @@ -596,6 +597,7 @@ func init() {
infoTypes.mixins = reflect.TypeOf(mixin{})
infoTypes.mixinUse = reflect.TypeOf(mixinUse{})
infoTypes.profile = reflect.TypeOf(profile)
infoTypes.schedule = reflect.TypeOf(ScheduleSection{})
infoTypes.genericSection = reflect.TypeOf(GenericSection{})
infoTypes.genericSectionNames = maps.Keys(profile.OtherSections)
}
Expand Down Expand Up @@ -623,6 +625,15 @@ func NewGroupInfo() NamedPropertySet {
return set
}

// NewScheduleInfo returns structural information on the "schedules" config v2 section
func NewScheduleInfo() NamedPropertySet {
return &namedPropertySet{
name: constants.SectionConfigurationSchedules,
description: "profile and group schedules",
propertySet: propertySetFromType(infoTypes.schedule),
}
}

// NewMixinsInfo returns structural information on the "mixins" config v2 section
func NewMixinsInfo() NamedPropertySet {
return &namedPropertySet{
Expand Down
23 changes: 17 additions & 6 deletions config/jsonschema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,16 @@ func schemaForGroups(version config.Version) SchemaType {
return object
}

func schemaForSchedules() SchemaType {
info := config.NewScheduleInfo()
object := newSchemaObject()
object.Description = info.Description()
schedules := schemaForPropertySet(info)
schedules.describe("schedule", "schedule declaration")
object.PatternProperties[matchAll] = schedules
return object
}

func schemaForGlobal() SchemaType {
return schemaForPropertySet(config.NewGlobalInfo())
}
Expand Down Expand Up @@ -433,12 +443,13 @@ func schemaForConfigV2(profileInfo config.ProfileInfo) (object *schemaObject) {
object = newSchemaObject()
object.Description = "resticprofile configuration v2"
object.Properties = map[string]SchemaType{
constants.SectionConfigurationGlobal: schemaForGlobal(),
constants.SectionConfigurationGroups: schemaForGroups(config.Version02),
constants.SectionConfigurationIncludes: schemaForIncludes(),
constants.SectionConfigurationMixins: schemaForMixins(),
constants.SectionConfigurationProfiles: schemaForProfile(profileInfo),
constants.ParameterVersion: schemaForConfigVersion(config.Version02),
constants.SectionConfigurationGlobal: schemaForGlobal(),
constants.SectionConfigurationGroups: schemaForGroups(config.Version02),
constants.SectionConfigurationIncludes: schemaForIncludes(),
constants.SectionConfigurationMixins: schemaForMixins(),
constants.SectionConfigurationProfiles: schemaForProfile(profileInfo),
constants.SectionConfigurationSchedules: schemaForSchedules(),
constants.ParameterVersion: schemaForConfigVersion(config.Version02),
}
object.Required = append(object.Required, constants.ParameterVersion)
{
Expand Down
2 changes: 1 addition & 1 deletion config/jsonschema/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func TestJsonSchemaValidation(t *testing.T) {
}

extensionMatcher := regexp.MustCompile(`\.(conf|toml|yaml|json)$`)
version2Matcher := regexp.MustCompile(`^version[:=\s]+2`)
version2Matcher := regexp.MustCompile(`version[:="'\s]+2`)
exclusions := regexp.MustCompile(`[\\/](rsyslogd\.conf|utf.*\.conf)$`)
testCount := 0

Expand Down
8 changes: 5 additions & 3 deletions config/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -820,12 +820,14 @@ func (p *Profile) SchedulableCommands() (commands []string) {
// Schedules returns a slice of Schedule for all the commands that have a schedule configuration
// Only v1 configuration have schedules inside the profile
func (p *Profile) Schedules() []*Schedule {
p.config.requireVersion(Version01)

// All SectionWithSchedule (backup, check, prune, etc)
sections := GetSectionsWith[Scheduling](p)
configs := make([]*Schedule, 0, len(sections))

for name, section := range sections {
if s := section.GetSchedule(); len(s.Schedule) > 0 {
for sectionName, section := range sections {
if s := section.GetSchedule(); s != nil && len(s.Schedule) > 0 {
env := util.NewDefaultEnvironment()

if len(s.ScheduleEnvCapture) > 0 {
Expand All @@ -852,7 +854,7 @@ func (p *Profile) Schedules() []*Schedule {
}

config := &Schedule{
CommandName: name,
CommandName: sectionName,
Group: "",
Profiles: []string{p.Name},
Schedules: s.Schedule,
Expand Down
2 changes: 1 addition & 1 deletion config/profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,7 @@ profile:
}
}

func TestSchedules(t *testing.T) {
func TestSchedulesV1(t *testing.T) {
util.ClearTempDir()
defer util.ClearTempDir()
logFile := path.Join(filepath.ToSlash(util.MustGetTempDir()), "rp.log")
Expand Down
44 changes: 44 additions & 0 deletions config/schedule_section.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package config

import (
"time"
)

// ScheduleSection contains the information from the schedule profile in the configuration file (v2+).
type ScheduleSection struct {
config *Config
name string
Group string `mapstructure:"group" description:"Group name to schedule (from groups section)"`
Profiles []string `mapstructure:"profiles" description:"List of profile name to schedule one after another"`
Command string `mapstructure:"run" default:"backup" examples:"backup;copy;check;forget;prune" description:"Command to schedule. Default is 'backup' if not specified"`
Schedule []string `mapstructure:"schedule" examples:"hourly;daily;weekly;monthly;10:00,14:00,18:00,22:00;Wed,Fri 17:48;*-*-15 02:45;Mon..Fri 00:30" description:"Set the times at which the scheduled command is run (times are specified in systemd timer format)"`
Permission string `mapstructure:"permission" default:"auto" enum:"auto;system;user;user_logged_on" description:"Specify whether the schedule runs with system or user privileges - see https://creativeprojects.github.io/resticprofile/schedules/configuration/"`
Log string `mapstructure:"log" examples:"/resticprofile.log;tcp://localhost:514" description:"Redirect the output into a log file or to syslog when running on schedule"`
Priority string `mapstructure:"priority" default:"background" enum:"background;standard" description:"Set the priority at which the schedule is run"`
LockMode string `mapstructure:"lock-mode" default:"default" enum:"default;fail;ignore" description:"Specify how locks are used when running on schedule - see https://creativeprojects.github.io/resticprofile/schedules/configuration/"`
LockWait time.Duration `mapstructure:"lock-wait" examples:"150s;15m;30m;45m;1h;2h30m" description:"Set the maximum time to wait for acquiring locks when running on schedule"`
EnvCapture []string `mapstructure:"capture-environment" show:"noshow" default:"RESTIC_*" description:"Set names (or glob expressions) of environment variables to capture during schedule creation. The captured environment is applied prior to \"profile.env\" when running the schedule. Whether capturing is supported depends on the type of scheduler being used (supported in \"systemd\" and \"launchd\")"`
IgnoreOnBattery bool `mapstructure:"ignore-on-battery" default:"false" description:"Don't schedule the start of this profile when running on battery"`
IgnoreOnBatteryLessThan int `mapstructure:"ignore-on-battery-less-than" default:"" description:"Don't schedule the start of this profile when running on battery, and the battery charge left is less than the value"`
}

// NewScheduleSection instantiates a new blank schedule
func NewScheduleSection(c *Config, name string) *ScheduleSection {
return &ScheduleSection{
name: name,
config: c,
}
}

func (s *ScheduleSection) GetSchedule() *Schedule {
// TODO: implement
return nil
}

func (s *ScheduleSection) Name() string {
if len(s.name) == 0 && len(s.Profiles) == 1 {
// configuration v1
return s.Profiles[0] + "-" + s.Command
}
return s.name
}
2 changes: 1 addition & 1 deletion flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func loadFlags(args []string) (*pflag.FlagSet, commandLineFlags, error) {
flagset.BoolVar(&flags.veryVerbose, "trace", constants.DefaultVerboseFlag, "display even more debugging information")
flagset.StringVarP(&flags.config, "config", "c", constants.DefaultConfigurationFile, "configuration file")
flagset.StringVarP(&flags.format, "format", "f", "", "file format of the configuration (default is to use the file extension)")
flagset.StringVarP(&flags.name, "name", "n", constants.DefaultProfileName, "profile name")
flagset.StringVarP(&flags.name, "name", "n", constants.DefaultProfileName, "profile (or schedule) name")
flagset.StringVarP(&flags.log, "log", "l", "", "logs to a target instead of the console")
flagset.BoolVar(&flags.dryRun, "dry-run", false, "display the restic commands instead of running them")
flagset.BoolVar(&flags.noLock, "no-lock", false, "skip profile lock file")
Expand Down
1 change: 0 additions & 1 deletion schedule/removeonly_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ func TestNewRemoveOnlyConfig(t *testing.T) {
assert.Equal(t, "", cfg.WorkingDirectory)
assert.Equal(t, "", cfg.Command)
assert.Empty(t, cfg.Arguments)
assert.Empty(t, cfg.Environment)
assert.Equal(t, "", cfg.Priority)
assert.Equal(t, "", cfg.ConfigFile)
{
Expand Down
Loading