diff --git a/commands.go b/commands.go index 81bff81f..7da12ebc 100644 --- a/commands.go +++ b/commands.go @@ -41,6 +41,7 @@ func init() { func getOwnCommands() []ownCommand { return []ownCommand{ + // commands that don't need loading the configuration { name: "help", description: "display help (use resticprofile help [command])", @@ -56,6 +57,29 @@ func getOwnCommands() []ownCommand { needConfiguration: false, flags: map[string]string{"-v, --verbose": "display detailed version information"}, }, + { + name: "random-key", + description: "generate a cryptographically secure random key to use as a restic keyfile", + action: randomKey, + needConfiguration: false, + hide: true, // replaced by the generate command + }, + { + name: "generate", + description: "generate resources such as random key, bash/zsh completion scripts, etc.", + longDescription: "The \"generate\" command is used to create various resources and print them to stdout", + action: generateCommand, + needConfiguration: false, + hide: false, + flags: map[string]string{ + "--random-key [size]": "generate a cryptographically secure random key to use as a restic keyfile (size defaults to 1024 when omitted)", + "--config-reference [--version 0.15] [template]": "generate a config file reference from a go template (defaults to the built-in markdown template when omitted)", + "--json-schema [--version 0.15] [v1|v2]": "generate a JSON schema that validates resticprofile configuration files in YAML or JSON format", + "--bash-completion": "generate a shell completion script for bash", + "--zsh-completion": "generate a shell completion script for zsh", + }, + }, + // commands that need the configuration { name: "profiles", description: "display profile names from the configuration file", @@ -70,13 +94,6 @@ func getOwnCommands() []ownCommand { action: showProfile, needConfiguration: true, }, - { - name: "random-key", - description: "generate a cryptographically secure random key to use as a restic keyfile", - action: randomKey, - needConfiguration: false, - hide: true, - }, { name: "schedule", description: "schedule jobs from a profile (or of all profiles)", @@ -108,19 +125,15 @@ func getOwnCommands() []ownCommand { flags: map[string]string{"--all": "display the status of all scheduled jobs of all profiles"}, }, { - name: "generate", - description: "generate resources such as random key, bash/zsh completion scripts, etc.", - longDescription: "The \"generate\" command is used to create various resources and print them to stdout", - action: generateCommand, - needConfiguration: false, + name: "run-schedule", + description: "runs a scheduled job. This command should only be called by the scheduling service", + longDescription: "The \"run-schedule\" command loads the scheduled job configuration from the name in parameter and runs the restic command with the arguments defined in the profile. The name in parameter is @ for the configuration file v1, and the schedule name for the configuration file v2+.", + pre: preRunSchedule, + action: runSchedule, + needConfiguration: true, hide: false, - flags: map[string]string{ - "--random-key [size]": "generate a cryptographically secure random key to use as a restic keyfile (size defaults to 1024 when omitted)", - "--config-reference [--version 0.15] [template]": "generate a config file reference from a go template (defaults to the built-in markdown template when omitted)", - "--json-schema [--version 0.15] [v1|v2]": "generate a JSON schema that validates resticprofile configuration files in YAML or JSON format", - "--bash-completion": "generate a shell completion script for bash", - "--zsh-completion": "generate a shell completion script for zsh", - }, + hideInCompletion: true, + noProfile: true, }, // hidden commands { @@ -328,10 +341,9 @@ func showProfile(output io.Writer, ctx commandContext) error { return nil } -func showSchedules(output io.Writer, schedulesConfig []*config.ScheduleConfig) { - for _, schedule := range schedulesConfig { - export := schedule.Export() - err := config.ShowStruct(output, export, "schedule "+export.Profiles[0]+"-"+export.Command) +func showSchedules(output io.Writer, schedules []*config.Schedule) { + for _, schedule := range schedules { + err := config.ShowStruct(output, schedule, "schedule "+schedule.CommandName+"@"+schedule.ProfileName) if err != nil { fmt.Fprintln(output, err) } @@ -408,7 +420,7 @@ func createSchedule(_ io.Writer, ctx commandContext) error { type profileJobs struct { scheduler schedule.SchedulerConfig profile string - jobs []*config.ScheduleConfig + jobs []*config.Schedule } allJobs := make([]profileJobs, 0, 1) @@ -516,7 +528,7 @@ func statusSchedule(w io.Writer, ctx commandContext) error { return nil } -func statusScheduleProfile(scheduler schedule.SchedulerConfig, profile *config.Profile, schedules []*config.ScheduleConfig, flags commandLineFlags) error { +func statusScheduleProfile(scheduler schedule.SchedulerConfig, profile *config.Profile, schedules []*config.Schedule, flags commandLineFlags) error { displayProfileDeprecationNotices(profile) err := statusJobs(schedule.NewHandler(scheduler), flags.name, schedules) @@ -526,7 +538,7 @@ func statusScheduleProfile(scheduler schedule.SchedulerConfig, profile *config.P return nil } -func getScheduleJobs(c *config.Config, flags commandLineFlags) (schedule.SchedulerConfig, *config.Profile, []*config.ScheduleConfig, error) { +func getScheduleJobs(c *config.Config, flags commandLineFlags) (schedule.SchedulerConfig, *config.Profile, []*config.Schedule, error) { global, err := c.GetGlobalSection() if err != nil { return nil, nil, nil, fmt.Errorf("cannot load global section: %w", err) @@ -543,14 +555,14 @@ func getScheduleJobs(c *config.Config, flags commandLineFlags) (schedule.Schedul return schedule.NewSchedulerConfig(global), profile, profile.Schedules(), nil } -func requireScheduleJobs(schedules []*config.ScheduleConfig, flags commandLineFlags) error { +func requireScheduleJobs(schedules []*config.Schedule, flags commandLineFlags) error { if len(schedules) == 0 { return fmt.Errorf("no schedule found for profile '%s'", flags.name) } return nil } -func getRemovableScheduleJobs(c *config.Config, flags commandLineFlags) (schedule.SchedulerConfig, *config.Profile, []*config.ScheduleConfig, error) { +func getRemovableScheduleJobs(c *config.Config, flags commandLineFlags) (schedule.SchedulerConfig, *config.Profile, []*config.Schedule, error) { scheduler, profile, schedules, err := getScheduleJobs(c, flags) if err != nil { return nil, nil, nil, err @@ -560,18 +572,62 @@ func getRemovableScheduleJobs(c *config.Config, flags commandLineFlags) (schedul for _, command := range profile.SchedulableCommands() { declared := false for _, s := range schedules { - if declared = s.SubTitle == command; declared { + if declared = s.CommandName == command; declared { break } } if !declared { - schedules = append(schedules, config.NewRemoveOnlyConfig(profile.Name, command)) + schedules = append(schedules, config.NewEmptySchedule(profile.Name, command)) } } return scheduler, profile, schedules, nil } +func preRunSchedule(ctx *Context) error { + if len(ctx.request.arguments) != 1 { + return errors.New("run-schedule command expects one argument (only): schedule name") + } + scheduleName := ctx.request.arguments[0] + if ctx.config.GetVersion() < config.Version02 { + // schedule name is in the form "command@profile" + commandName, profileName, ok := strings.Cut(scheduleName, "@") + if !ok { + return errors.New("the expected format of the schedule name is @") + } + ctx.request.profile = profileName + ctx.request.schedule = scheduleName + ctx.command = commandName + + // don't save the profile in the context now, it's only loaded but not prepared + profile, err := ctx.config.GetProfile(profileName) + if err != nil || profile == nil { + return fmt.Errorf("cannot load profile '%s': %w", profileName, err) + } + // get the list of all scheduled commands to find the current command + schedules := profile.Schedules() + for _, schedule := range schedules { + if schedule.CommandName == ctx.command { + clog.Debugf("preparing scheduled profile %q", ctx.request.schedule) + ctx.schedule = schedule + if len(schedule.Log) > 0 { + ctx.logTarget = schedule.Log + } + break + } + } + } + return nil +} + +func runSchedule(_ io.Writer, cmdCtx commandContext) error { + err := startProfileOrGroup(&cmdCtx.Context) + if err != nil { + return err + } + return nil +} + func testElevationCommand(_ io.Writer, ctx commandContext) error { if ctx.flags.isChild { client := remote.NewClient(ctx.flags.parentPort) diff --git a/commands_display.go b/commands_display.go index 9de1e154..19be14a9 100644 --- a/commands_display.go +++ b/commands_display.go @@ -114,9 +114,9 @@ func displayOwnCommandHelp(output io.Writer, commandName string, ctx commandCont commandFlags = "[command specific flags]" } out("Usage:\n") - out("\t%s %s\n\n", getCommonUsageHelpLine(command.name, command.needConfiguration), commandFlags) + out("\t%s %s\n\n", getCommonUsageHelpLine(command.name, command.needConfiguration && !command.noProfile), commandFlags) - var flags []string + var flags = make([]string, 0, len(command.flags)) for f, _ := range command.flags { flags = append(flags, f) } diff --git a/commands_test.go b/commands_test.go index bb56da20..36fb152c 100644 --- a/commands_test.go +++ b/commands_test.go @@ -64,14 +64,14 @@ schedule = "daily" declaredCount := 0 for _, jobConfig := range schedules { - scheduler := schedule.NewScheduler(schedule.NewHandler(schedule.SchedulerDefaultOS{}), jobConfig.Title) + scheduler := schedule.NewScheduler(schedule.NewHandler(schedule.SchedulerDefaultOS{}), jobConfig.ProfileName) defer func(s *schedule.Scheduler) { s.Close() }(scheduler) // Capture current ref to scheduler to be able to close it when function returns. - if jobConfig.SubTitle == "check" { - assert.False(t, scheduler.NewJob(jobConfig).RemoveOnly()) + if jobConfig.CommandName == "check" { + assert.False(t, scheduler.NewJob(scheduleToConfig(jobConfig)).RemoveOnly()) declaredCount++ } else { - assert.True(t, scheduler.NewJob(jobConfig).RemoveOnly()) + assert.True(t, scheduler.NewJob(scheduleToConfig(jobConfig)).RemoveOnly()) } } @@ -114,7 +114,7 @@ schedule = "daily" assert.NotNil(t, profile) assert.NotEmpty(t, schedules) assert.Len(t, schedules, 1) - assert.Equal(t, "check", schedules[0].SubTitle) + assert.Equal(t, "check", schedules[0].CommandName) } } @@ -271,3 +271,160 @@ func TestGenerateCommand(t *testing.T) { } }) } + +func TestShowSchedules(t *testing.T) { + buffer := &bytes.Buffer{} + schedules := []*config.Schedule{ + { + ProfileName: "default", + CommandName: "check", + Schedules: []string{"weekly"}, + }, + { + ProfileName: "default", + CommandName: "backup", + Schedules: []string{"daily"}, + }, + } + expected := strings.TrimSpace(` +schedule check@default: + profile: default + run: check + schedule: weekly + +schedule backup@default: + profile: default + run: backup + schedule: daily + +`) + showSchedules(buffer, schedules) + assert.Equal(t, expected, strings.TrimSpace(buffer.String())) +} + +func TestCreateScheduleWhenNoneAvailable(t *testing.T) { + // loads an (almost) empty config + cfg, err := config.Load(bytes.NewBufferString("[default]"), "toml") + assert.NoError(t, err) + + err = createSchedule(nil, commandContext{ + Context: Context{ + config: cfg, + flags: commandLineFlags{ + name: "default", + }, + }, + }) + assert.Error(t, err) +} + +func TestCreateScheduleAll(t *testing.T) { + // loads an (almost) empty config + // note that a default (or specific) profile is needed to load all schedules: + // TODO: we should be able to load them all without a default profile + cfg, err := config.Load(bytes.NewBufferString("[default]"), "toml") + assert.NoError(t, err) + + err = createSchedule(nil, commandContext{ + Context: Context{ + config: cfg, + flags: commandLineFlags{ + name: "default", + }, + request: Request{arguments: []string{"--all"}}, + }, + }) + assert.NoError(t, err) +} + +func TestPreRunScheduleNoScheduleName(t *testing.T) { + // loads an (almost) empty config + cfg, err := config.Load(bytes.NewBufferString("[default]"), "toml") + assert.NoError(t, err) + + err = preRunSchedule(&Context{ + config: cfg, + flags: commandLineFlags{ + name: "default", + }, + }) + assert.Error(t, err) + t.Log(err) +} + +func TestPreRunScheduleWrongScheduleName(t *testing.T) { + // loads an (almost) empty config + cfg, err := config.Load(bytes.NewBufferString("[default]"), "toml") + assert.NoError(t, err) + + err = preRunSchedule(&Context{ + request: Request{arguments: []string{"wrong"}}, + config: cfg, + flags: commandLineFlags{ + name: "default", + }, + }) + assert.Error(t, err) + t.Log(err) +} + +func TestPreRunScheduleProfileUnknown(t *testing.T) { + // loads an (almost) empty config + cfg, err := config.Load(bytes.NewBufferString("[default]"), "toml") + assert.NoError(t, err) + + err = preRunSchedule(&Context{ + request: Request{arguments: []string{"backup@profile"}}, + config: cfg, + }) + assert.ErrorIs(t, err, config.ErrNotFound) +} + +func TestRunScheduleNoScheduleName(t *testing.T) { + // loads an (almost) empty config + cfg, err := config.Load(bytes.NewBufferString("[default]"), "toml") + assert.NoError(t, err) + + err = runSchedule(nil, commandContext{ + Context: Context{ + config: cfg, + flags: commandLineFlags{ + name: "default", + }, + }, + }) + assert.Error(t, err) + t.Log(err) +} + +func TestRunScheduleWrongScheduleName(t *testing.T) { + // loads an (almost) empty config + cfg, err := config.Load(bytes.NewBufferString("[default]"), "toml") + assert.NoError(t, err) + + err = runSchedule(nil, commandContext{ + Context: Context{ + request: Request{arguments: []string{"wrong"}}, + config: cfg, + flags: commandLineFlags{ + name: "default", + }, + }, + }) + assert.Error(t, err) + t.Log(err) +} + +func TestRunScheduleProfileUnknown(t *testing.T) { + // loads an (almost) empty config + cfg, err := config.Load(bytes.NewBufferString("[default]"), "toml") + assert.NoError(t, err) + + err = runSchedule(nil, commandContext{ + Context: Context{ + request: Request{arguments: []string{"backup@profile"}}, + config: cfg, + }, + }) + assert.ErrorIs(t, err, ErrProfileNotFound) +} diff --git a/config/config.go b/config/config.go index 2bab3e1f..1fc19c37 100644 --- a/config/config.go +++ b/config/config.go @@ -651,8 +651,7 @@ func (c *Config) getProfilePath(key string) string { } // GetSchedules loads all schedules from the configuration. -// !!! Nothing is using this method yet !!! -func (c *Config) GetSchedules() ([]*ScheduleConfig, error) { +func (c *Config) GetSchedules() ([]*Schedule, error) { if c.GetVersion() <= Version01 { return c.getSchedulesV1() } diff --git a/config/config_schedule_test.go b/config/config_schedule_test.go index 0ae9d446..a3a66a82 100644 --- a/config/config_schedule_test.go +++ b/config/config_schedule_test.go @@ -47,7 +47,7 @@ schedules: require.NoError(t, err) assert.NotEmpty(t, schedules) assert.Equal(t, []string{"value"}, schedules["sname"].Profiles) - assert.Equal(t, []string{"daily"}, schedules["sname"].Schedule) + assert.Equal(t, []string{"daily"}, schedules["sname"].Schedules) }) } } diff --git a/config/config_v1.go b/config/config_v1.go index f7b917bb..807a95fd 100644 --- a/config/config_v1.go +++ b/config/config_v1.go @@ -119,12 +119,12 @@ func (c *Config) getProfileV1(profileKey string) (profile *Profile, err error) { } // getSchedulesV1 loads schedules from profiles -func (c *Config) getSchedulesV1() ([]*ScheduleConfig, error) { +func (c *Config) getSchedulesV1() ([]*Schedule, error) { profiles := c.GetProfileNames() if len(profiles) == 0 { return nil, nil } - schedules := []*ScheduleConfig{} + schedules := []*Schedule{} for _, profileName := range profiles { profile, err := c.GetProfile(profileName) if err != nil { diff --git a/config/profile.go b/config/profile.go index 79c7ec4c..6cb45323 100644 --- a/config/profile.go +++ b/config/profile.go @@ -807,11 +807,11 @@ func (p *Profile) SchedulableCommands() (commands []string) { return } -// Schedules returns a slice of ScheduleConfig that satisfy the schedule.Config interface -func (p *Profile) Schedules() []*ScheduleConfig { +// Schedules returns a slice of Schedule +func (p *Profile) Schedules() []*Schedule { // All SectionWithSchedule (backup, check, prune, etc) sections := GetSectionsWith[Scheduling](p) - configs := make([]*ScheduleConfig, 0, len(sections)) + configs := make([]*Schedule, 0, len(sections)) for name, section := range sections { if s := section.GetSchedule(); len(s.Schedule) > 0 { @@ -840,9 +840,11 @@ func (p *Profile) Schedules() []*ScheduleConfig { } } - config := &ScheduleConfig{ - Title: p.Name, - SubTitle: name, + config := &Schedule{ + ProfileName: p.Name, + CommandName: name, + Group: "", + Profiles: []string{}, Schedules: s.Schedule, Permission: s.SchedulePermission, Environment: env.Values(), @@ -850,11 +852,11 @@ func (p *Profile) Schedules() []*ScheduleConfig { LockMode: s.ScheduleLockMode, LockWait: s.ScheduleLockWait, Priority: s.SchedulePriority, - ConfigFile: p.config.configFile, IgnoreOnBattery: s.ScheduleIgnoreOnBattery, IgnoreOnBatteryLessThan: s.ScheduleIgnoreOnBatteryLessThan, AfterNetworkOnline: s.ScheduleAfterNetworkOnline, SystemdDropInFiles: p.SystemdDropInFiles, + ConfigFile: p.config.configFile, } if len(config.Log) > 0 { diff --git a/config/profile_test.go b/config/profile_test.go index ec042632..c82b4873 100644 --- a/config/profile_test.go +++ b/config/profile_test.go @@ -929,7 +929,7 @@ func TestSchedules(t *testing.T) { require.Len(t, config, 1) schedule := config[0] - assert.Equal(t, command, schedule.SubTitle) + assert.Equal(t, command, schedule.CommandName) assert.Equal(t, []string{"@hourly"}, schedule.Schedules) assert.Equal(t, path.Join(constants.TemporaryDirMarker, "rp.log"), schedule.Log) assert.Equal(t, map[string]string{ diff --git a/config/schedule.go b/config/schedule.go index 359436e5..4d0d73de 100644 --- a/config/schedule.go +++ b/config/schedule.go @@ -2,14 +2,38 @@ package config import "time" +// Schedule is an intermediary object between the configuration (v1, v2+) and the ScheduleConfig object used by the scheduler. +// The object is also used to display the scheduling configuration type Schedule struct { - Group string `mapstructure:"group"` - Profiles []string `mapstructure:"profiles"` - Command string `mapstructure:"run"` - Schedule []string `mapstructure:"schedule"` - Permission string `mapstructure:"permission"` - Log string `mapstructure:"log"` - Priority string `mapstructure:"priority"` - LockMode string `mapstructure:"lock-mode"` - LockWait time.Duration `mapstructure:"lock-wait"` + ProfileName string `mapstructure:"profile"` // v1 only + CommandName string `mapstructure:"run"` // v1 only + Group string `mapstructure:"group"` // v2+ only + Profiles []string `mapstructure:"profiles"` // v2+ only + Schedules []string `mapstructure:"schedule"` + Permission string `mapstructure:"permission"` + Log string `mapstructure:"log"` + Priority string `mapstructure:"priority"` + LockMode string `mapstructure:"lock-mode"` + LockWait time.Duration `mapstructure:"lock-wait"` + Environment []string `mapstructure:"environment"` + IgnoreOnBattery bool `mapstructure:"ignore-on-battery"` + IgnoreOnBatteryLessThan int `mapstructure:"ignore-on-battery-less-than"` + AfterNetworkOnline bool `mapstructure:"after-network-online"` + SystemdDropInFiles []string `mapstructure:"systemd-drop-in-files"` + ConfigFile string `show:"noshow"` + Flags map[string]string `show:"noshow"` +} + +func NewEmptySchedule(profileName, command string) *Schedule { + return &Schedule{ + ProfileName: profileName, + CommandName: command, + } +} + +func (s *Schedule) SetFlag(name, value string) { + if s.Flags == nil { + s.Flags = make(map[string]string) + } + s.Flags[name] = value } diff --git a/examples/dev.yaml b/examples/dev.yaml index 89e2d6c8..3adc02de 100644 --- a/examples/dev.yaml +++ b/examples/dev.yaml @@ -11,6 +11,7 @@ global: # legacy-arguments: true group-continue-on-error: true restic-lock-retry-after: "1m" + log: "_global.txt" groups: full-backup: @@ -113,6 +114,7 @@ self: schedule: - "*:00,30" schedule-permission: user + schedule-log: "_schedule-log.txt" run-after-fail: - "echo restic returned an error, command line = ${ERROR_COMMANDLINE}" diff --git a/main.go b/main.go index 8a467a8c..8cc8b365 100644 --- a/main.go +++ b/main.go @@ -544,6 +544,11 @@ func runProfile(ctx *Context) error { } profile.SetHost(hostname) + if ctx.request.schedule != "" { + // this is a scheduled profile + prepareScheduledProfile(ctx, profile) + } + // Catch CTR-C keypress, or other signal sent by a service manager (systemd) sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGABRT) @@ -574,6 +579,20 @@ func runProfile(ctx *Context) error { return nil } +func prepareScheduledProfile(ctx *Context, profile *config.Profile) error { + // get the list of all scheduled commands to find the current command + schedules := profile.Schedules() + for _, schedule := range schedules { + if schedule.CommandName == ctx.command { + // TODO: do we still need to do anything here? + // clog.Debugf("preparing scheduled profile %q", ctx.request.schedule) + ctx.schedule = schedule + break + } + } + return nil +} + // randomBool returns true for Heads and false for Tails func randomBool() bool { return rand.Int31n(10000) < 5000 diff --git a/config/schedule_config.go b/schedule/config.go similarity index 65% rename from config/schedule_config.go rename to schedule/config.go index 00ebb85c..222c9754 100644 --- a/config/schedule_config.go +++ b/schedule/config.go @@ -1,4 +1,4 @@ -package config +package schedule import ( "strings" @@ -19,48 +19,47 @@ const ( ScheduleLockModeIgnore = ScheduleLockMode(2) ) -// ScheduleConfig contains all information to schedule a profile command -type ScheduleConfig struct { - Title string - SubTitle string +// Config contains all information to schedule a profile command +type Config struct { + ProfileName string + CommandName string // restic command Schedules []string Permission string WorkingDirectory string - Command string + Command string // path to resticprofile executable Arguments []string Environment []string JobDescription string TimerDescription string - Priority string - Log string + Priority string // Priority is either "background" or "standard" LockMode string LockWait time.Duration ConfigFile string - Flags map[string]string - RemoveOnly bool + Flags map[string]string // flags added to the command line IgnoreOnBattery bool IgnoreOnBatteryLessThan int AfterNetworkOnline bool SystemdDropInFiles []string + removeOnly bool } // NewRemoveOnlyConfig creates a job config that may be used to call Job.Remove() on a scheduled job -func NewRemoveOnlyConfig(profileName, commandName string) *ScheduleConfig { - return &ScheduleConfig{ - Title: profileName, - SubTitle: commandName, - RemoveOnly: true, +func NewRemoveOnlyConfig(profileName, commandName string) *Config { + return &Config{ + ProfileName: profileName, + CommandName: commandName, + removeOnly: true, } } -func (s *ScheduleConfig) SetCommand(wd, command string, args []string) { +func (s *Config) SetCommand(wd, command string, args []string) { s.WorkingDirectory = wd s.Command = command s.Arguments = args } // Priority is either "background" or "standard" -func (s *ScheduleConfig) GetPriority() string { +func (s *Config) GetPriority() string { s.Priority = strings.ToLower(s.Priority) // default value for priority is "background" if s.Priority != constants.SchedulePriorityBackground && s.Priority != constants.SchedulePriorityStandard { @@ -69,7 +68,7 @@ func (s *ScheduleConfig) GetPriority() string { return s.Priority } -func (s *ScheduleConfig) GetLockMode() ScheduleLockMode { +func (s *Config) GetLockMode() ScheduleLockMode { switch s.LockMode { case constants.ScheduleLockModeOptionFail: return ScheduleLockModeFail @@ -80,14 +79,14 @@ func (s *ScheduleConfig) GetLockMode() ScheduleLockMode { } } -func (s *ScheduleConfig) GetLockWait() time.Duration { +func (s *Config) GetLockWait() time.Duration { if s.LockWait <= 2*time.Second { return 0 } return s.LockWait } -func (s *ScheduleConfig) GetFlag(name string) (string, bool) { +func (s *Config) GetFlag(name string) (string, bool) { if len(s.Flags) == 0 { return "", false } @@ -96,22 +95,9 @@ func (s *ScheduleConfig) GetFlag(name string) (string, bool) { return value, found } -func (s *ScheduleConfig) SetFlag(name, value string) { +func (s *Config) SetFlag(name, value string) { if s.Flags == nil { s.Flags = make(map[string]string) } s.Flags[name] = value } - -func (s *ScheduleConfig) Export() Schedule { - return Schedule{ - Profiles: []string{s.Title}, - Command: s.SubTitle, - Permission: s.Permission, - Log: s.Log, - Priority: s.Priority, - LockMode: s.LockMode, - LockWait: s.LockWait, - Schedule: s.Schedules, - } -} diff --git a/config/schedule_config_test.go b/schedule/config_test.go similarity index 67% rename from config/schedule_config_test.go rename to schedule/config_test.go index ea77525a..a2b06a02 100644 --- a/config/schedule_config_test.go +++ b/schedule/config_test.go @@ -1,4 +1,4 @@ -package config +package schedule import ( "testing" @@ -9,26 +9,30 @@ import ( ) func TestScheduleProperties(t *testing.T) { - schedule := ScheduleConfig{ - ConfigFile: "config", - Title: "profile", - SubTitle: "command name", - Schedules: []string{"1", "2", "3"}, - Permission: "admin", - WorkingDirectory: "home", - Command: "command", - Arguments: []string{"1", "2"}, - Environment: []string{"test=dev"}, - JobDescription: "job", - TimerDescription: "timer", - Log: "log.txt", - LockMode: "undefined", - LockWait: 1 * time.Minute, + schedule := Config{ + ProfileName: "profile", + CommandName: "command name", + Schedules: []string{"1", "2", "3"}, + Permission: "admin", + WorkingDirectory: "home", + Command: "command", + Arguments: []string{"1", "2"}, + Environment: []string{"test=dev"}, + JobDescription: "job", + TimerDescription: "timer", + Priority: "", + LockMode: "undefined", + LockWait: 1 * time.Minute, + ConfigFile: "config", + Flags: map[string]string{}, + removeOnly: false, + IgnoreOnBattery: false, + IgnoreOnBatteryLessThan: 0, } assert.Equal(t, "config", schedule.ConfigFile) - assert.Equal(t, "profile", schedule.Title) - assert.Equal(t, "command name", schedule.SubTitle) + assert.Equal(t, "profile", schedule.ProfileName) + assert.Equal(t, "command name", schedule.CommandName) assert.Equal(t, "job", schedule.JobDescription) assert.Equal(t, "timer", schedule.TimerDescription) assert.ElementsMatch(t, []string{"1", "2", "3"}, schedule.Schedules) @@ -38,13 +42,12 @@ func TestScheduleProperties(t *testing.T) { assert.ElementsMatch(t, []string{"1", "2"}, schedule.Arguments) assert.Equal(t, []string{"test=dev"}, schedule.Environment) assert.Equal(t, "background", schedule.GetPriority()) // default value - assert.Equal(t, "log.txt", schedule.Log) assert.Equal(t, ScheduleLockModeDefault, schedule.GetLockMode()) assert.Equal(t, 60*time.Second, schedule.GetLockWait()) } func TestLockModes(t *testing.T) { - tests := map[ScheduleLockMode]ScheduleConfig{ + tests := map[ScheduleLockMode]Config{ ScheduleLockModeDefault: {LockMode: ""}, ScheduleLockModeFail: {LockMode: constants.ScheduleLockModeOptionFail}, ScheduleLockModeIgnore: {LockMode: constants.ScheduleLockModeOptionIgnore}, @@ -55,7 +58,7 @@ func TestLockModes(t *testing.T) { } func TestLockWait(t *testing.T) { - tests := map[time.Duration]ScheduleConfig{ + tests := map[time.Duration]Config{ 0: {LockWait: 2 * time.Second}, // min lock wait is is >2 seconds 3 * time.Second: {LockWait: 3 * time.Second}, 120 * time.Hour: {LockWait: 120 * time.Hour}, @@ -66,28 +69,28 @@ func TestLockWait(t *testing.T) { } func TestStandardPriority(t *testing.T) { - schedule := ScheduleConfig{ + schedule := Config{ Priority: "standard", } assert.Equal(t, "standard", schedule.GetPriority()) } func TestCaseInsensitivePriority(t *testing.T) { - schedule := ScheduleConfig{ + schedule := Config{ Priority: "stANDard", } assert.Equal(t, "standard", schedule.GetPriority()) } func TestOtherPriority(t *testing.T) { - schedule := ScheduleConfig{ + schedule := Config{ Priority: "other", } assert.Equal(t, "background", schedule.GetPriority()) // default value } func TestScheduleFlags(t *testing.T) { - schedule := &ScheduleConfig{} + schedule := &Config{} flag, found := schedule.GetFlag("unit") assert.Empty(t, flag) diff --git a/schedule/handler.go b/schedule/handler.go index 4de17910..bf10c8c9 100644 --- a/schedule/handler.go +++ b/schedule/handler.go @@ -5,7 +5,6 @@ import ( "os/exec" "github.com/creativeprojects/resticprofile/calendar" - "github.com/creativeprojects/resticprofile/config" ) // Handler interface for the scheduling software available on the system @@ -16,9 +15,9 @@ type Handler interface { DisplayParsedSchedules(command string, events []*calendar.Event) DisplaySchedules(command string, schedules []string) error DisplayStatus(profileName string) error - CreateJob(job *config.ScheduleConfig, schedules []*calendar.Event, permission string) error - RemoveJob(job *config.ScheduleConfig, permission string) error - DisplayJobStatus(job *config.ScheduleConfig) error + CreateJob(job *Config, schedules []*calendar.Event, permission string) error + RemoveJob(job *Config, permission string) error + DisplayJobStatus(job *Config) error } func lookupBinary(name, binary string) error { diff --git a/schedule/handler_crond.go b/schedule/handler_crond.go index 07bdff70..bf7dd25e 100644 --- a/schedule/handler_crond.go +++ b/schedule/handler_crond.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/creativeprojects/resticprofile/calendar" - "github.com/creativeprojects/resticprofile/config" "github.com/creativeprojects/resticprofile/crond" ) @@ -55,14 +54,14 @@ func (h *HandlerCrond) DisplayStatus(profileName string) error { } // CreateJob is creating the crontab -func (h *HandlerCrond) CreateJob(job *config.ScheduleConfig, schedules []*calendar.Event, permission string) error { +func (h *HandlerCrond) CreateJob(job *Config, schedules []*calendar.Event, permission string) error { entries := make([]crond.Entry, len(schedules)) for i, event := range schedules { entries[i] = crond.NewEntry( event, job.ConfigFile, - job.Title, - job.SubTitle, + job.ProfileName, + job.CommandName, job.Command+" "+strings.Join(job.Arguments, " "), job.WorkingDirectory, ) @@ -75,13 +74,13 @@ func (h *HandlerCrond) CreateJob(job *config.ScheduleConfig, schedules []*calend return nil } -func (h *HandlerCrond) RemoveJob(job *config.ScheduleConfig, permission string) error { +func (h *HandlerCrond) RemoveJob(job *Config, permission string) error { entries := []crond.Entry{ crond.NewEntry( calendar.NewEvent(), job.ConfigFile, - job.Title, - job.SubTitle, + job.ProfileName, + job.CommandName, job.Command+" "+strings.Join(job.Arguments, " "), job.WorkingDirectory, ), @@ -98,7 +97,7 @@ func (h *HandlerCrond) RemoveJob(job *config.ScheduleConfig, permission string) } // DisplayJobStatus has nothing to display (crond doesn't provide running information) -func (h *HandlerCrond) DisplayJobStatus(job *config.ScheduleConfig) error { +func (h *HandlerCrond) DisplayJobStatus(job *Config) error { return nil } diff --git a/schedule/handler_darwin.go b/schedule/handler_darwin.go index 012518c1..5a4d0172 100644 --- a/schedule/handler_darwin.go +++ b/schedule/handler_darwin.go @@ -8,15 +8,12 @@ import ( "os/exec" "path" "regexp" - "slices" "sort" "strings" "text/tabwriter" "github.com/creativeprojects/resticprofile/calendar" - "github.com/creativeprojects/resticprofile/config" "github.com/creativeprojects/resticprofile/constants" - "github.com/creativeprojects/resticprofile/dial" "github.com/creativeprojects/resticprofile/term" "github.com/creativeprojects/resticprofile/util" "github.com/spf13/afero" @@ -109,7 +106,7 @@ func (h *HandlerLaunchd) DisplayStatus(profileName string) error { } // CreateJob creates a plist file and registers it with launchd -func (h *HandlerLaunchd) CreateJob(job *config.ScheduleConfig, schedules []*calendar.Event, permission string) error { +func (h *HandlerLaunchd) CreateJob(job *Config, schedules []*calendar.Event, permission string) error { filename, err := h.createPlistFile(h.getLaunchdJob(job, schedules), permission) if err != nil { if filename != "" { @@ -129,7 +126,7 @@ func (h *HandlerLaunchd) CreateJob(job *config.ScheduleConfig, schedules []*cale if _, noStart := job.GetFlag("no-start"); !noStart { // ask the user if he wants to start the service now - name := getJobName(job.Title, job.SubTitle) + name := getJobName(job.ProfileName, job.CommandName) message := ` By default, a macOS agent access is restricted. If you leave it to start in the background it's likely to fail. You have to start it manually the first time to accept the requests for access: @@ -152,24 +149,9 @@ Do you want to start it now?` return nil } -func (h *HandlerLaunchd) getLaunchdJob(job *config.ScheduleConfig, schedules []*calendar.Event) *LaunchdJob { - name := getJobName(job.Title, job.SubTitle) +func (h *HandlerLaunchd) getLaunchdJob(job *Config, schedules []*calendar.Event) *LaunchdJob { + name := getJobName(job.ProfileName, job.CommandName) args := job.Arguments - logfile := job.Log - - // if logfile is an url or in the volatile temp folder, we can't use it as a target for LaunchJob - if dial.IsURL(logfile) || strings.HasPrefix(logfile, constants.TemporaryDirMarker) { - logfile = "" - } else { - // removing the "--log" flag if we can use LaunchdJob's logging facility - logIndex := slices.Index(args, "--log") - if logIndex > -1 && len(args) >= logIndex+2 && args[logIndex+1] == logfile { - args = slices.Delete(args, logIndex, logIndex+2) - } - } - if logfile == "" { - logfile = name + ".log" - } // Format schedule env, adding PATH if not yet provided by the schedule config env := util.NewDefaultEnvironment(job.Environment...) @@ -188,8 +170,6 @@ func (h *HandlerLaunchd) getLaunchdJob(job *config.ScheduleConfig, schedules []* Label: name, Program: job.Command, ProgramArguments: append([]string{job.Command, "--no-prio"}, args...), - StandardOutPath: logfile, - StandardErrorPath: logfile, WorkingDirectory: job.WorkingDirectory, StartCalendarInterval: getCalendarIntervalsFromSchedules(schedules), EnvironmentVariables: env.ValuesAsMap(), @@ -221,8 +201,8 @@ func (h *HandlerLaunchd) createPlistFile(launchdJob *LaunchdJob, permission stri } // RemoveJob stops and unloads the agent from launchd, then removes the configuration file -func (h *HandlerLaunchd) RemoveJob(job *config.ScheduleConfig, permission string) error { - name := getJobName(job.Title, job.SubTitle) +func (h *HandlerLaunchd) RemoveJob(job *Config, permission string) error { + name := getJobName(job.ProfileName, job.CommandName) filename, err := getFilename(name, permission) if err != nil { return err @@ -254,13 +234,13 @@ func (h *HandlerLaunchd) RemoveJob(job *config.ScheduleConfig, permission string return nil } -func (h *HandlerLaunchd) DisplayJobStatus(job *config.ScheduleConfig) error { +func (h *HandlerLaunchd) DisplayJobStatus(job *Config) error { permission := getSchedulePermission(job.Permission) ok := checkPermission(permission) if !ok { return permissionError("view") } - cmd := exec.Command(launchctlBin, launchdList, getJobName(job.Title, job.SubTitle)) + cmd := exec.Command(launchctlBin, launchdList, getJobName(job.ProfileName, job.CommandName)) output, err := cmd.Output() if cmd.ProcessState.ExitCode() == codeServiceNotFound { return ErrorServiceNotFound diff --git a/schedule/handler_darwin_test.go b/schedule/handler_darwin_test.go index 25ade997..16d1767e 100644 --- a/schedule/handler_darwin_test.go +++ b/schedule/handler_darwin_test.go @@ -6,12 +6,9 @@ import ( "bytes" "fmt" "os" - "path" "testing" "github.com/creativeprojects/resticprofile/calendar" - "github.com/creativeprojects/resticprofile/config" - "github.com/creativeprojects/resticprofile/constants" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -130,40 +127,6 @@ func TestHandlerInstanceLaunchd(t *testing.T) { assert.NotNil(t, handler) } -func TestLaunchdJobLog(t *testing.T) { - fixtures := []struct { - log string - expected string - noLogArg bool - }{ - {log: path.Join(constants.TemporaryDirMarker, "file"), expected: "local.resticprofile.profile.backup.log"}, - {log: "", expected: "local.resticprofile.profile.backup.log", noLogArg: true}, - {log: "udp://localhost:123", expected: "local.resticprofile.profile.backup.log"}, - {log: "tcp://127.0.0.1:123", expected: "local.resticprofile.profile.backup.log"}, - {log: "other file", expected: "other file", noLogArg: true}, - } - - for _, fixture := range fixtures { - t.Run(fixture.log, func(t *testing.T) { - handler := NewHandler(SchedulerLaunchd{}) - args := []string{"--log", fixture.log} - cfg := &config.ScheduleConfig{ - Title: "profile", - SubTitle: "backup", - Log: fixture.log, - Arguments: args, - } - launchdJob := handler.getLaunchdJob(cfg, []*calendar.Event{}) - assert.Equal(t, fixture.expected, launchdJob.StandardOutPath) - if fixture.noLogArg { - assert.NotSubset(t, launchdJob.ProgramArguments, args) - } else { - assert.Subset(t, launchdJob.ProgramArguments, args) - } - }) - } -} - func TestLaunchdJobPreservesEnv(t *testing.T) { pathEnv := os.Getenv("PATH") fixtures := []struct { @@ -178,7 +141,7 @@ func TestLaunchdJobPreservesEnv(t *testing.T) { for i, fixture := range fixtures { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { handler := NewHandler(SchedulerLaunchd{}) - cfg := &config.ScheduleConfig{Title: "t", SubTitle: "s", Environment: fixture.environment} + cfg := &Config{ProfileName: "t", CommandName: "s", Environment: fixture.environment} launchdJob := handler.getLaunchdJob(cfg, []*calendar.Event{}) assert.Equal(t, fixture.expected, launchdJob.EnvironmentVariables) }) diff --git a/schedule/handler_fake_test.go b/schedule/handler_fake_test.go index 30eceba9..cebb64d6 100644 --- a/schedule/handler_fake_test.go +++ b/schedule/handler_fake_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/creativeprojects/resticprofile/calendar" - "github.com/creativeprojects/resticprofile/config" ) const ( @@ -19,9 +18,9 @@ type mockHandler struct { displayParsedSchedules func(command string, events []*calendar.Event) displaySchedules func(command string, schedules []string) error displayStatus func(profileName string) error - createJob func(job *config.ScheduleConfig, schedules []*calendar.Event, permission string) error - removeJob func(job *config.ScheduleConfig, permission string) error - displayJobStatus func(job *config.ScheduleConfig) error + createJob func(job *Config, schedules []*calendar.Event, permission string) error + removeJob func(job *Config, permission string) error + displayJobStatus func(job *Config) error } func (h mockHandler) Init() error { @@ -66,21 +65,21 @@ func (h mockHandler) DisplayStatus(profileName string) error { return h.displayStatus(profileName) } -func (h mockHandler) CreateJob(job *config.ScheduleConfig, schedules []*calendar.Event, permission string) error { +func (h mockHandler) CreateJob(job *Config, schedules []*calendar.Event, permission string) error { if h.createJob == nil { h.t.Fatal(errorMethodNotRegistered) } return h.createJob(job, schedules, permission) } -func (h mockHandler) RemoveJob(job *config.ScheduleConfig, permission string) error { +func (h mockHandler) RemoveJob(job *Config, permission string) error { if h.removeJob == nil { h.t.Fatal(errorMethodNotRegistered) } return h.removeJob(job, permission) } -func (h mockHandler) DisplayJobStatus(job *config.ScheduleConfig) error { +func (h mockHandler) DisplayJobStatus(job *Config) error { if h.displayJobStatus == nil { h.t.Fatal(errorMethodNotRegistered) } diff --git a/schedule/handler_systemd.go b/schedule/handler_systemd.go index b49ba1bd..669db7b0 100644 --- a/schedule/handler_systemd.go +++ b/schedule/handler_systemd.go @@ -11,7 +11,6 @@ import ( "github.com/creativeprojects/clog" "github.com/creativeprojects/resticprofile/calendar" - "github.com/creativeprojects/resticprofile/config" "github.com/creativeprojects/resticprofile/constants" "github.com/creativeprojects/resticprofile/systemd" "github.com/creativeprojects/resticprofile/term" @@ -100,7 +99,7 @@ func (h *HandlerSystemd) DisplayStatus(profileName string) error { } // CreateJob is creating the systemd unit and activating it -func (h *HandlerSystemd) CreateJob(job *config.ScheduleConfig, schedules []*calendar.Event, permission string) error { +func (h *HandlerSystemd) CreateJob(job *Config, schedules []*calendar.Event, permission string) error { unitType := systemd.UserUnit if os.Geteuid() == 0 { // user has sudoed already @@ -115,8 +114,8 @@ func (h *HandlerSystemd) CreateJob(job *config.ScheduleConfig, schedules []*cale CommandLine: job.Command + " --no-prio " + strings.Join(job.Arguments, " "), Environment: job.Environment, WorkingDirectory: job.WorkingDirectory, - Title: job.Title, - SubTitle: job.SubTitle, + Title: job.ProfileName, + SubTitle: job.CommandName, JobDescription: job.JobDescription, TimerDescription: job.TimerDescription, Schedules: job.Schedules, @@ -142,7 +141,7 @@ func (h *HandlerSystemd) CreateJob(job *config.ScheduleConfig, schedules []*cale } } - timerName := systemd.GetTimerFile(job.Title, job.SubTitle) + timerName := systemd.GetTimerFile(job.ProfileName, job.CommandName) // enable the job err = runSystemctlCommand(timerName, systemctlEnable, unitType, false) @@ -165,23 +164,23 @@ func (h *HandlerSystemd) CreateJob(job *config.ScheduleConfig, schedules []*cale } // RemoveJob is disabling the systemd unit and deleting the timer and service files -func (h *HandlerSystemd) RemoveJob(job *config.ScheduleConfig, permission string) error { +func (h *HandlerSystemd) RemoveJob(job *Config, permission string) error { unitType := systemd.UserUnit if os.Geteuid() == 0 { // user has sudoed already unitType = systemd.SystemUnit } var err error - timerFile := systemd.GetTimerFile(job.Title, job.SubTitle) + timerFile := systemd.GetTimerFile(job.ProfileName, job.CommandName) // stop the job - err = runSystemctlCommand(timerFile, systemctlStop, unitType, job.RemoveOnly) + err = runSystemctlCommand(timerFile, systemctlStop, unitType, job.removeOnly) if err != nil { return err } // disable the job - err = runSystemctlCommand(timerFile, systemctlDisable, unitType, job.RemoveOnly) + err = runSystemctlCommand(timerFile, systemctlDisable, unitType, job.removeOnly) if err != nil { return err } @@ -199,13 +198,13 @@ func (h *HandlerSystemd) RemoveJob(job *config.ScheduleConfig, permission string return nil } - serviceFile := systemd.GetServiceFile(job.Title, job.SubTitle) + serviceFile := systemd.GetServiceFile(job.ProfileName, job.CommandName) err = os.Remove(path.Join(systemdPath, serviceFile)) if err != nil { return nil } - dropInDir := systemd.GetServiceFileDropInDir(job.Title, job.SubTitle) + dropInDir := systemd.GetServiceFileDropInDir(job.ProfileName, job.CommandName) err = os.RemoveAll(path.Join(systemdPath, dropInDir)) if err != nil { return nil @@ -215,8 +214,8 @@ func (h *HandlerSystemd) RemoveJob(job *config.ScheduleConfig, permission string } // DisplayJobStatus displays information of a systemd service/timer -func (h *HandlerSystemd) DisplayJobStatus(job *config.ScheduleConfig) error { - timerName := systemd.GetTimerFile(job.Title, job.SubTitle) +func (h *HandlerSystemd) DisplayJobStatus(job *Config) error { + timerName := systemd.GetTimerFile(job.ProfileName, job.CommandName) permission := getSchedulePermission(job.Permission) if permission == constants.SchedulePermissionSystem { err := runJournalCtlCommand(timerName, systemd.SystemUnit) diff --git a/schedule/handler_windows.go b/schedule/handler_windows.go index f074a211..09d71790 100644 --- a/schedule/handler_windows.go +++ b/schedule/handler_windows.go @@ -6,7 +6,6 @@ import ( "errors" "github.com/creativeprojects/resticprofile/calendar" - "github.com/creativeprojects/resticprofile/config" "github.com/creativeprojects/resticprofile/constants" "github.com/creativeprojects/resticprofile/schtasks" ) @@ -54,7 +53,7 @@ func (h *HandlerWindows) DisplayStatus(profileName string) error { } // CreateJob is creating the task scheduler job. -func (h *HandlerWindows) CreateJob(job *config.ScheduleConfig, schedules []*calendar.Event, permission string) error { +func (h *HandlerWindows) CreateJob(job *Config, schedules []*calendar.Event, permission string) error { // default permission will be system perm := schtasks.SystemAccount if permission == constants.SchedulePermissionUser { @@ -62,7 +61,15 @@ func (h *HandlerWindows) CreateJob(job *config.ScheduleConfig, schedules []*cale } else if permission == constants.SchedulePermissionUserLoggedOn || permission == constants.SchedulePermissionUserLoggedIn { perm = schtasks.UserLoggedOnAccount } - err := schtasks.Create(job, schedules, perm) + jobConfig := &schtasks.Config{ + ProfileName: job.ProfileName, + CommandName: job.CommandName, + Command: job.Command, + Arguments: job.Arguments, + WorkingDirectory: job.WorkingDirectory, + JobDescription: job.JobDescription, + } + err := schtasks.Create(jobConfig, schedules, perm) if err != nil { return err } @@ -70,8 +77,8 @@ func (h *HandlerWindows) CreateJob(job *config.ScheduleConfig, schedules []*cale } // RemoveJob is deleting the task scheduler job -func (h *HandlerWindows) RemoveJob(job *config.ScheduleConfig, permission string) error { - err := schtasks.Delete(job.Title, job.SubTitle) +func (h *HandlerWindows) RemoveJob(job *Config, permission string) error { + err := schtasks.Delete(job.ProfileName, job.CommandName) if err != nil { if errors.Is(err, schtasks.ErrorNotRegistered) { return ErrorServiceNotFound @@ -82,8 +89,8 @@ func (h *HandlerWindows) RemoveJob(job *config.ScheduleConfig, permission string } // DisplayStatus display some information about the task scheduler job -func (h *HandlerWindows) DisplayJobStatus(job *config.ScheduleConfig) error { - err := schtasks.Status(job.Title, job.SubTitle) +func (h *HandlerWindows) DisplayJobStatus(job *Config) error { + err := schtasks.Status(job.ProfileName, job.CommandName) if err != nil { if errors.Is(err, schtasks.ErrorNotRegistered) { return ErrorServiceNotFound diff --git a/schedule/job.go b/schedule/job.go index 5d6fe5a1..97dbe599 100644 --- a/schedule/job.go +++ b/schedule/job.go @@ -2,26 +2,15 @@ package schedule import ( "errors" - - "github.com/creativeprojects/resticprofile/config" ) // -// Job: common code for all systems +// Job: common code for all scheduling systems // -// SchedulerJob interface -type SchedulerJob interface { - Accessible() bool - Create() error - Remove() error - RemoveOnly() bool - Status() error -} - // Job scheduler type Job struct { - config *config.ScheduleConfig + config *Config handler Handler } @@ -51,9 +40,9 @@ func (j *Job) Create() error { } if len(schedules) > 0 { - j.handler.DisplayParsedSchedules(j.config.SubTitle, schedules) + j.handler.DisplayParsedSchedules(j.config.CommandName, schedules) } else { - err := j.handler.DisplaySchedules(j.config.SubTitle, j.config.Schedules) + err := j.handler.DisplaySchedules(j.config.CommandName, j.config.Schedules) if err != nil { return err } @@ -85,7 +74,7 @@ func (j *Job) Remove() error { // RemoveOnly returns true if this job can be removed only func (j *Job) RemoveOnly() bool { - return j.config.RemoveOnly + return j.config.removeOnly } // Status of a job @@ -100,10 +89,9 @@ func (j *Job) Status() error { } if len(schedules) > 0 { - j.handler.DisplayParsedSchedules(j.config.SubTitle, schedules) + j.handler.DisplayParsedSchedules(j.config.CommandName, schedules) } else { - err := j.handler.DisplaySchedules(j.config.SubTitle, j.config.Schedules) - if err != nil { + if err := j.handler.DisplaySchedules(j.config.CommandName, j.config.Schedules); err != nil { return err } } @@ -114,6 +102,3 @@ func (j *Job) Status() error { } return nil } - -// Verify interface -var _ SchedulerJob = &Job{} diff --git a/schedule/job_test.go b/schedule/job_test.go index c2a53fcf..87d3e182 100644 --- a/schedule/job_test.go +++ b/schedule/job_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/creativeprojects/resticprofile/calendar" - "github.com/creativeprojects/resticprofile/config" "github.com/stretchr/testify/assert" ) @@ -21,13 +20,13 @@ func TestCreateJobHappyPathSystemd(t *testing.T) { counter |= 2 return nil }, - createJob: func(job *config.ScheduleConfig, schedules []*calendar.Event, permission string) error { + createJob: func(job *Config, schedules []*calendar.Event, permission string) error { counter |= 4 return nil }, } job := Job{ - config: &config.ScheduleConfig{}, + config: &Config{}, handler: handler, } err := job.Create() @@ -47,13 +46,13 @@ func TestCreateJobHappyPathOther(t *testing.T) { displayParsedSchedules: func(command string, events []*calendar.Event) { counter |= 2 }, - createJob: func(job *config.ScheduleConfig, schedules []*calendar.Event, permission string) error { + createJob: func(job *Config, schedules []*calendar.Event, permission string) error { counter |= 4 return nil }, } job := Job{ - config: &config.ScheduleConfig{}, + config: &Config{}, handler: handler, } err := job.Create() @@ -72,7 +71,7 @@ func TestCreateJobSadPath1(t *testing.T) { }, } job := Job{ - config: &config.ScheduleConfig{}, + config: &Config{}, handler: handler, } err := job.Create() @@ -95,7 +94,7 @@ func TestCreateJobSadPath2(t *testing.T) { }, } job := Job{ - config: &config.ScheduleConfig{}, + config: &Config{}, handler: handler, } err := job.Create() @@ -116,13 +115,13 @@ func TestCreateJobSadPath3(t *testing.T) { counter |= 2 return nil }, - createJob: func(job *config.ScheduleConfig, schedules []*calendar.Event, permission string) error { + createJob: func(job *Config, schedules []*calendar.Event, permission string) error { counter |= 4 return errors.New("test!") }, } job := Job{ - config: &config.ScheduleConfig{}, + config: &Config{}, handler: handler, } err := job.Create() diff --git a/schedule/removeonly_test.go b/schedule/removeonly_test.go index c2bf5a06..355ac60f 100644 --- a/schedule/removeonly_test.go +++ b/schedule/removeonly_test.go @@ -3,16 +3,14 @@ package schedule import ( "testing" - "github.com/creativeprojects/resticprofile/config" - "github.com/stretchr/testify/assert" ) func TestNewRemoveOnlyConfig(t *testing.T) { - cfg := config.NewRemoveOnlyConfig("profile", "command") + cfg := NewRemoveOnlyConfig("profile", "command") - assert.Equal(t, "profile", cfg.Title) - assert.Equal(t, "command", cfg.SubTitle) + assert.Equal(t, "profile", cfg.ProfileName) + assert.Equal(t, "command", cfg.CommandName) assert.Equal(t, "", cfg.JobDescription) assert.Equal(t, "", cfg.TimerDescription) assert.Empty(t, cfg.Schedules) @@ -22,7 +20,6 @@ func TestNewRemoveOnlyConfig(t *testing.T) { assert.Empty(t, cfg.Arguments) assert.Empty(t, cfg.Environment) assert.Equal(t, "", cfg.Priority) - assert.Equal(t, "", cfg.Log) assert.Equal(t, "", cfg.ConfigFile) { flag, found := cfg.GetFlag("") @@ -32,12 +29,12 @@ func TestNewRemoveOnlyConfig(t *testing.T) { } func TestDetectRemoveOnlyConfig(t *testing.T) { - assertRemoveOnly := func(expected bool, config *config.ScheduleConfig) { - assert.Equal(t, expected, config.RemoveOnly) + assertRemoveOnly := func(expected bool, config *Config) { + assert.Equal(t, expected, config.removeOnly) } - assertRemoveOnly(true, config.NewRemoveOnlyConfig("", "")) - assertRemoveOnly(false, &config.ScheduleConfig{}) + assertRemoveOnly(true, NewRemoveOnlyConfig("", "")) + assertRemoveOnly(false, &Config{}) } func TestRemoveOnlyJob(t *testing.T) { @@ -45,7 +42,7 @@ func TestRemoveOnlyJob(t *testing.T) { scheduler := NewScheduler(NewHandler(&SchedulerDefaultOS{}), profile) defer scheduler.Close() - job := scheduler.NewJob(config.NewRemoveOnlyConfig(profile, "check")) + job := scheduler.NewJob(NewRemoveOnlyConfig(profile, "check")) assert.Equal(t, ErrorJobCanBeRemovedOnly, job.Create()) assert.Equal(t, ErrorJobCanBeRemovedOnly, job.Status()) diff --git a/schedule/scheduler.go b/schedule/scheduler.go index bdf1818c..9bdaa7b3 100644 --- a/schedule/scheduler.go +++ b/schedule/scheduler.go @@ -2,7 +2,6 @@ package schedule import ( "github.com/creativeprojects/clog" - "github.com/creativeprojects/resticprofile/config" ) // Scheduler @@ -30,7 +29,7 @@ func (s *Scheduler) Close() { } // NewJob instantiates a Job object (of SchedulerJob interface) to schedule jobs -func (s *Scheduler) NewJob(config *config.ScheduleConfig) SchedulerJob { +func (s *Scheduler) NewJob(config *Config) *Job { return &Job{ config: config, handler: s.handler, diff --git a/schedule/scheduler_config.go b/schedule/scheduler_config.go index 34ea4b7a..b7fca483 100644 --- a/schedule/scheduler_config.go +++ b/schedule/scheduler_config.go @@ -1,10 +1,9 @@ package schedule import ( - "runtime" - "github.com/creativeprojects/resticprofile/config" "github.com/creativeprojects/resticprofile/constants" + "github.com/creativeprojects/resticprofile/platform" "github.com/spf13/afero" ) @@ -65,7 +64,7 @@ func NewSchedulerConfig(global *config.Global) SchedulerConfig { case constants.SchedulerWindows: return SchedulerWindows{} default: - if runtime.GOOS != "darwin" && runtime.GOOS != "windows" { + if !platform.IsDarwin() && !platform.IsWindows() { return SchedulerSystemd{ UnitTemplate: global.SystemdUnitTemplate, TimerTemplate: global.SystemdTimerTemplate, diff --git a/schedule_jobs.go b/schedule_jobs.go index 58100b8b..ada8ac2e 100644 --- a/schedule_jobs.go +++ b/schedule_jobs.go @@ -7,11 +7,10 @@ import ( "github.com/creativeprojects/clog" "github.com/creativeprojects/resticprofile/config" - "github.com/creativeprojects/resticprofile/constants" "github.com/creativeprojects/resticprofile/schedule" ) -func scheduleJobs(handler schedule.Handler, profileName string, configs []*config.ScheduleConfig) error { +func scheduleJobs(handler schedule.Handler, profileName string, configs []*config.Schedule) error { wd, err := os.Getwd() if err != nil { return err @@ -28,55 +27,37 @@ func scheduleJobs(handler schedule.Handler, profileName string, configs []*confi } defer scheduler.Close() - for _, scheduleConfig := range configs { + for _, cfg := range configs { + scheduleConfig := scheduleToConfig(cfg) + scheduleName := scheduleConfig.CommandName + "@" + scheduleConfig.ProfileName args := []string{ "--no-ansi", "--config", scheduleConfig.ConfigFile, - "--name", - scheduleConfig.Title, + "run-schedule", + scheduleName, } - if scheduleConfig.Log != "" { - args = append(args, "--log", scheduleConfig.Log) - } - - if scheduleConfig.GetLockMode() == config.ScheduleLockModeDefault { - if scheduleConfig.GetLockWait() > 0 { - args = append(args, "--lock-wait", scheduleConfig.GetLockWait().String()) - } - } else if scheduleConfig.GetLockMode() == config.ScheduleLockModeIgnore { - args = append(args, "--no-lock") - } - - if scheduleConfig.IgnoreOnBatteryLessThan > 0 && scheduleConfig.IgnoreOnBatteryLessThan <= 100 { - args = append(args, fmt.Sprintf("--ignore-on-battery=%d", scheduleConfig.IgnoreOnBatteryLessThan)) - } else if scheduleConfig.IgnoreOnBattery { - args = append(args, "--ignore-on-battery") - } - - args = append(args, getResticCommand(scheduleConfig.SubTitle)) - scheduleConfig.SetCommand(wd, binary, args) scheduleConfig.JobDescription = - fmt.Sprintf("resticprofile %s for profile %s in %s", scheduleConfig.SubTitle, scheduleConfig.Title, scheduleConfig.ConfigFile) + fmt.Sprintf("resticprofile %s for profile %s in %s", scheduleConfig.CommandName, scheduleConfig.ProfileName, scheduleConfig.ConfigFile) scheduleConfig.TimerDescription = - fmt.Sprintf("%s timer for profile %s in %s", scheduleConfig.SubTitle, scheduleConfig.Title, scheduleConfig.ConfigFile) + fmt.Sprintf("%s timer for profile %s in %s", scheduleConfig.CommandName, scheduleConfig.ProfileName, scheduleConfig.ConfigFile) job := scheduler.NewJob(scheduleConfig) err = job.Create() if err != nil { return fmt.Errorf("error creating job %s/%s: %w", - scheduleConfig.Title, - scheduleConfig.SubTitle, + scheduleConfig.ProfileName, + scheduleConfig.CommandName, err) } - clog.Infof("scheduled job %s/%s created", scheduleConfig.Title, scheduleConfig.SubTitle) + clog.Infof("scheduled job %s/%s created", scheduleConfig.ProfileName, scheduleConfig.CommandName) } return nil } -func removeJobs(handler schedule.Handler, profileName string, configs []*config.ScheduleConfig) error { +func removeJobs(handler schedule.Handler, profileName string, configs []*config.Schedule) error { scheduler := schedule.NewScheduler(handler, profileName) err := scheduler.Init() if err != nil { @@ -84,7 +65,8 @@ func removeJobs(handler schedule.Handler, profileName string, configs []*config. } defer scheduler.Close() - for _, scheduleConfig := range configs { + for _, cfg := range configs { + scheduleConfig := scheduleToConfig(cfg) job := scheduler.NewJob(scheduleConfig) // Skip over non-accessible, RemoveOnly jobs since they may not exist and must not causes errors @@ -98,22 +80,22 @@ func removeJobs(handler schedule.Handler, profileName string, configs []*config. if errors.Is(err, schedule.ErrorServiceNotFound) { // Display a warning and keep going. Skip message for RemoveOnly jobs since they may not exist if !job.RemoveOnly() { - clog.Warningf("service %s/%s not found", scheduleConfig.Title, scheduleConfig.SubTitle) + clog.Warningf("service %s/%s not found", scheduleConfig.ProfileName, scheduleConfig.CommandName) } continue } return fmt.Errorf("error removing job %s/%s: %w", - scheduleConfig.Title, - scheduleConfig.SubTitle, + scheduleConfig.ProfileName, + scheduleConfig.CommandName, err) } - clog.Infof("scheduled job %s/%s removed", scheduleConfig.Title, scheduleConfig.SubTitle) + clog.Infof("scheduled job %s/%s removed", scheduleConfig.ProfileName, scheduleConfig.CommandName) } return nil } -func statusJobs(handler schedule.Handler, profileName string, configs []*config.ScheduleConfig) error { +func statusJobs(handler schedule.Handler, profileName string, configs []*config.Schedule) error { scheduler := schedule.NewScheduler(handler, profileName) err := scheduler.Init() if err != nil { @@ -121,23 +103,24 @@ func statusJobs(handler schedule.Handler, profileName string, configs []*config. } defer scheduler.Close() - for _, scheduleConfig := range configs { + for _, cfg := range configs { + scheduleConfig := scheduleToConfig(cfg) job := scheduler.NewJob(scheduleConfig) err := job.Status() if err != nil { if errors.Is(err, schedule.ErrorServiceNotFound) { // Display a warning and keep going - clog.Warningf("service %s/%s not found", scheduleConfig.Title, scheduleConfig.SubTitle) + clog.Warningf("service %s/%s not found", scheduleConfig.ProfileName, scheduleConfig.CommandName) continue } if errors.Is(err, schedule.ErrorServiceNotRunning) { // Display a warning and keep going - clog.Warningf("service %s/%s is not running", scheduleConfig.Title, scheduleConfig.SubTitle) + clog.Warningf("service %s/%s is not running", scheduleConfig.ProfileName, scheduleConfig.CommandName) continue } return fmt.Errorf("error querying status of job %s/%s: %w", - scheduleConfig.Title, - scheduleConfig.SubTitle, + scheduleConfig.ProfileName, + scheduleConfig.CommandName, err) } } @@ -145,9 +128,28 @@ func statusJobs(handler schedule.Handler, profileName string, configs []*config. return nil } -func getResticCommand(profileCommand string) string { - if profileCommand == constants.SectionConfigurationRetention { - return constants.CommandForget +func scheduleToConfig(sched *config.Schedule) *schedule.Config { + if len(sched.Schedules) == 0 { + // there's no schedule defined, so this record is for removal only + return schedule.NewRemoveOnlyConfig(sched.ProfileName, sched.CommandName) + } + return &schedule.Config{ + ProfileName: sched.ProfileName, + CommandName: sched.CommandName, + Schedules: sched.Schedules, + Permission: sched.Permission, + WorkingDirectory: "", + Command: "", + Arguments: []string{}, + Environment: sched.Environment, + JobDescription: "", + TimerDescription: "", + Priority: sched.Priority, + LockMode: sched.LockMode, + LockWait: sched.LockWait, + ConfigFile: sched.ConfigFile, + Flags: sched.Flags, + IgnoreOnBattery: sched.IgnoreOnBattery, + IgnoreOnBatteryLessThan: sched.IgnoreOnBatteryLessThan, } - return profileCommand } diff --git a/schedule_jobs_test.go b/schedule_jobs_test.go index 050b835b..f24528a9 100644 --- a/schedule_jobs_test.go +++ b/schedule_jobs_test.go @@ -1,11 +1,12 @@ package main import ( + "errors" "testing" "github.com/creativeprojects/resticprofile/calendar" "github.com/creativeprojects/resticprofile/config" - "github.com/creativeprojects/resticprofile/platform" + "github.com/creativeprojects/resticprofile/schedule" "github.com/creativeprojects/resticprofile/schedule/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -13,81 +14,190 @@ import ( func TestScheduleNilJobs(t *testing.T) { handler := mocks.NewHandler(t) - handler.On("Init").Return(nil) - handler.On("Close") + handler.EXPECT().Init().Return(nil) + handler.EXPECT().Close() err := scheduleJobs(handler, "profile", nil) assert.NoError(t, err) } -func TestArgumentsOnScheduleJobNoLog(t *testing.T) { - handler := getMockHandler(t) - handler.On("CreateJob", - mock.AnythingOfType("*config.ScheduleConfig"), +func TestSimpleScheduleJob(t *testing.T) { + handler := mocks.NewHandler(t) + handler.EXPECT().Init().Return(nil) + handler.EXPECT().Close() + handler.EXPECT().ParseSchedules([]string{"sched"}).Return([]*calendar.Event{{}}, nil) + handler.EXPECT().DisplayParsedSchedules("backup", []*calendar.Event{{}}) + handler.EXPECT().CreateJob( + mock.AnythingOfType("*schedule.Config"), mock.AnythingOfType("[]*calendar.Event"), mock.AnythingOfType("string")). - Return(func(scheduleConfig *config.ScheduleConfig, events []*calendar.Event, permission string) error { - assert.Equal(t, []string{"--no-ansi", "--config", "", "--name", "profile", "backup"}, scheduleConfig.Arguments) + RunAndReturn(func(scheduleConfig *schedule.Config, events []*calendar.Event, permission string) error { + assert.Equal(t, []string{"--no-ansi", "--config", "", "run-schedule", "backup@profile"}, scheduleConfig.Arguments) return nil }) - scheduleConfig := &config.ScheduleConfig{ - Title: "profile", - SubTitle: "backup", + scheduleConfig := &config.Schedule{ + ProfileName: "profile", + CommandName: "backup", + Schedules: []string{"sched"}, } - err := scheduleJobs(handler, "profile", []*config.ScheduleConfig{scheduleConfig}) + err := scheduleJobs(handler, "profile", []*config.Schedule{scheduleConfig}) assert.NoError(t, err) } -func TestArgumentsOnScheduleJobLogFile(t *testing.T) { - handler := getMockHandler(t) +func TestFailScheduleJob(t *testing.T) { + handler := mocks.NewHandler(t) + handler.EXPECT().Init().Return(nil) + handler.EXPECT().Close() + handler.EXPECT().ParseSchedules([]string{"sched"}).Return([]*calendar.Event{{}}, nil) + handler.EXPECT().DisplayParsedSchedules("backup", []*calendar.Event{{}}) handler.EXPECT().CreateJob( - mock.AnythingOfType("*config.ScheduleConfig"), + mock.AnythingOfType("*schedule.Config"), mock.AnythingOfType("[]*calendar.Event"), mock.AnythingOfType("string")). - Run(func(scheduleConfig *config.ScheduleConfig, events []*calendar.Event, permission string) { - assert.Equal(t, []string{"--no-ansi", "--config", "", "--name", "profile", "--log", "/path/to/file", "backup"}, scheduleConfig.Arguments) - }). - Return(nil) - - scheduleConfig := &config.ScheduleConfig{ - Title: "profile", - SubTitle: "backup", - Log: "/path/to/file", + Return(errors.New("error creating job")) + + scheduleConfig := &config.Schedule{ + ProfileName: "profile", + CommandName: "backup", + Schedules: []string{"sched"}, } - err := scheduleJobs(handler, "profile", []*config.ScheduleConfig{scheduleConfig}) + err := scheduleJobs(handler, "profile", []*config.Schedule{scheduleConfig}) + assert.Error(t, err) +} + +func TestRemoveNilJobs(t *testing.T) { + handler := mocks.NewHandler(t) + handler.EXPECT().Init().Return(nil) + handler.EXPECT().Close() + + err := removeJobs(handler, "profile", nil) assert.NoError(t, err) } -func TestArgumentsOnScheduleJobLogSyslog(t *testing.T) { - if !platform.SupportsSyslog() { - t.Skip("syslog is not supported") +func TestRemoveJob(t *testing.T) { + handler := mocks.NewHandler(t) + handler.EXPECT().Init().Return(nil) + handler.EXPECT().Close() + handler.EXPECT().RemoveJob(mock.AnythingOfType("*schedule.Config"), mock.AnythingOfType("string")). + RunAndReturn(func(scheduleConfig *schedule.Config, user string) error { + assert.Equal(t, "profile", scheduleConfig.ProfileName) + assert.Equal(t, "backup", scheduleConfig.CommandName) + return nil + }) + + scheduleConfig := &config.Schedule{ + ProfileName: "profile", + CommandName: "backup", + Schedules: []string{"sched"}, } - handler := getMockHandler(t) - handler.On("CreateJob", - mock.AnythingOfType("*config.ScheduleConfig"), - mock.AnythingOfType("[]*calendar.Event"), - mock.AnythingOfType("string")). - Return(func(scheduleConfig *config.ScheduleConfig, events []*calendar.Event, permission string) error { - assert.Equal(t, []string{"--no-ansi", "--config", "", "--name", "profile", "--log", "tcp://localhost:123", "backup"}, scheduleConfig.Arguments) + err := removeJobs(handler, "profile", []*config.Schedule{scheduleConfig}) + assert.NoError(t, err) +} + +func TestRemoveJobNoConfig(t *testing.T) { + handler := mocks.NewHandler(t) + handler.EXPECT().Init().Return(nil) + handler.EXPECT().Close() + handler.EXPECT().RemoveJob(mock.AnythingOfType("*schedule.Config"), mock.AnythingOfType("string")). + RunAndReturn(func(scheduleConfig *schedule.Config, user string) error { + assert.Equal(t, "profile", scheduleConfig.ProfileName) + assert.Equal(t, "backup", scheduleConfig.CommandName) return nil }) - scheduleConfig := &config.ScheduleConfig{ - Title: "profile", - SubTitle: "backup", - Log: "tcp://localhost:123", + scheduleConfig := &config.Schedule{ + ProfileName: "profile", + CommandName: "backup", + } + err := removeJobs(handler, "profile", []*config.Schedule{scheduleConfig}) + assert.NoError(t, err) +} + +func TestFailRemoveJob(t *testing.T) { + handler := mocks.NewHandler(t) + handler.EXPECT().Init().Return(nil) + handler.EXPECT().Close() + handler.EXPECT().RemoveJob(mock.AnythingOfType("*schedule.Config"), mock.AnythingOfType("string")). + Return(errors.New("error removing job")) + + scheduleConfig := &config.Schedule{ + ProfileName: "profile", + CommandName: "backup", + Schedules: []string{"sched"}, } - err := scheduleJobs(handler, "profile", []*config.ScheduleConfig{scheduleConfig}) + err := removeJobs(handler, "profile", []*config.Schedule{scheduleConfig}) + assert.Error(t, err) +} + +func TestNoFailRemoveUnknownJob(t *testing.T) { + handler := mocks.NewHandler(t) + handler.EXPECT().Init().Return(nil) + handler.EXPECT().Close() + handler.EXPECT().RemoveJob(mock.AnythingOfType("*schedule.Config"), mock.AnythingOfType("string")). + Return(schedule.ErrorServiceNotFound) + + scheduleConfig := &config.Schedule{ + ProfileName: "profile", + CommandName: "backup", + Schedules: []string{"sched"}, + } + err := removeJobs(handler, "profile", []*config.Schedule{scheduleConfig}) + assert.NoError(t, err) +} + +func TestNoFailRemoveUnknownRemoveOnlyJob(t *testing.T) { + handler := mocks.NewHandler(t) + handler.EXPECT().Init().Return(nil) + handler.EXPECT().Close() + handler.EXPECT().RemoveJob(mock.AnythingOfType("*schedule.Config"), mock.AnythingOfType("string")). + Return(schedule.ErrorServiceNotFound) + + scheduleConfig := &config.Schedule{ + ProfileName: "profile", + CommandName: "backup", + } + err := removeJobs(handler, "profile", []*config.Schedule{scheduleConfig}) + assert.NoError(t, err) +} + +func TestStatusNilJobs(t *testing.T) { + handler := mocks.NewHandler(t) + handler.EXPECT().Init().Return(nil) + handler.EXPECT().Close() + handler.EXPECT().DisplayStatus("profile").Return(nil) + + err := statusJobs(handler, "profile", nil) + assert.NoError(t, err) +} + +func TestStatusJob(t *testing.T) { + handler := mocks.NewHandler(t) + handler.EXPECT().Init().Return(nil) + handler.EXPECT().Close() + handler.EXPECT().ParseSchedules([]string{"sched"}).Return([]*calendar.Event{{}}, nil) + handler.EXPECT().DisplayParsedSchedules("backup", []*calendar.Event{{}}) + handler.EXPECT().DisplayJobStatus(mock.AnythingOfType("*schedule.Config")).Return(nil) + handler.EXPECT().DisplayStatus("profile").Return(nil) + + scheduleConfig := &config.Schedule{ + ProfileName: "profile", + CommandName: "backup", + Schedules: []string{"sched"}, + } + err := statusJobs(handler, "profile", []*config.Schedule{scheduleConfig}) assert.NoError(t, err) } -func getMockHandler(t *testing.T) *mocks.Handler { - t.Helper() +func TestStatusRemoveOnlyJob(t *testing.T) { handler := mocks.NewHandler(t) - handler.On("Init").Return(nil) - handler.On("Close") - handler.On("ParseSchedules", []string(nil)).Return(nil, nil) - handler.On("DisplaySchedules", "backup", []string(nil)).Return(nil) - return handler + handler.EXPECT().Init().Return(nil) + handler.EXPECT().Close() + + scheduleConfig := &config.Schedule{ + ProfileName: "profile", + CommandName: "backup", + } + err := statusJobs(handler, "profile", []*config.Schedule{scheduleConfig}) + assert.Error(t, err) } diff --git a/schtasks/config.go b/schtasks/config.go new file mode 100644 index 00000000..eebb417c --- /dev/null +++ b/schtasks/config.go @@ -0,0 +1,10 @@ +package schtasks + +type Config struct { + ProfileName string + CommandName string + Command string + Arguments []string + WorkingDirectory string + JobDescription string +} diff --git a/schtasks/taskscheduler.go b/schtasks/taskscheduler.go index 327e04dd..c20805e1 100644 --- a/schtasks/taskscheduler.go +++ b/schtasks/taskscheduler.go @@ -14,7 +14,6 @@ import ( "github.com/capnspacehook/taskmaster" "github.com/creativeprojects/clog" "github.com/creativeprojects/resticprofile/calendar" - "github.com/creativeprojects/resticprofile/config" "github.com/creativeprojects/resticprofile/constants" "github.com/creativeprojects/resticprofile/term" "github.com/rickb777/date/period" @@ -85,7 +84,7 @@ func Close() { } // Create or update a task (if the name already exists in the Task Scheduler) -func Create(config *config.ScheduleConfig, schedules []*calendar.Event, permission Permission) error { +func Create(config *Config, schedules []*calendar.Event, permission Permission) error { if !IsConnected() { return ErrorNotConnected } @@ -100,8 +99,8 @@ func Create(config *config.ScheduleConfig, schedules []*calendar.Event, permissi } // createUserTask creates a new user task. Will update an existing task instead of overwritting -func createUserTask(config *config.ScheduleConfig, schedules []*calendar.Event) error { - taskName := getTaskPath(config.Title, config.SubTitle) +func createUserTask(config *Config, schedules []*calendar.Event) error { + taskName := getTaskPath(config.ProfileName, config.CommandName) registeredTask, err := taskService.GetRegisteredTask(taskName) if err == nil { // the task already exists @@ -144,8 +143,8 @@ func createUserTask(config *config.ScheduleConfig, schedules []*calendar.Event) } // updateUserTask updates an existing task -func updateUserTask(task taskmaster.RegisteredTask, config *config.ScheduleConfig, schedules []*calendar.Event) error { - taskName := getTaskPath(config.Title, config.SubTitle) +func updateUserTask(task taskmaster.RegisteredTask, config *Config, schedules []*calendar.Event) error { + taskName := getTaskPath(config.ProfileName, config.CommandName) username, password, err := userCredentials() if err != nil { @@ -201,8 +200,8 @@ func userCredentials() (string, string, error) { } // createUserLoggedOnTask creates a new user task. Will update an existing task instead of overwritting -func createUserLoggedOnTask(config *config.ScheduleConfig, schedules []*calendar.Event) error { - taskName := getTaskPath(config.Title, config.SubTitle) +func createUserLoggedOnTask(config *Config, schedules []*calendar.Event) error { + taskName := getTaskPath(config.ProfileName, config.CommandName) registeredTask, err := taskService.GetRegisteredTask(taskName) if err == nil { // the task already exists @@ -239,8 +238,8 @@ func createUserLoggedOnTask(config *config.ScheduleConfig, schedules []*calendar } // updateUserLoggedOnTask updates an existing task -func updateUserLoggedOnTask(task taskmaster.RegisteredTask, config *config.ScheduleConfig, schedules []*calendar.Event) error { - taskName := getTaskPath(config.Title, config.SubTitle) +func updateUserLoggedOnTask(task taskmaster.RegisteredTask, config *Config, schedules []*calendar.Event) error { + taskName := getTaskPath(config.ProfileName, config.CommandName) // clear up all actions and put ours back task.Definition.Actions = make([]taskmaster.Action, 0, 1) @@ -269,8 +268,8 @@ func updateUserLoggedOnTask(task taskmaster.RegisteredTask, config *config.Sched } // createSystemTask creates a new system task. Will update an existing task instead of overwritting -func createSystemTask(config *config.ScheduleConfig, schedules []*calendar.Event) error { - taskName := getTaskPath(config.Title, config.SubTitle) +func createSystemTask(config *Config, schedules []*calendar.Event) error { + taskName := getTaskPath(config.ProfileName, config.CommandName) registeredTask, err := taskService.GetRegisteredTask(taskName) if err == nil { // the task already exists @@ -302,8 +301,8 @@ func createSystemTask(config *config.ScheduleConfig, schedules []*calendar.Event } // updateSystemTask updates an existing task -func updateSystemTask(task taskmaster.RegisteredTask, config *config.ScheduleConfig, schedules []*calendar.Event) error { - taskName := getTaskPath(config.Title, config.SubTitle) +func updateSystemTask(task taskmaster.RegisteredTask, config *Config, schedules []*calendar.Event) error { + taskName := getTaskPath(config.ProfileName, config.CommandName) // clear up all actions and put ours back task.Definition.Actions = make([]taskmaster.Action, 0, 1) diff --git a/schtasks/taskscheduler_test.go b/schtasks/taskscheduler_test.go index e6b4eaa4..487863cc 100644 --- a/schtasks/taskscheduler_test.go +++ b/schtasks/taskscheduler_test.go @@ -14,7 +14,6 @@ import ( "github.com/capnspacehook/taskmaster" "github.com/creativeprojects/clog" "github.com/creativeprojects/resticprofile/calendar" - "github.com/creativeprojects/resticprofile/config" "github.com/rickb777/date/period" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -303,9 +302,9 @@ func TestCreationOfTasks(t *testing.T) { defer Close() assert.NoError(t, err) - scheduleConfig := &config.ScheduleConfig{ - Title: "test", - SubTitle: strconv.Itoa(count), + scheduleConfig := &Config{ + ProfileName: "test", + CommandName: strconv.Itoa(count), Command: "echo", Arguments: []string{"hello"}, WorkingDirectory: "C:\\", @@ -322,9 +321,9 @@ func TestCreationOfTasks(t *testing.T) { // user logged in doesn't need a password err = createUserLoggedOnTask(scheduleConfig, schedules) assert.NoError(t, err) - defer Delete(scheduleConfig.Title, scheduleConfig.SubTitle) + defer Delete(scheduleConfig.ProfileName, scheduleConfig.CommandName) - taskName := getTaskPath(scheduleConfig.Title, scheduleConfig.SubTitle) + taskName := getTaskPath(scheduleConfig.ProfileName, scheduleConfig.CommandName) buffer, err := exportTask(taskName) assert.NoError(t, err)