177 lines
5.4 KiB
Go
177 lines
5.4 KiB
Go
// Copyright 2020 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package config implements helper functions to parse OPA's configuration.
|
|
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"sigs.k8s.io/yaml"
|
|
|
|
"github.com/open-policy-agent/opa/internal/strvals"
|
|
"github.com/open-policy-agent/opa/v1/keys"
|
|
"github.com/open-policy-agent/opa/v1/logging"
|
|
"github.com/open-policy-agent/opa/v1/plugins/rest"
|
|
"github.com/open-policy-agent/opa/v1/tracing"
|
|
"github.com/open-policy-agent/opa/v1/util"
|
|
)
|
|
|
|
// ServiceOptions stores the options passed to ParseServicesConfig
|
|
type ServiceOptions struct {
|
|
Raw json.RawMessage
|
|
AuthPlugin rest.AuthPluginLookupFunc
|
|
Keys map[string]*keys.Config
|
|
Logger logging.Logger
|
|
DistributedTacingOpts tracing.Options
|
|
}
|
|
|
|
// ParseServicesConfig returns a set of named service clients. The service
|
|
// clients can be specified either as an array or as a map. Some systems (e.g.,
|
|
// Helm) do not have proper support for configuration values nested under
|
|
// arrays, so just support both here.
|
|
func ParseServicesConfig(opts ServiceOptions) (map[string]rest.Client, error) {
|
|
|
|
services := map[string]rest.Client{}
|
|
|
|
var arr []json.RawMessage
|
|
var obj map[string]json.RawMessage
|
|
|
|
if err := util.Unmarshal(opts.Raw, &arr); err == nil {
|
|
for _, s := range arr {
|
|
client, err := rest.New(s, opts.Keys, rest.AuthPluginLookup(opts.AuthPlugin), rest.Logger(opts.Logger), rest.DistributedTracingOpts(opts.DistributedTacingOpts))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
services[client.Service()] = client
|
|
}
|
|
} else if util.Unmarshal(opts.Raw, &obj) == nil {
|
|
for k := range obj {
|
|
client, err := rest.New(obj[k], opts.Keys, rest.Name(k), rest.AuthPluginLookup(opts.AuthPlugin), rest.Logger(opts.Logger), rest.DistributedTracingOpts(opts.DistributedTacingOpts))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
services[client.Service()] = client
|
|
}
|
|
} else {
|
|
// Return error from array decode as that is the default format.
|
|
return nil, err
|
|
}
|
|
|
|
return services, nil
|
|
}
|
|
|
|
// Load implements configuration file loading. The supplied config file will be
|
|
// read from disk (if specified) and overrides will be applied. If no config file is
|
|
// specified, the overrides can still be applied to an empty config.
|
|
func Load(configFile string, overrides []string, overrideFiles []string) ([]byte, error) {
|
|
baseConf := map[string]any{}
|
|
|
|
// User specified config file
|
|
if configFile != "" {
|
|
var bytes []byte
|
|
var err error
|
|
bytes, err = os.ReadFile(configFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
processedConf := subEnvVars(string(bytes))
|
|
|
|
if err := yaml.Unmarshal([]byte(processedConf), &baseConf); err != nil {
|
|
return nil, fmt.Errorf("failed to parse %s: %s", configFile, err)
|
|
}
|
|
}
|
|
|
|
overrideConf := map[string]any{}
|
|
|
|
// User specified a config override via --set
|
|
for _, override := range overrides {
|
|
processedOverride := subEnvVars(override)
|
|
if err := strvals.ParseInto(processedOverride, overrideConf); err != nil {
|
|
return nil, fmt.Errorf("failed parsing --set data: %s", err)
|
|
}
|
|
}
|
|
|
|
// User specified a config override value via --set-file
|
|
for _, override := range overrideFiles {
|
|
reader := func(rs []rune) (any, error) {
|
|
bytes, err := os.ReadFile(string(rs))
|
|
value := strings.TrimSpace(string(bytes))
|
|
return value, err
|
|
}
|
|
if err := strvals.ParseIntoFile(override, overrideConf, reader); err != nil {
|
|
return nil, fmt.Errorf("failed parsing --set-file data: %s", err)
|
|
}
|
|
}
|
|
|
|
// Merge together base config file and overrides, prefer the overrides
|
|
conf := mergeValues(baseConf, overrideConf)
|
|
|
|
// Take the patched config and marshal back to YAML
|
|
return yaml.Marshal(conf)
|
|
}
|
|
|
|
// regex looking for ${...} notation strings
|
|
var envRegex = regexp.MustCompile(`(?U:\${.*})`)
|
|
|
|
// SubEnvVars will look for any environment variables in the passed in string
|
|
// with the syntax of ${VAR_NAME} and replace that string with ENV[VAR_NAME]
|
|
func SubEnvVars(s string) string {
|
|
return subEnvVars(s)
|
|
}
|
|
|
|
func subEnvVars(s string) string {
|
|
updatedConfig := envRegex.ReplaceAllStringFunc(s, func(s string) string {
|
|
// Trim off the '${' and '}'
|
|
if len(s) <= 3 {
|
|
// This should never happen..
|
|
return ""
|
|
}
|
|
varName := s[2 : len(s)-1]
|
|
|
|
// Lookup the variable in the environment. We do not
|
|
// play by bash rules: if its undefined we'll keep it
|
|
// as-is, it could be replaced somewhere down the line.
|
|
// If it's set to "", we'll return that.
|
|
if lu, ok := os.LookupEnv(varName); ok {
|
|
return lu
|
|
}
|
|
return s
|
|
})
|
|
|
|
return updatedConfig
|
|
}
|
|
|
|
// mergeValues will merge source and destination map, preferring values from the source map
|
|
func mergeValues(dest map[string]any, src map[string]any) map[string]any {
|
|
for k, v := range src {
|
|
// If the key doesn't exist already, then just set the key to that value
|
|
if _, exists := dest[k]; !exists {
|
|
dest[k] = v
|
|
continue
|
|
}
|
|
nextMap, ok := v.(map[string]any)
|
|
// If it isn't another map, overwrite the value
|
|
if !ok {
|
|
dest[k] = v
|
|
continue
|
|
}
|
|
// Edge case: If the key exists in the destination, but isn't a map
|
|
destMap, isMap := dest[k].(map[string]any)
|
|
// If the source map has a map for this key, prefer it
|
|
if !isMap {
|
|
dest[k] = v
|
|
continue
|
|
}
|
|
// If we got to this point, it is a map in both, so merge them
|
|
dest[k] = mergeValues(destMap, nextMap)
|
|
}
|
|
return dest
|
|
}
|