Agent Auto Configuration: Configuration Syntax Updates (#8003)
This commit is contained in:
parent
98effaf69d
commit
d994dc7b35
|
@ -17,11 +17,15 @@ import (
|
|||
"github.com/hashicorp/consul/agent/checks"
|
||||
"github.com/hashicorp/consul/agent/connect/ca"
|
||||
"github.com/hashicorp/consul/agent/consul"
|
||||
"github.com/hashicorp/consul/agent/consul/authmethod/ssoauth"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/ipaddr"
|
||||
"github.com/hashicorp/consul/lib"
|
||||
libtempl "github.com/hashicorp/consul/lib/template"
|
||||
"github.com/hashicorp/consul/tlsutil"
|
||||
"github.com/hashicorp/consul/types"
|
||||
"github.com/hashicorp/go-bexpr"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/go-sockaddr/template"
|
||||
"github.com/hashicorp/memberlist"
|
||||
|
@ -905,6 +909,7 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
|
|||
AutoEncryptDNSSAN: autoEncryptDNSSAN,
|
||||
AutoEncryptIPSAN: autoEncryptIPSAN,
|
||||
AutoEncryptAllowTLS: autoEncryptAllowTLS,
|
||||
AutoConfig: b.autoConfigVal(c.AutoConfig),
|
||||
ConnectEnabled: connectEnabled,
|
||||
ConnectCAProvider: connectCAProvider,
|
||||
ConnectCAConfig: connectCAConfig,
|
||||
|
@ -1285,6 +1290,10 @@ func (b *Builder) Validate(rt RuntimeConfig) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := b.validateAutoConfig(rt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1918,6 +1927,142 @@ func (b *Builder) isUnixAddr(a net.Addr) bool {
|
|||
return a != nil && ok
|
||||
}
|
||||
|
||||
func (b *Builder) autoConfigVal(raw AutoConfigRaw) AutoConfig {
|
||||
var val AutoConfig
|
||||
|
||||
val.Enabled = b.boolValWithDefault(raw.Enabled, false)
|
||||
val.IntroToken = b.stringVal(raw.IntroToken)
|
||||
val.IntroTokenFile = b.stringVal(raw.IntroTokenFile)
|
||||
// These can be go-discover values and so don't have to resolve fully yet
|
||||
val.ServerAddresses = b.expandAllOptionalAddrs("auto_config.server_addresses", raw.ServerAddresses)
|
||||
val.DNSSANs = raw.DNSSANs
|
||||
|
||||
for _, i := range raw.IPSANs {
|
||||
ip := net.ParseIP(i)
|
||||
if ip == nil {
|
||||
b.warn(fmt.Sprintf("Cannot parse ip %q from auto_config.ip_sans", i))
|
||||
continue
|
||||
}
|
||||
val.IPSANs = append(val.IPSANs, ip)
|
||||
}
|
||||
|
||||
val.Authorizer = b.autoConfigAuthorizerVal(raw.Authorizer)
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func (b *Builder) autoConfigAuthorizerVal(raw AutoConfigAuthorizerRaw) AutoConfigAuthorizer {
|
||||
var val AutoConfigAuthorizer
|
||||
|
||||
val.Enabled = b.boolValWithDefault(raw.Enabled, false)
|
||||
val.ClaimAssertions = raw.ClaimAssertions
|
||||
val.AllowReuse = b.boolValWithDefault(raw.AllowReuse, false)
|
||||
val.AuthMethod = structs.ACLAuthMethod{
|
||||
Name: "Auto Config Authorizer",
|
||||
Type: "jwt",
|
||||
// TODO (autoconf) - Configurable token TTL
|
||||
MaxTokenTTL: 72 * time.Hour,
|
||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
Config: map[string]interface{}{
|
||||
"JWTSupportedAlgs": raw.JWTSupportedAlgs,
|
||||
"BoundAudiences": raw.BoundAudiences,
|
||||
"ClaimMappings": raw.ClaimMappings,
|
||||
"ListClaimMappings": raw.ListClaimMappings,
|
||||
"OIDCDiscoveryURL": b.stringVal(raw.OIDCDiscoveryURL),
|
||||
"OIDCDiscoveryCACert": b.stringVal(raw.OIDCDiscoveryCACert),
|
||||
"JWKSURL": b.stringVal(raw.JWKSURL),
|
||||
"JWKSCACert": b.stringVal(raw.JWKSCACert),
|
||||
"JWTValidationPubKeys": raw.JWTValidationPubKeys,
|
||||
"BoundIssuer": b.stringVal(raw.BoundIssuer),
|
||||
"ExpirationLeeway": b.durationVal("auto_config.authorizer.expiration_leeway", raw.ExpirationLeeway),
|
||||
"NotBeforeLeeway": b.durationVal("auto_config.authorizer.not_before_leeway", raw.NotBeforeLeeway),
|
||||
"ClockSkewLeeway": b.durationVal("auto_config.authorizer.clock_skew_leeway", raw.ClockSkewLeeway),
|
||||
},
|
||||
// should be unnecessary as we aren't using the typical login process to create tokens but this is our
|
||||
// desired mode regardless so if it ever did matter its probably better to be explicit.
|
||||
TokenLocality: "local",
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func (b *Builder) validateAutoConfig(rt RuntimeConfig) error {
|
||||
autoconf := rt.AutoConfig
|
||||
|
||||
if err := b.validateAutoConfigAuthorizer(rt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !autoconf.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Auto Config doesn't currently support configuring servers
|
||||
if rt.ServerMode {
|
||||
return fmt.Errorf("auto_config.enabled cannot be set to true for server agents.")
|
||||
}
|
||||
|
||||
// When both are set we will prefer the given value over the file.
|
||||
if autoconf.IntroToken != "" && autoconf.IntroTokenFile != "" {
|
||||
b.warn("auto_config.intro_token and auto_config.intro_token_file are both set. Using the value of auto_config.intro_token")
|
||||
} else if autoconf.IntroToken == "" && autoconf.IntroTokenFile == "" {
|
||||
return fmt.Errorf("one of auto_config.intro_token or auto_config.intro_token_file must be set to enable auto_config")
|
||||
}
|
||||
|
||||
if len(autoconf.ServerAddresses) == 0 {
|
||||
// TODO (autoconf) can we/should we infer this from the join/retry join addresses. I think no, as we will potentially
|
||||
// be overriding those retry join addresses with the autoconf process anyways.
|
||||
return fmt.Errorf("auto_config.enabled is set without providing a list of addresses")
|
||||
}
|
||||
|
||||
// TODO (autoconf) should we validate the DNS and IP SANs? The IP SANs have already been parsed into IPs
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) validateAutoConfigAuthorizer(rt RuntimeConfig) error {
|
||||
authz := rt.AutoConfig.Authorizer
|
||||
|
||||
if !authz.Enabled {
|
||||
return nil
|
||||
}
|
||||
// Auto Config Authorization is only supported on servers
|
||||
if !rt.ServerMode {
|
||||
return fmt.Errorf("auto_config.authorizer.enabled cannot be set to true for client agents")
|
||||
}
|
||||
|
||||
// build out the validator to ensure that the given configuration was valid
|
||||
null := hclog.NewNullLogger()
|
||||
validator, err := ssoauth.NewValidator(null, &authz.AuthMethod)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("auto_config.authorizer has invalid configuration: %v", err)
|
||||
}
|
||||
|
||||
// create a blank identity for use to validate the claim assertions.
|
||||
blankID := validator.NewIdentity()
|
||||
varMap := map[string]string{
|
||||
"node": "fake",
|
||||
"segment": "fake",
|
||||
}
|
||||
|
||||
// validate all the claim assertions
|
||||
for _, raw := range authz.ClaimAssertions {
|
||||
// validate any HIL
|
||||
filled, err := libtempl.InterpolateHIL(raw, varMap, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("auto_config.claim_assertion %q is invalid: %v", raw, err)
|
||||
}
|
||||
|
||||
// validate the bexpr syntax - note that for now all the keys mapped by the claim mappings
|
||||
// are not validateable due to them being put inside a map. Some bexpr updates to setup keys
|
||||
// from current map keys would probably be nice here.
|
||||
if _, err := bexpr.CreateEvaluatorForType(filled, nil, blankID.SelectableFields); err != nil {
|
||||
return fmt.Errorf("auto_config.claim_assertion %q is invalid: %v", raw, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// decodeBytes returns the encryption key decoded.
|
||||
func decodeBytes(key string) ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(key)
|
||||
|
|
|
@ -108,6 +108,7 @@ type Config struct {
|
|||
AdvertiseAddrWAN *string `json:"advertise_addr_wan,omitempty" hcl:"advertise_addr_wan" mapstructure:"advertise_addr_wan"`
|
||||
AdvertiseAddrWANIPv4 *string `json:"advertise_addr_wan_ipv4,omitempty" hcl:"advertise_addr_wan_ipv4" mapstructure:"advertise_addr_wan_ipv4"`
|
||||
AdvertiseAddrWANIPv6 *string `json:"advertise_addr_wan_ipv6,omitempty" hcl:"advertise_addr_wan_ipv6" mapstructure:"advertise_addr_ipv6"`
|
||||
AutoConfig AutoConfigRaw `json:"auto_config,omitempty" hcl:"auto_config" mapstructure:"auto_config"`
|
||||
Autopilot Autopilot `json:"autopilot,omitempty" hcl:"autopilot" mapstructure:"autopilot"`
|
||||
BindAddr *string `json:"bind_addr,omitempty" hcl:"bind_addr" mapstructure:"bind_addr"`
|
||||
Bootstrap *bool `json:"bootstrap,omitempty" hcl:"bootstrap" mapstructure:"bootstrap"`
|
||||
|
@ -693,3 +694,34 @@ type AuditSink struct {
|
|||
RotateDuration *string `json:"rotate_duration,omitempty" hcl:"rotate_duration" mapstructure:"rotate_duration"`
|
||||
RotateMaxFiles *int `json:"rotate_max_files,omitempty" hcl:"rotate_max_files" mapstructure:"rotate_max_files"`
|
||||
}
|
||||
|
||||
type AutoConfigRaw struct {
|
||||
Enabled *bool `json:"enabled,omitempty" hcl:"enabled" mapstructure:"enabled"`
|
||||
IntroToken *string `json:"intro_token,omitempty" hcl:"intro_token" mapstructure:"intro_token"`
|
||||
IntroTokenFile *string `json:"intro_token_file,omitempty" hcl:"intro_token_file" mapstructure:"intro_token_file"`
|
||||
ServerAddresses []string `json:"server_addresses,omitempty" hcl:"server_addresses" mapstructure:"server_addresses"`
|
||||
DNSSANs []string `json:"dns_sans,omitempty" hcl:"dns_sans" mapstructure:"dns_sans"`
|
||||
IPSANs []string `json:"ip_sans,omitempty" hcl:"ip_sans" mapstructure:"ip_sans"`
|
||||
Authorizer AutoConfigAuthorizerRaw `json:"authorizer,omitempty" hcl:"authorizer" mapstructure:"authorizer"`
|
||||
}
|
||||
|
||||
type AutoConfigAuthorizerRaw struct {
|
||||
Enabled *bool `json:"enabled,omitempty" hcl:"enabled" mapstructure:"enabled"`
|
||||
ClaimAssertions []string `json:"claim_assertions,omitempty" hcl:"claim_assertions" mapstructure:"claim_assertions"`
|
||||
AllowReuse *bool `json:"allow_reuse,omitempty" hcl:"allow_reuse" mapstructure:"allow_reuse"`
|
||||
|
||||
// Fields to be shared with the JWT Auth Method
|
||||
JWTSupportedAlgs []string `json:"jwt_supported_algs,omitempty" hcl:"jwt_supported_algs" mapstructure:"jwt_supported_algs"`
|
||||
BoundAudiences []string `json:"bound_audiences,omitempty" hcl:"bound_audiences" mapstructure:"bound_audiences"`
|
||||
ClaimMappings map[string]string `json:"claim_mappings,omitempty" hcl:"claim_mappings" mapstructure:"claim_mappings"`
|
||||
ListClaimMappings map[string]string `json:"list_claim_mappings,omitempty" hcl:"list_claim_mappings" mapstructure:"list_claim_mappings"`
|
||||
OIDCDiscoveryURL *string `json:"oidc_discovery_url,omitempty" hcl:"oidc_discovery_url" mapstructure:"oidc_discovery_url"`
|
||||
OIDCDiscoveryCACert *string `json:"oidc_discovery_ca_cert,omitempty" hcl:"oidc_discovery_ca_cert" mapstructure:"oidc_discovery_ca_cert"`
|
||||
JWKSURL *string `json:"jwks_url,omitempty" hcl:"jwks_url" mapstructure:"jwks_url"`
|
||||
JWKSCACert *string `json:"jwks_ca_cert,omitempty" hcl:"jwks_ca_cert" mapstructure:"jwks_ca_cert"`
|
||||
JWTValidationPubKeys []string `json:"jwt_validation_pub_keys,omitempty" hcl:"jwt_validation_pub_keys" mapstructure:"jwt_validation_pub_keys"`
|
||||
BoundIssuer *string `json:"bound_issuer,omitempty" hcl:"bound_issuer" mapstructure:"bound_issuer"`
|
||||
ExpirationLeeway *string `json:"expiration_leeway,omitempty" hcl:"expiration_leeway" mapstructure:"expiration_leeway"`
|
||||
NotBeforeLeeway *string `json:"not_before_leeway,omitempty" hcl:"not_before_leeway" mapstructure:"not_before_leeway"`
|
||||
ClockSkewLeeway *string `json:"clock_skew_leeway,omitempty" hcl:"clock_skew_leeway" mapstructure:"clock_skew_leeway"`
|
||||
}
|
||||
|
|
|
@ -28,6 +28,14 @@ func merge(a, b interface{}) interface{} {
|
|||
func mergeValue(a, b reflect.Value) reflect.Value {
|
||||
switch a.Kind() {
|
||||
case reflect.Map:
|
||||
// dont bother allocating a new map to aggregate keys in when either one
|
||||
// or both of the maps to merge is the zero value - nil
|
||||
if a.IsZero() {
|
||||
return b
|
||||
} else if b.IsZero() {
|
||||
return a
|
||||
}
|
||||
|
||||
r := reflect.MakeMap(a.Type())
|
||||
for _, k := range a.MapKeys() {
|
||||
v := a.MapIndex(k)
|
||||
|
|
|
@ -39,12 +39,7 @@ func TestMerge(t *testing.T) {
|
|||
"a": "b",
|
||||
"c": "e",
|
||||
},
|
||||
Ports: Ports{DNS: pInt(2), HTTP: pInt(3)},
|
||||
SnapshotAgent: map[string]interface{}{},
|
||||
TaggedAddresses: map[string]string{},
|
||||
HTTPConfig: HTTPConfig{ResponseHeaders: map[string]string{}},
|
||||
DNS: DNS{ServiceTTL: map[string]string{}},
|
||||
Connect: Connect{CAConfig: map[string]interface{}{}},
|
||||
Ports: Ports{DNS: pInt(2), HTTP: pInt(3)},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -540,6 +540,10 @@ type RuntimeConfig struct {
|
|||
// AutoEncrypt.Sign requests.
|
||||
AutoEncryptAllowTLS bool
|
||||
|
||||
// AutoConfig is a grouping of the configurations around the agent auto configuration
|
||||
// process including how servers can authorize requests.
|
||||
AutoConfig AutoConfig
|
||||
|
||||
// ConnectEnabled opts the agent into connect. It should be set on all clients
|
||||
// and servers in a cluster for correct connect operation.
|
||||
ConnectEnabled bool
|
||||
|
@ -1566,6 +1570,24 @@ type RuntimeConfig struct {
|
|||
EnterpriseRuntimeConfig
|
||||
}
|
||||
|
||||
type AutoConfig struct {
|
||||
Enabled bool
|
||||
IntroToken string
|
||||
IntroTokenFile string
|
||||
ServerAddresses []string
|
||||
DNSSANs []string
|
||||
IPSANs []net.IP
|
||||
Authorizer AutoConfigAuthorizer
|
||||
}
|
||||
|
||||
type AutoConfigAuthorizer struct {
|
||||
Enabled bool
|
||||
AuthMethod structs.ACLAuthMethod
|
||||
// AuthMethodConfig ssoauth.Config
|
||||
ClaimAssertions []string
|
||||
AllowReuse bool
|
||||
}
|
||||
|
||||
func (c *RuntimeConfig) apiAddresses(maxPerType int) (unixAddrs, httpAddrs, httpsAddrs []string) {
|
||||
if len(c.HTTPSAddrs) > 0 {
|
||||
for i, addr := range c.HTTPSAddrs {
|
||||
|
@ -1729,6 +1751,10 @@ func (c *RuntimeConfig) ToTLSUtilConfig() tlsutil.Config {
|
|||
// isSecret determines whether a field name represents a field which
|
||||
// may contain a secret.
|
||||
func isSecret(name string) bool {
|
||||
// special cases for AuthMethod locality and intro token file
|
||||
if name == "TokenLocality" || name == "IntroTokenFile" {
|
||||
return false
|
||||
}
|
||||
name = strings.ToLower(name)
|
||||
return strings.Contains(name, "key") || strings.Contains(name, "token") || strings.Contains(name, "secret")
|
||||
}
|
||||
|
|
|
@ -1628,6 +1628,7 @@ func TestConfigFlagsAndEdgecases(t *testing.T) {
|
|||
},
|
||||
warns: []string{`config key "acl_enforce_version_8" is deprecated and should be removed`},
|
||||
},
|
||||
|
||||
{
|
||||
desc: "advertise address detect fails v4",
|
||||
args: []string{`-data-dir=` + dataDir},
|
||||
|
@ -3795,6 +3796,272 @@ func TestConfigFlagsAndEdgecases(t *testing.T) {
|
|||
rt.RPCMaxConnsPerClient = 100
|
||||
},
|
||||
},
|
||||
|
||||
///////////////////////////////////
|
||||
// Auto Config related tests
|
||||
{
|
||||
desc: "auto config not allowed for servers",
|
||||
args: []string{
|
||||
`-data-dir=` + dataDir,
|
||||
},
|
||||
hcl: []string{`
|
||||
server = true
|
||||
auto_config {
|
||||
enabled = true
|
||||
intro_token = "blah"
|
||||
server_addresses = ["198.18.0.1"]
|
||||
}
|
||||
`},
|
||||
json: []string{`
|
||||
{
|
||||
"server": true,
|
||||
"auto_config": {
|
||||
"enabled": true,
|
||||
"intro_token": "blah",
|
||||
"server_addresses": ["198.18.0.1"]
|
||||
}
|
||||
}`},
|
||||
err: "auto_config.enabled cannot be set to true for server agents",
|
||||
},
|
||||
|
||||
{
|
||||
desc: "auto config no intro token",
|
||||
args: []string{
|
||||
`-data-dir=` + dataDir,
|
||||
},
|
||||
hcl: []string{`
|
||||
auto_config {
|
||||
enabled = true
|
||||
server_addresses = ["198.18.0.1"]
|
||||
|
||||
}
|
||||
`},
|
||||
json: []string{`
|
||||
{
|
||||
"auto_config": {
|
||||
"enabled": true,
|
||||
"server_addresses": ["198.18.0.1"]
|
||||
}
|
||||
}`},
|
||||
err: "one of auto_config.intro_token or auto_config.intro_token_file must be set to enable auto_config",
|
||||
},
|
||||
|
||||
{
|
||||
desc: "auto config no server addresses",
|
||||
args: []string{
|
||||
`-data-dir=` + dataDir,
|
||||
},
|
||||
hcl: []string{`
|
||||
auto_config {
|
||||
enabled = true
|
||||
intro_token = "blah"
|
||||
}
|
||||
`},
|
||||
json: []string{`
|
||||
{
|
||||
"auto_config": {
|
||||
"enabled": true,
|
||||
"intro_token": "blah"
|
||||
}
|
||||
}`},
|
||||
err: "auto_config.enabled is set without providing a list of addresses",
|
||||
},
|
||||
|
||||
{
|
||||
desc: "auto config client",
|
||||
args: []string{
|
||||
`-data-dir=` + dataDir,
|
||||
},
|
||||
hcl: []string{`
|
||||
auto_config {
|
||||
enabled = true
|
||||
intro_token = "blah"
|
||||
intro_token_file = "blah"
|
||||
server_addresses = ["198.18.0.1"]
|
||||
dns_sans = ["foo"]
|
||||
ip_sans = ["invalid", "127.0.0.1"]
|
||||
}
|
||||
`},
|
||||
json: []string{`
|
||||
{
|
||||
"auto_config": {
|
||||
"enabled": true,
|
||||
"intro_token": "blah",
|
||||
"intro_token_file": "blah",
|
||||
"server_addresses": ["198.18.0.1"],
|
||||
"dns_sans": ["foo"],
|
||||
"ip_sans": ["invalid", "127.0.0.1"]
|
||||
}
|
||||
}`},
|
||||
warns: []string{
|
||||
"Cannot parse ip \"invalid\" from auto_config.ip_sans",
|
||||
"auto_config.intro_token and auto_config.intro_token_file are both set. Using the value of auto_config.intro_token",
|
||||
},
|
||||
patch: func(rt *RuntimeConfig) {
|
||||
rt.AutoConfig.Enabled = true
|
||||
rt.AutoConfig.IntroToken = "blah"
|
||||
rt.AutoConfig.IntroTokenFile = "blah"
|
||||
rt.AutoConfig.ServerAddresses = []string{"198.18.0.1"}
|
||||
rt.AutoConfig.DNSSANs = []string{"foo"}
|
||||
rt.AutoConfig.IPSANs = []net.IP{net.IPv4(127, 0, 0, 1)}
|
||||
rt.DataDir = dataDir
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
desc: "auto config authorizer client not allowed",
|
||||
args: []string{
|
||||
`-data-dir=` + dataDir,
|
||||
},
|
||||
hcl: []string{`
|
||||
auto_config {
|
||||
authorizer {
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
`},
|
||||
json: []string{`
|
||||
{
|
||||
"auto_config": {
|
||||
"authorizer": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}`},
|
||||
err: "auto_config.authorizer.enabled cannot be set to true for client agents",
|
||||
},
|
||||
|
||||
{
|
||||
desc: "auto config authorizer invalid config",
|
||||
args: []string{
|
||||
`-data-dir=` + dataDir,
|
||||
`-server`,
|
||||
},
|
||||
hcl: []string{`
|
||||
auto_config {
|
||||
authorizer {
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
`},
|
||||
json: []string{`
|
||||
{
|
||||
"auto_config": {
|
||||
"authorizer": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}`},
|
||||
err: `auto_config.authorizer has invalid configuration: exactly one of 'JWTValidationPubKeys', 'JWKSURL', or 'OIDCDiscoveryURL' must be set for type "jwt"`,
|
||||
},
|
||||
|
||||
{
|
||||
desc: "auto config authorizer invalid config 2",
|
||||
args: []string{
|
||||
`-data-dir=` + dataDir,
|
||||
`-server`,
|
||||
},
|
||||
hcl: []string{`
|
||||
auto_config {
|
||||
authorizer {
|
||||
enabled = true
|
||||
jwks_url = "https://fake.uri.local"
|
||||
oidc_discovery_url = "https://fake.uri.local"
|
||||
}
|
||||
}
|
||||
`},
|
||||
json: []string{`
|
||||
{
|
||||
"auto_config": {
|
||||
"authorizer": {
|
||||
"enabled": true,
|
||||
"jwks_url": "https://fake.uri.local",
|
||||
"oidc_discovery_url": "https://fake.uri.local"
|
||||
}
|
||||
}
|
||||
}`},
|
||||
err: `auto_config.authorizer has invalid configuration: exactly one of 'JWTValidationPubKeys', 'JWKSURL', or 'OIDCDiscoveryURL' must be set for type "jwt"`,
|
||||
},
|
||||
|
||||
{
|
||||
desc: "auto config authorizer invalid claim assertion",
|
||||
args: []string{
|
||||
`-data-dir=` + dataDir,
|
||||
`-server`,
|
||||
},
|
||||
hcl: []string{`
|
||||
auto_config {
|
||||
authorizer {
|
||||
enabled = true
|
||||
jwt_validation_pub_keys = ["-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"]
|
||||
claim_assertions = [
|
||||
"values.node == ${node}"
|
||||
]
|
||||
}
|
||||
}
|
||||
`},
|
||||
json: []string{`
|
||||
{
|
||||
"auto_config": {
|
||||
"authorizer": {
|
||||
"enabled": true,
|
||||
"jwt_validation_pub_keys": ["-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"],
|
||||
"claim_assertions": [
|
||||
"values.node == ${node}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}`},
|
||||
err: `auto_config.claim_assertion "values.node == ${node}" is invalid: Selector "values" is not valid`,
|
||||
},
|
||||
{
|
||||
desc: "auto config authorizer ok",
|
||||
args: []string{
|
||||
`-data-dir=` + dataDir,
|
||||
`-server`,
|
||||
},
|
||||
hcl: []string{`
|
||||
auto_config {
|
||||
authorizer {
|
||||
enabled = true
|
||||
jwt_validation_pub_keys = ["-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"]
|
||||
claim_assertions = [
|
||||
"value.node == ${node}"
|
||||
]
|
||||
claim_mappings = {
|
||||
node = "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
`},
|
||||
json: []string{`
|
||||
{
|
||||
"auto_config": {
|
||||
"authorizer": {
|
||||
"enabled": true,
|
||||
"jwt_validation_pub_keys": ["-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"],
|
||||
"claim_assertions": [
|
||||
"value.node == ${node}"
|
||||
],
|
||||
"claim_mappings": {
|
||||
"node": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`},
|
||||
patch: func(rt *RuntimeConfig) {
|
||||
rt.AutoConfig.Authorizer.Enabled = true
|
||||
rt.AutoConfig.Authorizer.AuthMethod.Config["ClaimMappings"] = map[string]string{
|
||||
"node": "node",
|
||||
}
|
||||
rt.AutoConfig.Authorizer.AuthMethod.Config["JWTValidationPubKeys"] = []string{"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"}
|
||||
rt.AutoConfig.Authorizer.ClaimAssertions = []string{"value.node == ${node}"}
|
||||
rt.DataDir = dataDir
|
||||
rt.LeaveOnTerm = false
|
||||
rt.ServerMode = true
|
||||
rt.SkipLeaveOnInt = true
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testConfig(t, tests, dataDir)
|
||||
|
@ -4027,6 +4294,28 @@ func TestFullConfig(t *testing.T) {
|
|||
"audit": {
|
||||
"enabled": false
|
||||
},
|
||||
"auto_config": {
|
||||
"enabled": false,
|
||||
"intro_token": "OpBPGRwt",
|
||||
"intro_token_file": "gFvAXwI8",
|
||||
"dns_sans": ["6zdaWg9J"],
|
||||
"ip_sans": ["198.18.99.99"],
|
||||
"server_addresses": ["198.18.100.1"],
|
||||
"authorizer": {
|
||||
"enabled": true,
|
||||
"allow_reuse": true,
|
||||
"claim_mappings": {
|
||||
"node": "node"
|
||||
},
|
||||
"list_claim_mappings": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"bound_issuer": "consul",
|
||||
"bound_audiences": ["consul-cluster-1"],
|
||||
"claim_assertions": ["value.node == \"${node}\""],
|
||||
"jwt_validation_pub_keys": ["-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"]
|
||||
}
|
||||
},
|
||||
"autopilot": {
|
||||
"cleanup_dead_servers": true,
|
||||
"disable_upgrade_migration": true,
|
||||
|
@ -4663,6 +4952,28 @@ func TestFullConfig(t *testing.T) {
|
|||
audit = {
|
||||
enabled = false
|
||||
}
|
||||
auto_config = {
|
||||
enabled = false
|
||||
intro_token = "OpBPGRwt"
|
||||
intro_token_file = "gFvAXwI8"
|
||||
dns_sans = ["6zdaWg9J"]
|
||||
ip_sans = ["198.18.99.99"]
|
||||
server_addresses = ["198.18.100.1"]
|
||||
authorizer = {
|
||||
enabled = true
|
||||
allow_reuse = true
|
||||
claim_mappings = {
|
||||
node = "node"
|
||||
}
|
||||
list_claim_mappings = {
|
||||
foo = "bar"
|
||||
}
|
||||
bound_issuer = "consul"
|
||||
bound_audiences = ["consul-cluster-1"]
|
||||
claim_assertions = ["value.node == \"${node}\""]
|
||||
jwt_validation_pub_keys = ["-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"]
|
||||
}
|
||||
}
|
||||
autopilot = {
|
||||
cleanup_dead_servers = true
|
||||
disable_upgrade_migration = true
|
||||
|
@ -5496,10 +5807,49 @@ func TestFullConfig(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
AutoEncryptTLS: false,
|
||||
AutoEncryptDNSSAN: []string{"a.com", "b.com"},
|
||||
AutoEncryptIPSAN: []net.IP{net.ParseIP("192.168.4.139"), net.ParseIP("192.168.4.140")},
|
||||
AutoEncryptAllowTLS: true,
|
||||
AutoEncryptTLS: false,
|
||||
AutoEncryptDNSSAN: []string{"a.com", "b.com"},
|
||||
AutoEncryptIPSAN: []net.IP{net.ParseIP("192.168.4.139"), net.ParseIP("192.168.4.140")},
|
||||
AutoEncryptAllowTLS: true,
|
||||
AutoConfig: AutoConfig{
|
||||
Enabled: false,
|
||||
IntroToken: "OpBPGRwt",
|
||||
IntroTokenFile: "gFvAXwI8",
|
||||
DNSSANs: []string{"6zdaWg9J"},
|
||||
IPSANs: []net.IP{net.IPv4(198, 18, 99, 99)},
|
||||
ServerAddresses: []string{"198.18.100.1"},
|
||||
Authorizer: AutoConfigAuthorizer{
|
||||
Enabled: true,
|
||||
AllowReuse: true,
|
||||
ClaimAssertions: []string{"value.node == \"${node}\""},
|
||||
AuthMethod: structs.ACLAuthMethod{
|
||||
Name: "Auto Config Authorizer",
|
||||
Type: "jwt",
|
||||
MaxTokenTTL: 72 * time.Hour,
|
||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
Config: map[string]interface{}{
|
||||
"JWTValidationPubKeys": []string{"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"},
|
||||
"ClaimMappings": map[string]string{
|
||||
"node": "node",
|
||||
},
|
||||
"BoundIssuer": "consul",
|
||||
"BoundAudiences": []string{"consul-cluster-1"},
|
||||
"ListClaimMappings": map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
"OIDCDiscoveryURL": "",
|
||||
"OIDCDiscoveryCACert": "",
|
||||
"JWKSURL": "",
|
||||
"JWKSCACert": "",
|
||||
"ExpirationLeeway": 0 * time.Second,
|
||||
"NotBeforeLeeway": 0 * time.Second,
|
||||
"ClockSkewLeeway": 0 * time.Second,
|
||||
"JWTSupportedAlgs": []string(nil),
|
||||
},
|
||||
TokenLocality: "local",
|
||||
},
|
||||
},
|
||||
},
|
||||
ConnectEnabled: true,
|
||||
ConnectSidecarMinPort: 8888,
|
||||
ConnectSidecarMaxPort: 9999,
|
||||
|
@ -6621,7 +6971,35 @@ func TestSanitize(t *testing.T) {
|
|||
"AllowWriteHTTPFrom": [
|
||||
"127.0.0.0/8",
|
||||
"::1/128"
|
||||
]
|
||||
],
|
||||
"AutoConfig": {
|
||||
"Authorizer": {
|
||||
"Enabled": false,
|
||||
"AllowReuse": false,
|
||||
"AuthMethod": {
|
||||
"ACLAuthMethodEnterpriseFields": {},
|
||||
"Config": {},
|
||||
"Description": "",
|
||||
"DisplayName": "",
|
||||
"EnterpriseMeta": {},
|
||||
"MaxTokenTTL": "0s",
|
||||
"Name": "",
|
||||
"RaftIndex": {
|
||||
"CreateIndex": 0,
|
||||
"ModifyIndex": 0
|
||||
},
|
||||
"Type": "",
|
||||
"TokenLocality": ""
|
||||
},
|
||||
"ClaimAssertions": []
|
||||
},
|
||||
"Enabled": false,
|
||||
"DNSSANs": [],
|
||||
"IntroToken": "hidden",
|
||||
"IntroTokenFile": "",
|
||||
"IPSANs": [],
|
||||
"ServerAddresses": []
|
||||
}
|
||||
}`
|
||||
b, err := json.MarshalIndent(rt.Sanitized(), "", " ")
|
||||
if err != nil {
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/lib"
|
||||
"github.com/hashicorp/consul/lib/template"
|
||||
"github.com/hashicorp/go-bexpr"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
memdb "github.com/hashicorp/go-memdb"
|
||||
|
@ -706,7 +707,7 @@ func validateBindingRuleBindName(bindType, bindName string, availableFields []st
|
|||
// - If the computed name is not valid for the type ("INVALID_NAME", false, nil) is returned.
|
||||
// - If the computed name is valid for the type ("VALID_NAME", true, nil) is returned.
|
||||
func computeBindingRuleBindName(bindType, bindName string, projectedVars map[string]string) (string, bool, error) {
|
||||
bindName, err := InterpolateHIL(bindName, projectedVars, true)
|
||||
bindName, err := template.InterpolateHIL(bindName, projectedVars, true)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
|
|
@ -6,13 +6,10 @@ import (
|
|||
"net"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/consul/agent/metadata"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/hashicorp/hil"
|
||||
"github.com/hashicorp/hil/ast"
|
||||
"github.com/hashicorp/serf/serf"
|
||||
)
|
||||
|
||||
|
@ -440,45 +437,3 @@ func ServersGetACLMode(provider checkServersProvider, leaderAddr string, datacen
|
|||
|
||||
return state.found, state.mode, state.leaderMode
|
||||
}
|
||||
|
||||
// InterpolateHIL processes the string as if it were HIL and interpolates only
|
||||
// the provided string->string map as possible variables.
|
||||
func InterpolateHIL(s string, vars map[string]string, lowercase bool) (string, error) {
|
||||
if strings.Index(s, "${") == -1 {
|
||||
// Skip going to the trouble of parsing something that has no HIL.
|
||||
return s, nil
|
||||
}
|
||||
|
||||
tree, err := hil.Parse(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
vm := make(map[string]ast.Variable)
|
||||
for k, v := range vars {
|
||||
if lowercase {
|
||||
v = strings.ToLower(v)
|
||||
}
|
||||
vm[k] = ast.Variable{
|
||||
Type: ast.TypeString,
|
||||
Value: v,
|
||||
}
|
||||
}
|
||||
|
||||
config := &hil.EvalConfig{
|
||||
GlobalScope: &ast.BasicScope{
|
||||
VarMap: vm,
|
||||
},
|
||||
}
|
||||
|
||||
result, err := hil.Eval(tree, config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if result.Type != hil.TypeString {
|
||||
return "", fmt.Errorf("generated unexpected hil type: %s", result.Type)
|
||||
}
|
||||
|
||||
return result.Value.(string), nil
|
||||
}
|
||||
|
|
|
@ -442,161 +442,6 @@ func TestServersInDCMeetMinimumVersion(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestInterpolateHIL(t *testing.T) {
|
||||
for name, test := range map[string]struct {
|
||||
in string
|
||||
vars map[string]string
|
||||
exp string // when lower=false
|
||||
expLower string // when lower=true
|
||||
ok bool
|
||||
}{
|
||||
// valid HIL
|
||||
"empty": {
|
||||
"",
|
||||
map[string]string{},
|
||||
"",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
"no vars": {
|
||||
"nothing",
|
||||
map[string]string{},
|
||||
"nothing",
|
||||
"nothing",
|
||||
true,
|
||||
},
|
||||
"just lowercase var": {
|
||||
"${item}",
|
||||
map[string]string{"item": "value"},
|
||||
"value",
|
||||
"value",
|
||||
true,
|
||||
},
|
||||
"just uppercase var": {
|
||||
"${item}",
|
||||
map[string]string{"item": "VaLuE"},
|
||||
"VaLuE",
|
||||
"value",
|
||||
true,
|
||||
},
|
||||
"lowercase var in middle": {
|
||||
"before ${item}after",
|
||||
map[string]string{"item": "value"},
|
||||
"before valueafter",
|
||||
"before valueafter",
|
||||
true,
|
||||
},
|
||||
"uppercase var in middle": {
|
||||
"before ${item}after",
|
||||
map[string]string{"item": "VaLuE"},
|
||||
"before VaLuEafter",
|
||||
"before valueafter",
|
||||
true,
|
||||
},
|
||||
"two vars": {
|
||||
"before ${item}after ${more}",
|
||||
map[string]string{"item": "value", "more": "xyz"},
|
||||
"before valueafter xyz",
|
||||
"before valueafter xyz",
|
||||
true,
|
||||
},
|
||||
"missing map val": {
|
||||
"${item}",
|
||||
map[string]string{"item": ""},
|
||||
"",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
// "weird" HIL, but not technically invalid
|
||||
"just end": {
|
||||
"}",
|
||||
map[string]string{},
|
||||
"}",
|
||||
"}",
|
||||
true,
|
||||
},
|
||||
"var without start": {
|
||||
" item }",
|
||||
map[string]string{"item": "value"},
|
||||
" item }",
|
||||
" item }",
|
||||
true,
|
||||
},
|
||||
"two vars missing second start": {
|
||||
"before ${ item }after more }",
|
||||
map[string]string{"item": "value", "more": "xyz"},
|
||||
"before valueafter more }",
|
||||
"before valueafter more }",
|
||||
true,
|
||||
},
|
||||
// invalid HIL
|
||||
"just start": {
|
||||
"${",
|
||||
map[string]string{},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
"backwards": {
|
||||
"}${",
|
||||
map[string]string{},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
"no varname": {
|
||||
"${}",
|
||||
map[string]string{},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
"missing map key": {
|
||||
"${item}",
|
||||
map[string]string{},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
"var without end": {
|
||||
"${ item ",
|
||||
map[string]string{"item": "value"},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
"two vars missing first end": {
|
||||
"before ${ item after ${ more }",
|
||||
map[string]string{"item": "value", "more": "xyz"},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
} {
|
||||
test := test
|
||||
t.Run(name+" lower=false", func(t *testing.T) {
|
||||
out, err := InterpolateHIL(test.in, test.vars, false)
|
||||
if test.ok {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.exp, out)
|
||||
} else {
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, out, "")
|
||||
}
|
||||
})
|
||||
t.Run(name+" lower=true", func(t *testing.T) {
|
||||
out, err := InterpolateHIL(test.in, test.vars, true)
|
||||
if test.ok {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expLower, out)
|
||||
} else {
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, out, "")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServersGetACLMode(t *testing.T) {
|
||||
t.Parallel()
|
||||
makeServer := func(datacenter string, acls structs.ACLMode, status serf.MemberStatus, addr net.IP) metadata.Server {
|
||||
|
|
|
@ -22,6 +22,12 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func unNilMap(in map[string]string) map[string]string {
|
||||
if in == nil {
|
||||
return make(map[string]string)
|
||||
}
|
||||
return in
|
||||
}
|
||||
func TestAgentAntiEntropy_Services(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := agent.NewTestAgent(t, "")
|
||||
|
@ -170,7 +176,7 @@ func TestAgentAntiEntropy_Services(t *testing.T) {
|
|||
delete(meta, structs.MetaSegmentKey) // Added later, not in config.
|
||||
assert.Equal(t, a.Config.NodeID, id)
|
||||
assert.Equal(t, a.Config.TaggedAddresses, addrs)
|
||||
assert.Equal(t, a.Config.NodeMeta, meta)
|
||||
assert.Equal(t, unNilMap(a.Config.NodeMeta), meta)
|
||||
|
||||
// We should have 6 services (consul included)
|
||||
if len(services.NodeServices.Services) != 6 {
|
||||
|
@ -1045,7 +1051,7 @@ func TestAgentAntiEntropy_Checks(t *testing.T) {
|
|||
delete(meta, structs.MetaSegmentKey) // Added later, not in config.
|
||||
assert.Equal(t, a.Config.NodeID, id)
|
||||
assert.Equal(t, a.Config.TaggedAddresses, addrs)
|
||||
assert.Equal(t, a.Config.NodeMeta, meta)
|
||||
assert.Equal(t, unNilMap(a.Config.NodeMeta), meta)
|
||||
}
|
||||
})
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
|
@ -1686,7 +1692,7 @@ func TestAgentAntiEntropy_NodeInfo(t *testing.T) {
|
|||
delete(meta, structs.MetaSegmentKey) // Added later, not in config.
|
||||
require.Equal(t, a.Config.NodeID, id)
|
||||
require.Equal(t, a.Config.TaggedAddresses, addrs)
|
||||
require.Equal(t, a.Config.NodeMeta, meta)
|
||||
assert.Equal(t, unNilMap(a.Config.NodeMeta), meta)
|
||||
|
||||
// Blow away the catalog version of the node info
|
||||
if err := a.RPC("Catalog.Register", args, &out); err != nil {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hil"
|
||||
"github.com/hashicorp/hil/ast"
|
||||
)
|
||||
|
||||
// InterpolateHIL processes the string as if it were HIL and interpolates only
|
||||
// the provided string->string map as possible variables.
|
||||
func InterpolateHIL(s string, vars map[string]string, lowercase bool) (string, error) {
|
||||
if strings.Index(s, "${") == -1 {
|
||||
// Skip going to the trouble of parsing something that has no HIL.
|
||||
return s, nil
|
||||
}
|
||||
|
||||
tree, err := hil.Parse(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
vm := make(map[string]ast.Variable)
|
||||
for k, v := range vars {
|
||||
if lowercase {
|
||||
v = strings.ToLower(v)
|
||||
}
|
||||
vm[k] = ast.Variable{
|
||||
Type: ast.TypeString,
|
||||
Value: v,
|
||||
}
|
||||
}
|
||||
|
||||
config := &hil.EvalConfig{
|
||||
GlobalScope: &ast.BasicScope{
|
||||
VarMap: vm,
|
||||
},
|
||||
}
|
||||
|
||||
result, err := hil.Eval(tree, config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if result.Type != hil.TypeString {
|
||||
return "", fmt.Errorf("generated unexpected hil type: %s", result.Type)
|
||||
}
|
||||
|
||||
return result.Value.(string), nil
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInterpolateHIL(t *testing.T) {
|
||||
for name, test := range map[string]struct {
|
||||
in string
|
||||
vars map[string]string
|
||||
exp string // when lower=false
|
||||
expLower string // when lower=true
|
||||
ok bool
|
||||
}{
|
||||
// valid HIL
|
||||
"empty": {
|
||||
"",
|
||||
map[string]string{},
|
||||
"",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
"no vars": {
|
||||
"nothing",
|
||||
map[string]string{},
|
||||
"nothing",
|
||||
"nothing",
|
||||
true,
|
||||
},
|
||||
"just lowercase var": {
|
||||
"${item}",
|
||||
map[string]string{"item": "value"},
|
||||
"value",
|
||||
"value",
|
||||
true,
|
||||
},
|
||||
"just uppercase var": {
|
||||
"${item}",
|
||||
map[string]string{"item": "VaLuE"},
|
||||
"VaLuE",
|
||||
"value",
|
||||
true,
|
||||
},
|
||||
"lowercase var in middle": {
|
||||
"before ${item}after",
|
||||
map[string]string{"item": "value"},
|
||||
"before valueafter",
|
||||
"before valueafter",
|
||||
true,
|
||||
},
|
||||
"uppercase var in middle": {
|
||||
"before ${item}after",
|
||||
map[string]string{"item": "VaLuE"},
|
||||
"before VaLuEafter",
|
||||
"before valueafter",
|
||||
true,
|
||||
},
|
||||
"two vars": {
|
||||
"before ${item}after ${more}",
|
||||
map[string]string{"item": "value", "more": "xyz"},
|
||||
"before valueafter xyz",
|
||||
"before valueafter xyz",
|
||||
true,
|
||||
},
|
||||
"missing map val": {
|
||||
"${item}",
|
||||
map[string]string{"item": ""},
|
||||
"",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
// "weird" HIL, but not technically invalid
|
||||
"just end": {
|
||||
"}",
|
||||
map[string]string{},
|
||||
"}",
|
||||
"}",
|
||||
true,
|
||||
},
|
||||
"var without start": {
|
||||
" item }",
|
||||
map[string]string{"item": "value"},
|
||||
" item }",
|
||||
" item }",
|
||||
true,
|
||||
},
|
||||
"two vars missing second start": {
|
||||
"before ${ item }after more }",
|
||||
map[string]string{"item": "value", "more": "xyz"},
|
||||
"before valueafter more }",
|
||||
"before valueafter more }",
|
||||
true,
|
||||
},
|
||||
// invalid HIL
|
||||
"just start": {
|
||||
"${",
|
||||
map[string]string{},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
"backwards": {
|
||||
"}${",
|
||||
map[string]string{},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
"no varname": {
|
||||
"${}",
|
||||
map[string]string{},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
"missing map key": {
|
||||
"${item}",
|
||||
map[string]string{},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
"var without end": {
|
||||
"${ item ",
|
||||
map[string]string{"item": "value"},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
"two vars missing first end": {
|
||||
"before ${ item after ${ more }",
|
||||
map[string]string{"item": "value", "more": "xyz"},
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
} {
|
||||
test := test
|
||||
t.Run(name+" lower=false", func(t *testing.T) {
|
||||
out, err := InterpolateHIL(test.in, test.vars, false)
|
||||
if test.ok {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.exp, out)
|
||||
} else {
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, out, "")
|
||||
}
|
||||
})
|
||||
t.Run(name+" lower=true", func(t *testing.T) {
|
||||
out, err := InterpolateHIL(test.in, test.vars, true)
|
||||
if test.ok {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expLower, out)
|
||||
} else {
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, out, "")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue