@@ -3,8 +3,11 @@ package main
3
3
import (
4
4
"context"
5
5
"errors"
6
+ "fmt"
6
7
"os"
7
8
"path"
9
+ "reflect"
10
+ "slices"
8
11
"time"
9
12
10
13
"github.com/prometheus/client_golang/prometheus"
32
35
Name : "git_mirror_config_last_reload_success_timestamp_seconds" ,
33
36
Help : "Timestamp of the last successful configuration reload." ,
34
37
})
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 {})
35
43
)
36
44
37
45
// WatchConfig polls the config file every interval and reloads if modified
@@ -174,14 +182,132 @@ func parseConfigFile(path string) (*mirror.RepoPoolConfig, error) {
174
182
if err != nil {
175
183
return nil , err
176
184
}
185
+
186
+ err = validateConfigYaml ([]byte (yamlFile ))
187
+ if err != nil {
188
+ return nil , err
189
+ }
190
+
177
191
conf := & mirror.RepoPoolConfig {}
178
192
err = yaml .Unmarshal (yamlFile , conf )
179
193
if err != nil {
180
194
return nil , err
181
195
}
196
+
182
197
return conf , nil
183
198
}
184
199
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
+
185
311
// diffRepositories will do the diff between current state and new config and
186
312
// return new repositories config and list of remote url which are not found in config
187
313
func diffRepositories (repoPool * mirror.RepoPool , newConfig * mirror.RepoPoolConfig ) (
0 commit comments