Compare commits

..

2 Commits

Author SHA1 Message Date
a2f7dbca82 wip: new config unit test 2024-01-06 23:09:34 +00:00
006f8278d7 wip: feat(cron in new config): added parser
wip: fix: remove unreachable code
wip: test cron
2024-01-04 22:55:16 +00:00
4 changed files with 344 additions and 13 deletions

View File

@ -49,5 +49,4 @@ func convertSchedule(cfg config.CronTimeSpec) task.CronTimeSpec {
} }
panic("invalid conversion") panic("invalid conversion")
return nil
} }

View File

@ -1,11 +1,12 @@
package config package config
import ( import (
"gitea.suyono.dev/suyono/wingmate"
"github.com/stretchr/testify/assert"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"gitea.suyono.dev/suyono/wingmate"
"github.com/stretchr/testify/assert"
) )
const ( const (
@ -96,11 +97,11 @@ const crontabTestCase3 = `a 13 3,5,7 * * /path/to/executable
*/5 13 a * * /path/to/executable */5 13 a * * /path/to/executable
*/5 13 3,5,7 a * /path/to/executable */5 13 3,5,7 a * /path/to/executable
*/5 13 3,5,7 * a /path/to/executable */5 13 3,5,7 * a /path/to/executable
*/x 13 3,5,7 * a /path/to/executable */x 13 3,5,7 * * /path/to/executable
76 13 3,5,7 * a /path/to/executable 76 13 3,5,7 * * /path/to/executable
*/75 13 3,5,7 * a /path/to/executable */75 13 3,5,7 * * /path/to/executable
*/5 13 3,x,7 * a /path/to/executable */5 13 3,x,7 * * /path/to/executable
*/5 13 3,5,67 * a /path/to/executable */5 13 3,5,67 * * /path/to/executable
*/5 13 * * /path/to/executable */5 13 * * /path/to/executable
*/5 13 3,5,7 * * /path/to/executable` */5 13 3,5,7 * * /path/to/executable`

View File

@ -1,18 +1,29 @@
package config package config
import ( import (
"errors"
"fmt" "fmt"
"regexp"
"strconv"
"strings"
"gitea.suyono.dev/suyono/wingmate" "gitea.suyono.dev/suyono/wingmate"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
const ( const (
CrontabScheduleRegexPattern = `^\s*(?P<minute>\S+)\s+(?P<hour>\S+)\s+(?P<dom>\S+)\s+(?P<month>\S+)\s+(?P<dow>\S+)\s*$`
CrontabScheduleSubMatchLen = 6
ServiceConfigGroup = "service" ServiceConfigGroup = "service"
CronConfigGroup = "cron" CronConfigGroup = "cron"
ServiceKeyFormat = "service.%s" ServiceKeyFormat = "service.%s"
CronKeyFormat = "cron.%s" CronKeyFormat = "cron.%s"
) )
var (
crontabScheduleRegex = regexp.MustCompile(CrontabScheduleRegexPattern)
)
func readConfigYaml(path, name, format string) ([]ServiceTask, []CronTask, error) { func readConfigYaml(path, name, format string) ([]ServiceTask, []CronTask, error) {
var ( var (
err error err error
@ -54,8 +65,138 @@ func readConfigYaml(path, name, format string) ([]ServiceTask, []CronTask, error
continue continue
} }
cronTask.Name = itemName cronTask.Name = itemName
if cronTask.CronSchedule, err = parseYamlSchedule(cronTask.Schedule); err != nil {
wingmate.Log().Error().Msgf("parsing cron schedule: %+v", err)
continue
}
crones = append(crones, cronTask) crones = append(crones, cronTask)
} }
return services, crones, nil return services, crones, nil
} }
func parseYamlSchedule(input string) (schedule CronSchedule, err error) {
var (
parts []string
pSched *CronSchedule
)
parts = crontabScheduleRegex.FindStringSubmatch(input)
if len(parts) != CrontabScheduleSubMatchLen {
return schedule, fmt.Errorf("invalid schedule: %s", input)
}
pSched = &schedule
if err = pSched.setField(minute, parts[1]); err != nil {
return schedule, fmt.Errorf("error parsing Minute field: %w", err)
}
if err = pSched.setField(hour, parts[2]); err != nil {
return schedule, fmt.Errorf("error parsing Hour field: %w", err)
}
if err = pSched.setField(dom, parts[3]); err != nil {
return schedule, fmt.Errorf("error parsing Day of Month field: %w", err)
}
if err = pSched.setField(month, parts[4]); err != nil {
return schedule, fmt.Errorf("error parsing Month field: %w", err)
}
if err = pSched.setField(dow, parts[5]); err != nil {
return schedule, fmt.Errorf("error parsing Day of Week field: %w", err)
}
return
}
func (c *CronSchedule) setField(field cronField, input string) error {
var (
fr *fieldRange
cField *CronTimeSpec
err error
parsed64 uint64
parsed uint8
multi []uint8
current uint8
multiStr []string
)
switch field {
case minute:
fr = newRange(0, 59)
cField = &c.Minute
case hour:
fr = newRange(0, 23)
cField = &c.Hour
case dom:
fr = newRange(1, 31)
cField = &c.DoM
case month:
fr = newRange(1, 12)
cField = &c.Month
case dow:
fr = newRange(0, 6)
cField = &c.DoW
default:
return errors.New("invalid cron field descriptor")
}
if input == "*" {
*cField = &SpecAny{}
} else if strings.HasPrefix(input, "*/") {
if parsed64, err = strconv.ParseUint(input[2:], 10, 8); err != nil {
return fmt.Errorf("error parse field %+v with input %s: %w", field, input, err)
}
parsed = uint8(parsed64)
if !fr.valid(parsed) {
return fmt.Errorf("error parse field %+v with input %s parsed to %d: invalid value", field, input, parsed)
}
multi = make([]uint8, 0)
current = parsed
for fr.valid(current) {
multi = append(multi, current)
current += parsed
}
*cField = &SpecMultiOccurrence{
values: multi,
}
} else {
multiStr = strings.Split(input, ",")
if len(multiStr) > 1 {
multi = make([]uint8, 0)
for _, s := range multiStr {
if parsed64, err = strconv.ParseUint(s, 10, 8); err != nil {
return fmt.Errorf("error parse field %+v with input %s: %w", field, input, err)
}
parsed = uint8(parsed64)
if !fr.valid(parsed) {
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input)
}
multi = append(multi, parsed)
}
*cField = &SpecMultiOccurrence{
values: multi,
}
} else {
if parsed64, err = strconv.ParseUint(input, 10, 8); err != nil {
return fmt.Errorf("error parse field %+v with input %s: %w", field, input, err)
}
parsed = uint8(parsed64)
if !fr.valid(parsed) {
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input)
}
*cField = &SpecExact{
value: parsed,
}
}
}
return nil
}

View File

@ -1,10 +1,12 @@
package config package config
import ( import (
"gitea.suyono.dev/suyono/wingmate" "fmt"
"os" "os"
"path" "path"
"testing" "testing"
"gitea.suyono.dev/suyono/wingmate"
) )
const configName = "wingmate.yaml" const configName = "wingmate.yaml"
@ -23,7 +25,31 @@ func TestYaml(t *testing.T) {
config: yamlTestCase0, config: yamlTestCase0,
wantErr: false, wantErr: false,
}, },
{
name: "service only",
config: yamlTestCase1,
wantErr: false,
},
{
name: "cron only",
config: yamlTestCase2,
wantErr: false,
},
{
name: "invalid content - service",
config: yamlTestCase3,
wantErr: false,
},
} }
for i, tc := range yamlBlobs {
tests = append(tests, testEntry{
name: fmt.Sprintf("negative - %d", i),
config: tc,
wantErr: true,
})
}
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
setup(t) setup(t)
@ -59,6 +85,25 @@ func writeYaml(t *testing.T, path, content string) {
} }
const yamlTestCase0 = `version: "1" const yamlTestCase0 = `version: "1"
service:
one:
command: ["command", "arg0", "arg1"]
environ: ["ENV1=value1", "ENV2=valueX"]
user: "user1"
group: "999"
working_dir: "/path/to/working"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 * * * 2,3"`
const yamlTestCase1 = `version: "1"
service: service:
one: one:
command: ["command", "arg0", "arg1"] command: ["command", "arg0", "arg1"]
@ -66,3 +111,148 @@ service:
user: "user1" user: "user1"
group: "999" group: "999"
working_dir: "/path/to/working"` working_dir: "/path/to/working"`
const yamlTestCase2 = `version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 * * * 2,3"`
const yamlTestCase3 = `version: "1"
service:
one:
command: 12345
environ: ["ENV1=value1", "ENV2=valueX"]
user: "user1"
group: "999"
working_dir: "/path/to/working"`
var yamlBlobs = []string{
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "a 13 3,5,7 * *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 a 3,5,7 * *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 13 a * *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 13 3,5,7 a *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 13 3,5,7 * a"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/x 13 3,5,7 * *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "76 13 3,5,7 * *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/75 13 3,5,7 * *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 13 3,x,7 * *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 13 3,5,67 * *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 13 * *"`,
}