-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This takes a page out of k8s' and systemd's playbooks and implements drop-in configs. The method of handling JSON and YAML is borrowed from k8s, but simplified because the configurations aren't arbitrary. The drop-in scheme is inspired by systemd's drop-in overrides. This allows for operators to modularize the config and put secrets in a different file than the main config. It also allows for easier configuration skew -- please note that running multiple different configurations in the same system is not supported. Signed-off-by: Hank Donnay <[email protected]>
- Loading branch information
Showing
28 changed files
with
569 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
package cmd | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io/fs" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
jsonpatch "github.com/evanphx/json-patch/v5" | ||
"github.com/quay/clair/config" | ||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
// LoadConfig loads the named config file or reports an error. | ||
// | ||
// JSON and YAML formatted files are supported, as determined by the file extension ("json" or "yaml" -- "yml" is not supported). | ||
// If a directory suffixed with ".d" exists (e.g. a file "config.json" and a directory "config.json.d"), | ||
// then all files with the same extension or the same extension suffixed with "-patch" will be loaded in lexical order and merged with the main configuration or applied as an RFC6902 patch, respectively. | ||
// | ||
// For example, given the paths: | ||
// | ||
// config.yaml | ||
// config.yaml.d/ | ||
// config.yaml.d/secrets.yaml | ||
// config.yaml.d/override.yaml-patch | ||
// config.yaml.d/unloved.json-patch | ||
// | ||
// "Config.yaml" will be the base config, | ||
// "override.yaml-patch" will be applied as a patch to the base config, | ||
// "secrets.yaml" will be merged into the base config, | ||
// and "unloved.json-patch" will be ignored. | ||
// | ||
// The "strict" argument controls whether the function returns on the first | ||
// error, or runs the full routine and returns all accumulated errors at the | ||
// end. | ||
func LoadConfig(cfg *config.Config, name string, strict bool) error { | ||
// This function would probably benefit from some logging, but the logging | ||
// configuration is specified _inside_ the configuration, so it's hard to | ||
// say what should be done here. | ||
name = filepath.Clean(name) | ||
ext := filepath.Ext(name) | ||
switch ext { | ||
case ".yaml": // OK | ||
case ".json": // OK | ||
default: | ||
return fmt.Errorf("unknown config kind %q", ext) | ||
} | ||
var errs []error | ||
|
||
b, err := loadAsJSON(name) | ||
if err != nil { | ||
if strict { | ||
return err | ||
} | ||
errs = append(errs, err) | ||
} | ||
dropinDir := name + ".d" | ||
err = filepath.WalkDir(dropinDir, func(path string, d fs.DirEntry, err error) error { | ||
switch { | ||
case path == dropinDir: | ||
return nil | ||
case !errors.Is(err, nil): | ||
return fmt.Errorf("error walking filesystem: %w", err) | ||
case d.IsDir(): | ||
return fs.SkipDir | ||
} | ||
// After this, make sure everything assigns errors to "err" so that the | ||
// non-strict behavior works. | ||
|
||
var doc []byte | ||
switch dext := filepath.Ext(path); { | ||
case dext == ext: | ||
doc, err = loadAsJSON(path) | ||
if err != nil { | ||
break | ||
} | ||
b, err = jsonpatch.MergePatch(b, doc) | ||
if err != nil { | ||
err = fmt.Errorf("error merging drop-in %q: %w", path, err) | ||
break | ||
} | ||
case dext == ext+"-patch": | ||
doc, err = loadAsJSON(path) | ||
if err != nil { | ||
break | ||
} | ||
var p jsonpatch.Patch | ||
p, err = jsonpatch.DecodePatch(doc) | ||
if err != nil { | ||
err = fmt.Errorf("bad patch %q: %w", path, err) | ||
break | ||
} | ||
b, err = p.Apply(b) | ||
if err != nil { | ||
err = fmt.Errorf("error applying patch %q: %w", path, err) | ||
break | ||
} | ||
} | ||
if err != nil { | ||
if strict { | ||
return err | ||
} | ||
errs = append(errs, err) | ||
} | ||
return nil | ||
}) | ||
switch { | ||
case errors.Is(err, nil): | ||
case errors.Is(err, fs.ErrNotExist): // OK | ||
case strict: | ||
return err | ||
default: | ||
errs = append(errs, err) | ||
} | ||
|
||
if len(b) == 0 { | ||
err := fmt.Errorf("error load config %q: empty document after merges", name) | ||
if strict { | ||
return err | ||
} | ||
errs = append(errs, err) | ||
} | ||
dec := json.NewDecoder(bytes.NewReader(b)) | ||
dec.DisallowUnknownFields() | ||
if err := dec.Decode(cfg); err != nil { | ||
// Hide that this error is coming from the `json` package, as it might | ||
// confuse people. | ||
err := fmt.Errorf("error decoding config %q: %s", name, strings.TrimPrefix(err.Error(), `json: `)) | ||
if strict { | ||
return err | ||
} | ||
errs = append(errs, err) | ||
} | ||
return errors.Join(errs...) | ||
} | ||
|
||
func loadAsJSON(path string) ([]byte, error) { | ||
b, err := os.ReadFile(path) | ||
if err != nil { | ||
return nil, fmt.Errorf("error reading file %q: %w", path, err) | ||
} | ||
ext := filepath.Ext(path) | ||
switch ext { | ||
case ".json", ".json-patch": | ||
if len(b) < 2 { | ||
return nil, fmt.Errorf("malformed file %q: not a JSON document", path) | ||
} | ||
case ".yaml", ".yaml-patch": | ||
var y interface{} | ||
if err := yaml.Unmarshal(b, &y); err != nil { | ||
msg := strings.TrimPrefix(err.Error(), `yaml: `) | ||
return nil, fmt.Errorf("malformed file %q: %v", path, msg) | ||
} | ||
// For arbitrary yaml documents we'd have to do a step to ensure there's | ||
// no disallowed constructs (binary keys, binary data tags) but we know | ||
// this should only ever be some snippet of our config.Config type. | ||
b, err = json.Marshal(y) | ||
if err != nil { // Not sure how this would happen. 🤔 | ||
msg := strings.TrimPrefix(err.Error(), `json: `) | ||
return nil, fmt.Errorf("malformed file %q: %s", path, msg) | ||
} | ||
default: | ||
panic("programmer error: called on bad path") | ||
} | ||
switch ext { | ||
case ".json": | ||
if b[0] != '{' { | ||
return nil, fmt.Errorf("malformed file %q: not a JSON object", path) | ||
} | ||
case ".json-patch", ".yaml-patch": | ||
if b[0] != '[' { | ||
return nil, fmt.Errorf("malformed file %q: not a patch document", path) | ||
} | ||
case ".yaml": | ||
if b[0] != '{' { | ||
// If this was an empty file (for some reason), note it and return an | ||
// empty JSON object. This can't happen with JSON -- we checked if it | ||
// meets the minimum size above. | ||
b = []byte("{}") | ||
} | ||
} | ||
return b, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package cmd_test | ||
|
||
import ( | ||
"encoding/json" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/quay/clair/config" | ||
|
||
"github.com/quay/clair/v4/cmd" | ||
) | ||
|
||
func TestLoadConfig(t *testing.T) { | ||
ms, err := filepath.Glob(`testdata/*/config.*[^d]`) | ||
if err != nil { | ||
panic("programmer error") | ||
} | ||
for _, m := range ms { | ||
name := filepath.Base(filepath.Dir(m)) | ||
t.Run(name, func(t *testing.T) { | ||
wantpath := filepath.Join(filepath.Dir(m), "want.json") | ||
wf, err := os.Open(wantpath) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
defer wf.Close() | ||
var got, want config.Config | ||
if err := json.NewDecoder(wf).Decode(&want); err != nil { | ||
t.Error(err) | ||
} | ||
if err := cmd.LoadConfig(&got, m, true); err != nil { | ||
t.Error(err) | ||
} | ||
if !cmp.Equal(got, want) { | ||
t.Error(cmp.Diff(got, want)) | ||
} | ||
}) | ||
} | ||
ms, err = filepath.Glob(`testdata/Error/*[^d]`) | ||
if err != nil { | ||
panic("programmer error") | ||
} | ||
t.Run("Error", func(t *testing.T) { | ||
for _, m := range ms { | ||
name := filepath.Base(m) | ||
name = strings.TrimSuffix(name, filepath.Ext(name)) | ||
t.Run(name, func(t *testing.T) { | ||
var got config.Config | ||
err := cmd.LoadConfig(&got, m, false) | ||
t.Log(err) | ||
if err == nil { | ||
t.Fail() | ||
} | ||
}) | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"http_listen_addr": ":80", | ||
"log_level": "error", | ||
"matcher": {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"log_level": "error" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"http_listen_addr": ":80", | ||
"log_level": "error" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
--- | ||
log_level: debug-color | ||
introspection_addr: ":8089" | ||
http_listen_addr: ":6060" | ||
updaters: | ||
sets: | ||
- ubuntu | ||
- debian | ||
- rhel | ||
- alpine | ||
auth: | ||
psk: | ||
key: 'c2VjcmV0' | ||
iss: | ||
- quay | ||
- clairctl | ||
indexer: | ||
connstring: host=clair-database user=clair dbname=indexer sslmode=disable | ||
scanlock_retry: 10 | ||
layer_scan_concurrency: 5 | ||
migrations: true | ||
matcher: | ||
indexer_addr: http://clair-indexer:6060/ | ||
connstring: host=clair-database user=clair dbname=matcher sslmode=disable | ||
max_conn_pool: 100 | ||
migrations: true | ||
matchers: {} | ||
notifier: | ||
indexer_addr: http://clair-indexer:6060/ | ||
matcher_addr: http://clair-matcher:6060/ | ||
connstring: host=clair-database user=clair dbname=notifier sslmode=disable | ||
migrations: true | ||
delivery_interval: 30s | ||
poll_interval: 1m | ||
webhook: | ||
target: "http://webhook-target/" | ||
callback: "http://clair-notifier:6060/notifier/api/v1/notification/" | ||
# amqp: | ||
# direct: true | ||
# exchange: | ||
# name: "" | ||
# type: "direct" | ||
# durable: true | ||
# auto_delete: false | ||
# uris: ["amqp://guest:guest@clair-rabbitmq:5672/"] | ||
# routing_key: "notifications" | ||
# callback: "http://clair-notifier/notifier/api/v1/notifications" | ||
# tracing and metrics config | ||
trace: | ||
name: "jaeger" | ||
# probability: 1 | ||
jaeger: | ||
agent: | ||
endpoint: "clair-jaeger:6831" | ||
service_name: "clair" | ||
metrics: | ||
name: "prometheus" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
log_level: null |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# This file is ignored -- look at all this invalid JSON! | ||
[ | ||
{ | ||
"op": "add", | ||
"path": "/updaters/sets/-", | ||
"value": "osv", | ||
}, | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
log_level: panic | ||
updaters: | ||
sets: | ||
- rhel |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
- op: add | ||
path: /updaters/sets/- | ||
value: osv |
Oops, something went wrong.