config: detect duplicate & internal job names at parse time (#908)
Before this PR, config parsing would accept duplicate job names. `zrepl daemon` would later fail to start with a panic. But tools like `zrepl configcheck` would pass. This PR adds a check to ensure job names are unique. Similarly, internal job names were not being rejected by config parsing Move that check to parse-time as well. Last, drive-by change: remove `internal/config/config_include_test.go` introduced in #856 . These aren't necessary because there is already a wildcard test for all valid configs. This PR adds the complimentary "invalid config" wildcard test.
This commit is contained in:
committed by
GitHub
parent
4d6583ea5f
commit
860a9be37f
@@ -11,7 +11,7 @@ import (
|
|||||||
yaml "github.com/zrepl/yaml-config"
|
yaml "github.com/zrepl/yaml-config"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/internal/client/status/viewmodel/stringbuilder"
|
"github.com/zrepl/zrepl/internal/client/status/viewmodel/stringbuilder"
|
||||||
"github.com/zrepl/zrepl/internal/daemon"
|
"github.com/zrepl/zrepl/internal/config"
|
||||||
"github.com/zrepl/zrepl/internal/daemon/job"
|
"github.com/zrepl/zrepl/internal/daemon/job"
|
||||||
"github.com/zrepl/zrepl/internal/daemon/pruner"
|
"github.com/zrepl/zrepl/internal/daemon/pruner"
|
||||||
"github.com/zrepl/zrepl/internal/daemon/snapper"
|
"github.com/zrepl/zrepl/internal/daemon/snapper"
|
||||||
@@ -85,7 +85,7 @@ func (m *M) Update(p Params) {
|
|||||||
// filter out internal jobs
|
// filter out internal jobs
|
||||||
var jobsList []*Job
|
var jobsList []*Job
|
||||||
for _, j := range m.jobsList {
|
for _, j := range m.jobsList {
|
||||||
if daemon.IsInternalJobName(j.name) {
|
if config.IsInternalJobName(j.name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
jobsList = append(jobsList, j)
|
jobsList = append(jobsList, j)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
pathpkg "path"
|
pathpkg "path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -693,9 +694,32 @@ func ParseConfig(path string) (rootConfig *Config, err error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = validateJobNames(rootConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return rootConfig, err
|
return rootConfig, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsInternalJobName(s string) bool {
|
||||||
|
return strings.HasPrefix(s, "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateJobNames(config *Config) error {
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
for _, job := range config.Jobs {
|
||||||
|
name := job.Name()
|
||||||
|
if IsInternalJobName(name) {
|
||||||
|
return errors.Errorf("job name %q is reserved for internal use (starts with _)", name)
|
||||||
|
}
|
||||||
|
if _, ok := seen[name]; ok {
|
||||||
|
return errors.Errorf("duplicate job name %q", name)
|
||||||
|
}
|
||||||
|
seen[name] = struct{}{}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func expandConfigInclude(configPath string, config *Config) (err error) {
|
func expandConfigInclude(configPath string, config *Config) (err error) {
|
||||||
var includeConfigPaths []string
|
var includeConfigPaths []string
|
||||||
for _, path := range config.Include {
|
for _, path := range config.Include {
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestIncludeSingle(t *testing.T) {
|
|
||||||
path := "./samples/include.yml"
|
|
||||||
|
|
||||||
t.Run(path, func(t *testing.T) {
|
|
||||||
config, err := ParseConfig(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("error parsing %s:\n%+v", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NotNil(t, config)
|
|
||||||
require.NotNil(t, config.Global)
|
|
||||||
require.NotEmpty(t, config.Jobs)
|
|
||||||
|
|
||||||
t.Logf("file: %s", path)
|
|
||||||
t.Log(pretty.Sprint(config))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIncludeDirectory(t *testing.T) {
|
|
||||||
path := "./samples/include_directory.yml"
|
|
||||||
|
|
||||||
t.Run(path, func(t *testing.T) {
|
|
||||||
config, err := ParseConfig(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("error parsing %s:\n%+v", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NotNil(t, config)
|
|
||||||
require.NotNil(t, config.Global)
|
|
||||||
require.NotEmpty(t, config.Jobs)
|
|
||||||
|
|
||||||
t.Logf("file: %s", path)
|
|
||||||
t.Log(pretty.Sprint(config))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -45,6 +45,20 @@ func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInvalidSampleConfigsFailToParse(t *testing.T) {
|
||||||
|
paths, err := filepath.Glob("./samples/invalid/*/zrepl.yml")
|
||||||
|
require.NoError(t, err, "glob failed")
|
||||||
|
require.NotEmpty(t, paths, "no invalid sample configs found")
|
||||||
|
|
||||||
|
for _, p := range paths {
|
||||||
|
t.Run(p, func(t *testing.T) {
|
||||||
|
_, err := ParseConfig(p)
|
||||||
|
require.Error(t, err, "expected config %s to fail parsing", p)
|
||||||
|
t.Logf("config %s failed as expected: %v", p, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// template must be a template/text template with a single '{{ . }}' as placeholder for val
|
// template must be a template/text template with a single '{{ . }}' as placeholder for val
|
||||||
//
|
//
|
||||||
//nolint:deadcode,unused
|
//nolint:deadcode,unused
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
jobs:
|
||||||
|
- type: snap
|
||||||
|
name: "my_job"
|
||||||
|
filesystems: {
|
||||||
|
"<": true,
|
||||||
|
}
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
pruning:
|
||||||
|
keep:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
|
||||||
|
- type: snap
|
||||||
|
name: "my_job"
|
||||||
|
filesystems: {
|
||||||
|
"<": true,
|
||||||
|
}
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
pruning:
|
||||||
|
keep:
|
||||||
|
- type: last_n
|
||||||
|
count: 5
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
jobs:
|
||||||
|
- type: snap
|
||||||
|
name: "my_job"
|
||||||
|
filesystems: {
|
||||||
|
"<": true,
|
||||||
|
}
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
pruning:
|
||||||
|
keep:
|
||||||
|
- type: last_n
|
||||||
|
count: 5
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
jobs:
|
||||||
|
- type: snap
|
||||||
|
name: "my_job"
|
||||||
|
filesystems: {
|
||||||
|
"<": true,
|
||||||
|
}
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
pruning:
|
||||||
|
keep:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
|
|
||||||
|
include:
|
||||||
|
- ./included.yml
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
jobs:
|
||||||
|
- type: snap
|
||||||
|
name: "_internal_job"
|
||||||
|
filesystems: {
|
||||||
|
"<": true,
|
||||||
|
}
|
||||||
|
snapshotting:
|
||||||
|
type: manual
|
||||||
|
pruning:
|
||||||
|
keep:
|
||||||
|
- type: last_n
|
||||||
|
count: 10
|
||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -64,7 +63,7 @@ func Run(ctx context.Context, conf *config.Config) error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
for _, job := range confJobs {
|
for _, job := range confJobs {
|
||||||
if IsInternalJobName(job.Name()) {
|
if config.IsInternalJobName(job.Name()) {
|
||||||
panic(fmt.Sprintf("internal job name used for config job '%s'", job.Name())) //FIXME
|
panic(fmt.Sprintf("internal job name used for config job '%s'", job.Name())) //FIXME
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,10 +210,6 @@ const (
|
|||||||
jobNameControl = "_control"
|
jobNameControl = "_control"
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsInternalJobName(s string) bool {
|
|
||||||
return strings.HasPrefix(s, "_")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
|
func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
|
||||||
s.m.Lock()
|
s.m.Lock()
|
||||||
defer s.m.Unlock()
|
defer s.m.Unlock()
|
||||||
@@ -222,10 +217,12 @@ func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
|
|||||||
ctx = logging.WithInjectedField(ctx, logging.JobField, j.Name())
|
ctx = logging.WithInjectedField(ctx, logging.JobField, j.Name())
|
||||||
|
|
||||||
jobName := j.Name()
|
jobName := j.Name()
|
||||||
if !internal && IsInternalJobName(jobName) {
|
|
||||||
|
// package `config` enforces these with clean errors, these are just assertions
|
||||||
|
if !internal && config.IsInternalJobName(jobName) {
|
||||||
panic(fmt.Sprintf("internal job name used for non-internal job %s", jobName))
|
panic(fmt.Sprintf("internal job name used for non-internal job %s", jobName))
|
||||||
}
|
}
|
||||||
if internal && !IsInternalJobName(jobName) {
|
if internal && !config.IsInternalJobName(jobName) {
|
||||||
panic(fmt.Sprintf("internal job does not use internal job name %s", jobName))
|
panic(fmt.Sprintf("internal job does not use internal job name %s", jobName))
|
||||||
}
|
}
|
||||||
if _, ok := s.jobs[jobName]; ok {
|
if _, ok := s.jobs[jobName]; ok {
|
||||||
|
|||||||
Reference in New Issue
Block a user