Skip to content

Commit 7cdb2ab

Browse files
authored
Add yaml schema verification (#46)
1 parent 77bbbc3 commit 7cdb2ab

File tree

2 files changed

+304
-2
lines changed

2 files changed

+304
-2
lines changed

config.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ package main
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"os"
78
"path"
9+
"reflect"
10+
"slices"
811
"time"
912

1013
"github.com/prometheus/client_golang/prometheus"
@@ -32,6 +35,11 @@ var (
3235
Name: "git_mirror_config_last_reload_success_timestamp_seconds",
3336
Help: "Timestamp of the last successful configuration reload.",
3437
})
38+
allowedRepoPoolConfig = getAllowedKeys(mirror.RepoPoolConfig{})
39+
allowedDefaults = getAllowedKeys(mirror.DefaultConfig{})
40+
allowedAuthKeys = getAllowedKeys(mirror.Auth{})
41+
allowedRepoKeys = getAllowedKeys(mirror.RepositoryConfig{})
42+
allowedWorktreeKeys = getAllowedKeys(mirror.WorktreeConfig{})
3543
)
3644

3745
// WatchConfig polls the config file every interval and reloads if modified
@@ -174,14 +182,132 @@ func parseConfigFile(path string) (*mirror.RepoPoolConfig, error) {
174182
if err != nil {
175183
return nil, err
176184
}
185+
186+
err = validateConfigYaml([]byte(yamlFile))
187+
if err != nil {
188+
return nil, err
189+
}
190+
177191
conf := &mirror.RepoPoolConfig{}
178192
err = yaml.Unmarshal(yamlFile, conf)
179193
if err != nil {
180194
return nil, err
181195
}
196+
182197
return conf, nil
183198
}
184199

200+
func validateConfigYaml(yamlData []byte) error {
201+
var raw map[string]interface{}
202+
if err := yaml.Unmarshal(yamlData, &raw); err != nil {
203+
return err
204+
}
205+
206+
// check all root config sections for unexpected keys
207+
if key := findUnexpectedKey(raw, allowedRepoPoolConfig); key != "" {
208+
return fmt.Errorf("unexpected key: .%v", key)
209+
}
210+
211+
// check ".defaults" if it's not empty
212+
if raw["defaults"] != nil {
213+
defaultsMap, ok := raw["defaults"].(map[string]interface{})
214+
if !ok {
215+
return fmt.Errorf(".defaults config is not valid")
216+
}
217+
218+
if key := findUnexpectedKey(defaultsMap, allowedDefaults); key != "" {
219+
return fmt.Errorf("unexpected key: .defaults.%v", key)
220+
}
221+
222+
// check ".defaults.auth"
223+
if authMap, ok := defaultsMap["auth"].(map[string]interface{}); ok {
224+
if key := findUnexpectedKey(authMap, allowedAuthKeys); key != "" {
225+
return fmt.Errorf("unexpected key: .defaults.auth.%v", key)
226+
}
227+
}
228+
}
229+
230+
// skip further config checks if ".repositories" is empty
231+
if raw["repositories"] == nil {
232+
return nil
233+
}
234+
235+
// check ".repositories"
236+
reposInterface, ok := raw["repositories"].([]interface{})
237+
if !ok {
238+
return fmt.Errorf(".repositories config must be an array")
239+
}
240+
241+
// check each repository in ".repositories"
242+
for _, repoInterface := range reposInterface {
243+
repoMap, ok := repoInterface.(map[string]interface{})
244+
if !ok {
245+
return fmt.Errorf(".repositories config is not valid")
246+
}
247+
248+
if key := findUnexpectedKey(repoMap, allowedRepoKeys); key != "" {
249+
return fmt.Errorf("unexpected key: .repositories[%v].%v", repoMap["remote"], key)
250+
}
251+
252+
// skip further repository checks if "worktrees" is empty
253+
if repoMap["worktrees"] == nil {
254+
continue
255+
}
256+
257+
// check "worktrees" in each repository
258+
worktreesInterface, ok := repoMap["worktrees"].([]interface{})
259+
if !ok {
260+
return fmt.Errorf("worktrees config must be an array in .repositories[%v]", repoMap["remote"])
261+
}
262+
263+
for i, worktreeInterface := range worktreesInterface {
264+
worktreeMap, ok := worktreeInterface.(map[string]interface{})
265+
if !ok {
266+
return fmt.Errorf("worktrees config is not valid in .repositories[%v]", repoMap["remote"])
267+
}
268+
269+
if key := findUnexpectedKey(worktreeMap, allowedWorktreeKeys); key != "" {
270+
return fmt.Errorf("unexpected key: .repositories[%v].worktrees[%v].%v", repoMap["remote"], i, key)
271+
}
272+
273+
// Check "pathspecs" in each worktree
274+
if pathspecsInterface, exists := worktreeMap["pathspecs"]; exists {
275+
if _, ok := pathspecsInterface.([]interface{}); !ok {
276+
return fmt.Errorf("pathspecs config must be an array in .repositories[%v].worktrees[%v]", repoMap["remote"], i)
277+
}
278+
}
279+
}
280+
}
281+
282+
return nil
283+
}
284+
285+
// getAllowedKeys retrieves a list of allowed keys from the specified struct
286+
func getAllowedKeys(config interface{}) []string {
287+
var allowedKeys []string
288+
val := reflect.ValueOf(config)
289+
typ := reflect.TypeOf(config)
290+
291+
for i := 0; i < val.NumField(); i++ {
292+
field := typ.Field(i)
293+
yamlTag := field.Tag.Get("yaml")
294+
if yamlTag != "" {
295+
allowedKeys = append(allowedKeys, yamlTag)
296+
}
297+
}
298+
return allowedKeys
299+
}
300+
301+
func findUnexpectedKey(raw map[string]interface{}, allowedKeys []string) string {
302+
for key := range raw {
303+
if !slices.Contains(allowedKeys, key) {
304+
return key
305+
}
306+
}
307+
308+
return ""
309+
}
310+
185311
// diffRepositories will do the diff between current state and new config and
186312
// return new repositories config and list of remote url which are not found in config
187313
func diffRepositories(repoPool *mirror.RepoPool, newConfig *mirror.RepoPoolConfig) (

config_test.go

Lines changed: 178 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
)
1212

1313
func Test_diffRepositories(t *testing.T) {
14-
1514
tests := []struct {
1615
name string
1716
initialConfig *mirror.RepoPoolConfig
@@ -199,7 +198,6 @@ func Test_diffWorktrees(t *testing.T) {
199198
}
200199
for _, tt := range tests {
201200
t.Run(tt.name, func(t *testing.T) {
202-
203201
if err := tt.initialRepoConf.PopulateEmptyLinkPaths(); err != nil {
204202
t.Fatalf("failed to create repo error = %v", err)
205203
}
@@ -234,3 +232,181 @@ func Test_diffWorktrees(t *testing.T) {
234232
})
235233
}
236234
}
235+
236+
func Test_validateConfigYaml(t *testing.T) {
237+
tests := []struct {
238+
name string
239+
yamlData []byte
240+
wantError bool
241+
}{
242+
{
243+
name: "valid - full config",
244+
yamlData: []byte(`
245+
defaults:
246+
root: /tmp/git-mirror
247+
link_root: /tmp/links
248+
interval: 30s
249+
mirror_timeout: 2m
250+
git_gc: always
251+
auth:
252+
ssh_key_path: /etc/git-secret/ssh
253+
ssh_known_hosts_path: /etc/git-secret/known_hosts
254+
255+
repositories:
256+
- remote: https://github.com/utilitywarehouse/git-mirror
257+
worktrees:
258+
- link: aaa
259+
ref: main
260+
- link: bbb
261+
ref: main
262+
- remote: https://github.com/utilitywarehouse/another-repo
263+
root: /some/other/location
264+
link_root: /some/path
265+
interval: 1m
266+
mirror_timeout: 5m
267+
git_gc: always
268+
auth:
269+
ssh_key_path: /some/other/location
270+
ssh_known_hosts_path: /some/other/location
271+
worktrees:
272+
- link: alerts
273+
ref: main
274+
pathspecs:
275+
- path
276+
- path2/*.yaml
277+
`),
278+
wantError: false,
279+
},
280+
{
281+
name: "valid - empty config",
282+
yamlData: []byte(`
283+
`),
284+
wantError: false,
285+
},
286+
{
287+
name: "valid - defaults config only",
288+
yamlData: []byte(`
289+
defaults:
290+
`),
291+
wantError: false,
292+
},
293+
{
294+
name: "valid - repositories config only",
295+
yamlData: []byte(`
296+
repositories:
297+
`),
298+
wantError: false,
299+
},
300+
{
301+
name: "invalid - unexpected key",
302+
yamlData: []byte(`
303+
not-valid:
304+
test: test
305+
306+
defaults:
307+
root: /tmp/git-mirror
308+
309+
repositories:
310+
- remote: https://github.com/utilitywarehouse/git-mirror
311+
`),
312+
wantError: true,
313+
},
314+
{
315+
name: "invalid - unexpected key in defaults",
316+
yamlData: []byte(`
317+
defaults:
318+
root: /tmp/git-mirror
319+
not_valid: test
320+
321+
repositories:
322+
- remote: https://github.com/utilitywarehouse/git-mirror
323+
`),
324+
wantError: true,
325+
},
326+
{
327+
name: "invalid - unexpected key in auth",
328+
yamlData: []byte(`
329+
defaults:
330+
root: /tmp/git-mirror
331+
auth:
332+
not_valid: test
333+
334+
repositories:
335+
- remote: https://github.com/utilitywarehouse/git-mirror
336+
`),
337+
wantError: true,
338+
},
339+
{
340+
name: "invalid - unexpected key in repositories",
341+
yamlData: []byte(`
342+
defaults:
343+
root: /tmp/git-mirror
344+
345+
repositories:
346+
- remote: https://github.com/utilitywarehouse/git-mirror
347+
not_valid: test
348+
`),
349+
wantError: true,
350+
},
351+
{
352+
name: "invalid - unexpected key in repository worktrees",
353+
yamlData: []byte(`
354+
defaults:
355+
root: /tmp/git-mirror
356+
357+
repositories:
358+
- remote: https://github.com/utilitywarehouse/git-mirror
359+
worktrees:
360+
- link: aaa
361+
not_valid: test
362+
`),
363+
wantError: true,
364+
},
365+
{
366+
name: "invalid - repositories is not an array",
367+
yamlData: []byte(`
368+
defaults:
369+
root: /tmp/git-mirror
370+
371+
repositories: https://github.com/utilitywarehouse/git-mirror
372+
`),
373+
wantError: true,
374+
},
375+
{
376+
name: "invalid - worktrees is not an array",
377+
yamlData: []byte(`
378+
defaults:
379+
root: /tmp/git-mirror
380+
381+
repositories:
382+
- remote: https://github.com/utilitywarehouse/git-mirror
383+
worktrees: test
384+
`),
385+
wantError: true,
386+
},
387+
{
388+
name: "invalid - pathspecs is not an array",
389+
yamlData: []byte(`
390+
defaults:
391+
root: /tmp/git-mirror
392+
393+
repositories:
394+
- remote: https://github.com/utilitywarehouse/git-mirror
395+
worktrees:
396+
- link: aaa
397+
not_valid: test
398+
pathspecs: readme.md
399+
`),
400+
wantError: true,
401+
},
402+
}
403+
404+
for _, tt := range tests {
405+
t.Run(tt.name, func(t *testing.T) {
406+
err := validateConfigYaml(tt.yamlData)
407+
if (err != nil) != tt.wantError {
408+
t.Errorf("validateConfigYaml() error = %v, wantError %v", err, tt.wantError)
409+
}
410+
})
411+
}
412+
}

0 commit comments

Comments
 (0)