diff --git a/.changelog/13677.txt b/.changelog/13677.txt new file mode 100644 index 0000000000..639cc5eb1c --- /dev/null +++ b/.changelog/13677.txt @@ -0,0 +1,4 @@ +```release-note:feature +cli: A new flag for config delete to delete a config entry in a +valid config file, e.g., config delete -filename intention-allow.hcl +``` diff --git a/command/config/delete/config_delete.go b/command/config/delete/config_delete.go index 913d09e85f..caa21c0947 100644 --- a/command/config/delete/config_delete.go +++ b/command/config/delete/config_delete.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/hashicorp/consul/command/flags" + "github.com/hashicorp/consul/command/helpers" "github.com/mitchellh/cli" ) @@ -25,10 +26,13 @@ type cmd struct { name string cas bool modifyIndex uint64 + filename string } func (c *cmd) init() { c.flags = flag.NewFlagSet("", flag.ContinueOnError) + + c.flags.StringVar(&c.filename, "filename", "", "The filename of the config entry to delete") c.flags.StringVar(&c.kind, "kind", "", "The kind of configuration to delete.") c.flags.StringVar(&c.name, "name", "", "The name of configuration to delete.") c.flags.BoolVar(&c.cas, "cas", false, @@ -55,6 +59,25 @@ func (c *cmd) Run(args []string) int { return 1 } + kind := c.kind + name := c.name + var err error + if c.filename != "" { + data, err := helpers.LoadDataSourceNoRaw(c.filename, nil) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to load data: %v", err)) + return 1 + } + + entry, err := helpers.ParseConfigEntry(data) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to decode config entry input: %v", err)) + return 1 + } + kind = entry.GetKind() + name = entry.GetName() + } + client, err := c.http.APIClient() if err != nil { c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) @@ -64,33 +87,58 @@ func (c *cmd) Run(args []string) int { var deleted bool if c.cas { - deleted, _, err = entries.DeleteCAS(c.kind, c.name, c.modifyIndex, nil) + deleted, _, err = entries.DeleteCAS(kind, name, c.modifyIndex, nil) } else { - _, err = entries.Delete(c.kind, c.name, nil) + _, err = entries.Delete(kind, name, nil) deleted = err == nil } if err != nil { - c.UI.Error(fmt.Sprintf("Error deleting config entry %s/%s: %v", c.kind, c.name, err)) + c.UI.Error(fmt.Sprintf("Error deleting config entry %s/%s: %v", kind, name, err)) return 1 } if !deleted { - c.UI.Error(fmt.Sprintf("Config entry not deleted: %s/%s", c.kind, c.name)) + c.UI.Error(fmt.Sprintf("Config entry not deleted: %s/%s", kind, name)) return 1 } - c.UI.Info(fmt.Sprintf("Config entry deleted: %s/%s", c.kind, c.name)) + c.UI.Info(fmt.Sprintf("Config entry deleted: %s/%s", kind, name)) return 0 } func (c *cmd) validateArgs() error { - if c.kind == "" { - return errors.New("Must specify the -kind parameter") + count := 0 + if c.filename != "" { + count++ } - if c.name == "" { - return errors.New("Must specify the -name parameter") + if c.kind != "" { + count++ + } + + if c.name != "" { + count++ + } + + if count >= 3 { + return errors.New("filename can't be used with kind or name") + } else if count == 0 { + return errors.New("Must specify the -kind or -filename parameter") + } + + if c.filename != "" { + if count == 2 { + return errors.New("filename can't be used with kind or name") + } + } else { + if c.kind == "" { + return errors.New("Must specify the -kind parameter") + } + + if c.name == "" { + return errors.New("Must specify the -name parameter") + } } if c.cas && c.modifyIndex == 0 { @@ -115,12 +163,13 @@ func (c *cmd) Help() string { const ( synopsis = "Delete a centralized config entry" help = ` -Usage: consul config delete [options] -kind -name +Usage: consul config delete [options] ([-kind -name ] | [-f FILENAME]) Deletes the configuration entry specified by the kind and name. Example: $ consul config delete -kind service-defaults -name web + $ consul config delete -filename service-defaults-web.hcl ` ) diff --git a/command/config/delete/config_delete_test.go b/command/config/delete/config_delete_test.go index 01c00b549b..d4f9b704ed 100644 --- a/command/config/delete/config_delete_test.go +++ b/command/config/delete/config_delete_test.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/consul/agent" "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/sdk/testutil" "github.com/mitchellh/cli" "github.com/stretchr/testify/require" ) @@ -30,11 +31,7 @@ func TestConfigDelete(t *testing.T) { ui := cli.NewMockUi() c := New(ui) - _, _, err := client.ConfigEntries().Set(&api.ServiceConfigEntry{ - Kind: api.ServiceDefaults, - Name: "web", - Protocol: "tcp", - }, nil) + err := createEntry(client) require.NoError(t, err) args := []string{ @@ -54,6 +51,15 @@ func TestConfigDelete(t *testing.T) { require.Nil(t, entry) } +func createEntry(client *api.Client) error { + _, _, err := client.ConfigEntries().Set(&api.ServiceConfigEntry{ + Kind: api.ServiceDefaults, + Name: "web", + Protocol: "tcp", + }, nil) + return err +} + func TestConfigDelete_CAS(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") @@ -65,11 +71,7 @@ func TestConfigDelete_CAS(t *testing.T) { defer a.Shutdown() client := a.Client() - _, _, err := client.ConfigEntries().Set(&api.ServiceConfigEntry{ - Kind: api.ServiceDefaults, - Name: "web", - Protocol: "tcp", - }, nil) + err := createEntry(client) require.NoError(t, err) entry, _, err := client.ConfigEntries().Get(api.ServiceDefaults, "web", nil) @@ -111,11 +113,44 @@ func TestConfigDelete_CAS(t *testing.T) { require.Contains(t, ui.OutputWriter.String(), "Config entry deleted: service-defaults/web") require.Empty(t, ui.ErrorWriter.String()) - entry, _, err := client.ConfigEntries().Get(api.ServiceDefaults, "web", nil) require.Error(t, err) require.Nil(t, entry) }) + + t.Run("delete from file with a valid modify index", func(t *testing.T) { + err := createEntry(client) + require.NoError(t, err) + entry, _, err := client.ConfigEntries().Get(api.ServiceDefaults, "web", nil) + require.NoError(t, err) + + ui := cli.NewMockUi() + c := New(ui) + f := testutil.TempFile(t, "config-write-svc-web.hcl") + _, err = f.WriteString(` + Kind = "service-defaults" + Name = "web" + Protocol = "tcp" + `) + require.NoError(t, err) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-filename=" + f.Name(), + "-cas", + "-modify-index=" + strconv.FormatUint(entry.GetModifyIndex(), 10), + } + + code := c.Run(args) + require.Equal(t, 0, code) + require.Contains(t, ui.OutputWriter.String(), + "Config entry deleted: service-defaults/web") + require.Empty(t, ui.ErrorWriter.String()) + + entry, _, err = client.ConfigEntries().Get(api.ServiceDefaults, "web", nil) + require.Error(t, err) + require.Nil(t, entry) + }) } func TestConfigDelete_InvalidArgs(t *testing.T) { @@ -125,8 +160,12 @@ func TestConfigDelete_InvalidArgs(t *testing.T) { args []string err string }{ - "no kind": { + "no kind or filename": { args: []string{}, + err: "Must specify the -kind or -filename parameter", + }, + "no kind": { + args: []string{"-name", "web"}, err: "Must specify the -kind parameter", }, "no name": { @@ -145,6 +184,18 @@ func TestConfigDelete_InvalidArgs(t *testing.T) { args: []string{"-kind", api.ServiceDefaults, "-name", "web", "-modify-index", "1"}, err: "Cannot specify -modify-index without -cas", }, + "kind and filename": { + args: []string{"-kind", api.ServiceDefaults, "-filename", "config-file.hcl"}, + err: "filename can't be used with kind or name", + }, + "name and filename": { + args: []string{"-name", "db", "-filename", "config-file.hcl"}, + err: "filename can't be used with kind or name", + }, + "kind, name, and filename": { + args: []string{"-kind", api.ServiceDefaults, "-name", "db", "-filename", "config-file.hcl"}, + err: "filename can't be used with kind or name", + }, } for name, tcase := range cases { diff --git a/command/config/write/config_write.go b/command/config/write/config_write.go index 9e08fde38a..0c377e6c45 100644 --- a/command/config/write/config_write.go +++ b/command/config/write/config_write.go @@ -66,7 +66,7 @@ func (c *cmd) Run(args []string) int { return 1 } - entry, err := parseConfigEntry(data) + entry, err := helpers.ParseConfigEntry(data) if err != nil { c.UI.Error(fmt.Sprintf("Failed to decode config entry input: %v", err)) return 1 @@ -100,16 +100,6 @@ func (c *cmd) Run(args []string) int { return 0 } -func parseConfigEntry(data string) (api.ConfigEntry, error) { - // parse the data - var raw map[string]interface{} - if err := hclDecode(&raw, data); err != nil { - return nil, fmt.Errorf("Failed to decode config entry input: %v", err) - } - - return newDecodeConfigEntry(raw) -} - // There is a 'structs' variation of this in // agent/structs/config_entry.go:DecodeConfigEntry func newDecodeConfigEntry(raw map[string]interface{}) (api.ConfigEntry, error) { diff --git a/command/config/write/config_write_test.go b/command/config/write/config_write_test.go index 566e2ab9b1..3ea5a8be2a 100644 --- a/command/config/write/config_write_test.go +++ b/command/config/write/config_write_test.go @@ -5,9 +5,6 @@ import ( "io" "strings" "testing" - "time" - - "github.com/hashicorp/consul/agent/structs" "github.com/mitchellh/cli" "github.com/stretchr/testify/require" @@ -152,3068 +149,7 @@ http { }) } -// TestParseConfigEntry is the 'api' mirror image of -// agent/structs/config_entry_test.go:TestDecodeConfigEntry -func TestParseConfigEntry(t *testing.T) { - t.Parallel() - for _, tc := range []struct { - name string - camel, camelJSON string - snake, snakeJSON string - expect api.ConfigEntry - expectJSON api.ConfigEntry - expectErr string - }{ - { - name: "proxy-defaults: extra fields or typo", - snake: ` - kind = "proxy-defaults" - name = "main" - cornfig { - "foo" = 19 - } - `, - camel: ` - Kind = "proxy-defaults" - Name = "main" - Cornfig { - "foo" = 19 - } - `, - snakeJSON: ` - { - "kind": "proxy-defaults", - "name": "main", - "cornfig": { - "foo": 19 - } - } - `, - camelJSON: ` - { - "Kind": "proxy-defaults", - "Name": "main", - "Cornfig": { - "foo": 19 - } - } - `, - expectErr: `invalid config key "cornfig"`, - }, - { - name: "proxy-defaults", - snake: ` - kind = "proxy-defaults" - name = "main" - meta { - "foo" = "bar" - "gir" = "zim" - } - config { - "foo" = 19 - "bar" = "abc" - "moreconfig" { - "moar" = "config" - } - } - mesh_gateway { - mode = "remote" - } - mode = "direct" - transparent_proxy = { - outbound_listener_port = 10101 - dialed_directly = true - } - `, - camel: ` - Kind = "proxy-defaults" - Name = "main" - Meta { - "foo" = "bar" - "gir" = "zim" - } - Config { - "foo" = 19 - "bar" = "abc" - "moreconfig" { - "moar" = "config" - } - } - MeshGateway { - Mode = "remote" - } - Mode = "direct" - TransparentProxy = { - outbound_listener_port = 10101 - dialed_directly = true - } - `, - snakeJSON: ` - { - "kind": "proxy-defaults", - "name": "main", - "meta" : { - "foo": "bar", - "gir": "zim" - }, - "config": { - "foo": 19, - "bar": "abc", - "moreconfig": { - "moar": "config" - } - }, - "mesh_gateway": { - "mode": "remote" - }, - "mode": "direct", - "transparent_proxy": { - "outbound_listener_port": 10101, - "dialed_directly": true - } - } - `, - camelJSON: ` - { - "Kind": "proxy-defaults", - "Name": "main", - "Meta" : { - "foo": "bar", - "gir": "zim" - }, - "Config": { - "foo": 19, - "bar": "abc", - "moreconfig": { - "moar": "config" - } - }, - "MeshGateway": { - "Mode": "remote" - }, - "Mode": "direct", - "TransparentProxy": { - "OutboundListenerPort": 10101, - "DialedDirectly": true - } - } - `, - expect: &api.ProxyConfigEntry{ - Kind: "proxy-defaults", - Name: "main", - Meta: map[string]string{ - "foo": "bar", - "gir": "zim", - }, - Config: map[string]interface{}{ - "foo": 19, - "bar": "abc", - "moreconfig": map[string]interface{}{ - "moar": "config", - }, - }, - MeshGateway: api.MeshGatewayConfig{ - Mode: api.MeshGatewayModeRemote, - }, - Mode: api.ProxyModeDirect, - TransparentProxy: &api.TransparentProxyConfig{ - OutboundListenerPort: 10101, - DialedDirectly: true, - }, - }, - expectJSON: &api.ProxyConfigEntry{ - Kind: "proxy-defaults", - Name: "main", - Meta: map[string]string{ - "foo": "bar", - "gir": "zim", - }, - Config: map[string]interface{}{ - "foo": float64(19), // json decoding gives float64 instead of int here - "bar": "abc", - "moreconfig": map[string]interface{}{ - "moar": "config", - }, - }, - MeshGateway: api.MeshGatewayConfig{ - Mode: api.MeshGatewayModeRemote, - }, - Mode: api.ProxyModeDirect, - TransparentProxy: &api.TransparentProxyConfig{ - OutboundListenerPort: 10101, - DialedDirectly: true, - }, - }, - }, - { - name: "terminating-gateway", - snake: ` - kind = "terminating-gateway" - name = "terminating-gw-west" - namespace = "default" - meta { - "foo" = "bar" - "gir" = "zim" - } - services = [ - { - name = "billing" - namespace = "biz" - ca_file = "/etc/ca.crt" - cert_file = "/etc/client.crt" - key_file = "/etc/tls.key" - sni = "mydomain" - }, - { - name = "*" - namespace = "ops" - } - ] - `, - camel: ` - Kind = "terminating-gateway" - Name = "terminating-gw-west" - Namespace = "default" - Meta { - "foo" = "bar" - "gir" = "zim" - } - Services = [ - { - Name = "billing" - Namespace = "biz" - CAFile = "/etc/ca.crt" - CertFile = "/etc/client.crt" - KeyFile = "/etc/tls.key" - SNI = "mydomain" - }, - { - Name = "*" - Namespace = "ops" - } - ] - `, - snakeJSON: ` - { - "kind": "terminating-gateway", - "name": "terminating-gw-west", - "namespace": "default", - "meta" : { - "foo": "bar", - "gir": "zim" - }, - "services": [ - { - "name": "billing", - "namespace": "biz", - "ca_file": "/etc/ca.crt", - "cert_file": "/etc/client.crt", - "key_file": "/etc/tls.key", - "sni": "mydomain" - }, - { - "name": "*", - "namespace": "ops" - } - ] - } - `, - camelJSON: ` - { - "Kind": "terminating-gateway", - "Name": "terminating-gw-west", - "Namespace": "default", - "Meta" : { - "foo": "bar", - "gir": "zim" - }, - "Services": [ - { - "Name": "billing", - "Namespace": "biz", - "CAFile": "/etc/ca.crt", - "CertFile": "/etc/client.crt", - "KeyFile": "/etc/tls.key", - "SNI": "mydomain" - }, - { - "Name": "*", - "Namespace": "ops" - } - ] - } - `, - expect: &api.TerminatingGatewayConfigEntry{ - Kind: "terminating-gateway", - Name: "terminating-gw-west", - Namespace: "default", - Meta: map[string]string{ - "foo": "bar", - "gir": "zim", - }, - Services: []api.LinkedService{ - { - Name: "billing", - Namespace: "biz", - CAFile: "/etc/ca.crt", - CertFile: "/etc/client.crt", - KeyFile: "/etc/tls.key", - SNI: "mydomain", - }, - { - Name: "*", - Namespace: "ops", - }, - }, - }, - expectJSON: &api.TerminatingGatewayConfigEntry{ - Kind: "terminating-gateway", - Name: "terminating-gw-west", - Namespace: "default", - Meta: map[string]string{ - "foo": "bar", - "gir": "zim", - }, - Services: []api.LinkedService{ - { - Name: "billing", - Namespace: "biz", - CAFile: "/etc/ca.crt", - CertFile: "/etc/client.crt", - KeyFile: "/etc/tls.key", - SNI: "mydomain", - }, - { - Name: "*", - Namespace: "ops", - }, - }, - }, - }, - { - name: "service-defaults: kitchen sink (upstreams edition)", - snake: ` - kind = "service-defaults" - name = "main" - meta { - "foo" = "bar" - "gir" = "zim" - } - protocol = "http" - external_sni = "abc-123" - mesh_gateway { - mode = "remote" - } - mode = "direct" - transparent_proxy = { - outbound_listener_port = 10101 - dialed_directly = true - } - upstream_config { - overrides = [ - { - name = "redis" - passive_health_check { - max_failures = 3 - interval = "2s" - } - envoy_listener_json = "{ \"listener-foo\": 5 }" - envoy_cluster_json = "{ \"cluster-bar\": 5 }" - protocol = "grpc" - connect_timeout_ms = 6543 - }, - { - name = "finance--billing" - mesh_gateway { - mode = "remote" - } - limits { - max_connections = 1111 - max_pending_requests = 2222 - max_concurrent_requests = 3333 - } - } - ] - defaults { - envoy_cluster_json = "zip" - envoy_listener_json = "zop" - connect_timeout_ms = 5000 - protocol = "http" - limits { - max_connections = 3 - max_pending_requests = 4 - max_concurrent_requests = 5 - } - passive_health_check { - max_failures = 5 - interval = "4s" - } - } - } - `, - camel: ` - Kind = "service-defaults" - Name = "main" - Meta { - "foo" = "bar" - "gir" = "zim" - } - Protocol = "http" - ExternalSNI = "abc-123" - MeshGateway { - Mode = "remote" - } - Mode = "direct" - TransparentProxy = { - outbound_listener_port = 10101 - dialed_directly = true - } - UpstreamConfig { - Overrides = [ - { - Name = "redis" - PassiveHealthCheck { - MaxFailures = 3 - Interval = "2s" - } - EnvoyListenerJson = "{ \"listener-foo\": 5 }" - EnvoyClusterJson = "{ \"cluster-bar\": 5 }" - Protocol = "grpc" - ConnectTimeoutMs = 6543 - }, - { - Name = "finance--billing" - MeshGateway { - Mode = "remote" - } - Limits { - MaxConnections = 1111 - MaxPendingRequests = 2222 - MaxConcurrentRequests = 3333 - } - } - ] - Defaults { - EnvoyClusterJson = "zip" - EnvoyListenerJson = "zop" - ConnectTimeoutMs = 5000 - Protocol = "http" - Limits { - MaxConnections = 3 - MaxPendingRequests = 4 - MaxConcurrentRequests = 5 - } - PassiveHealthCheck { - MaxFailures = 5 - Interval = "4s" - } - } - } - `, - snakeJSON: ` - { - "kind": "service-defaults", - "name": "main", - "meta" : { - "foo": "bar", - "gir": "zim" - }, - "protocol": "http", - "external_sni": "abc-123", - "mesh_gateway": { - "mode": "remote" - }, - "mode": "direct", - "transparent_proxy": { - "outbound_listener_port": 10101, - "dialed_directly": true - }, - "upstream_config": { - "overrides": [ - { - "name": "redis", - "passive_health_check": { - "max_failures": 3, - "interval": "2s" - }, - "envoy_listener_json": "{ \"listener-foo\": 5 }", - "envoy_cluster_json": "{ \"cluster-bar\": 5 }", - "protocol": "grpc", - "connect_timeout_ms": 6543 - }, - { - "name": "finance--billing", - "mesh_gateway": { - "mode": "remote" - }, - "limits": { - "max_connections": 1111, - "max_pending_requests": 2222, - "max_concurrent_requests": 3333 - } - } - ], - "defaults": { - "envoy_cluster_json": "zip", - "envoy_listener_json": "zop", - "connect_timeout_ms": 5000, - "protocol": "http", - "limits": { - "max_connections": 3, - "max_pending_requests": 4, - "max_concurrent_requests": 5 - }, - "passive_health_check": { - "max_failures": 5, - "interval": "4s" - } - } - } - } - `, - camelJSON: ` - { - "Kind": "service-defaults", - "Name": "main", - "Meta" : { - "foo": "bar", - "gir": "zim" - }, - "Protocol": "http", - "ExternalSNI": "abc-123", - "MeshGateway": { - "Mode": "remote" - }, - "Mode": "direct", - "TransparentProxy": { - "OutboundListenerPort": 10101, - "DialedDirectly": true - }, - "UpstreamConfig": { - "Overrides": [ - { - "Name": "redis", - "PassiveHealthCheck": { - "MaxFailures": 3, - "Interval": "2s" - }, - "EnvoyListenerJson": "{ \"listener-foo\": 5 }", - "EnvoyClusterJson": "{ \"cluster-bar\": 5 }", - "Protocol": "grpc", - "ConnectTimeoutMs": 6543 - }, - { - "Name": "finance--billing", - "MeshGateway": { - "Mode": "remote" - }, - "Limits": { - "MaxConnections": 1111, - "MaxPendingRequests": 2222, - "MaxConcurrentRequests": 3333 - } - } - ], - "Defaults": { - "EnvoyClusterJson": "zip", - "EnvoyListenerJson": "zop", - "ConnectTimeoutMs": 5000, - "Protocol": "http", - "Limits": { - "MaxConnections": 3, - "MaxPendingRequests": 4, - "MaxConcurrentRequests": 5 - }, - "PassiveHealthCheck": { - "MaxFailures": 5, - "Interval": "4s" - } - } - } - } - `, - expect: &api.ServiceConfigEntry{ - Kind: "service-defaults", - Name: "main", - Meta: map[string]string{ - "foo": "bar", - "gir": "zim", - }, - Protocol: "http", - ExternalSNI: "abc-123", - MeshGateway: api.MeshGatewayConfig{ - Mode: api.MeshGatewayModeRemote, - }, - Mode: api.ProxyModeDirect, - TransparentProxy: &api.TransparentProxyConfig{ - OutboundListenerPort: 10101, - DialedDirectly: true, - }, - UpstreamConfig: &api.UpstreamConfiguration{ - Overrides: []*api.UpstreamConfig{ - { - Name: "redis", - PassiveHealthCheck: &api.PassiveHealthCheck{ - MaxFailures: 3, - Interval: 2 * time.Second, - }, - EnvoyListenerJSON: `{ "listener-foo": 5 }`, - EnvoyClusterJSON: `{ "cluster-bar": 5 }`, - Protocol: "grpc", - ConnectTimeoutMs: 6543, - }, - { - Name: "finance--billing", - MeshGateway: api.MeshGatewayConfig{ - Mode: "remote", - }, - Limits: &api.UpstreamLimits{ - MaxConnections: intPointer(1111), - MaxPendingRequests: intPointer(2222), - MaxConcurrentRequests: intPointer(3333), - }, - }, - }, - Defaults: &api.UpstreamConfig{ - EnvoyClusterJSON: "zip", - EnvoyListenerJSON: "zop", - Protocol: "http", - ConnectTimeoutMs: 5000, - Limits: &api.UpstreamLimits{ - MaxConnections: intPointer(3), - MaxPendingRequests: intPointer(4), - MaxConcurrentRequests: intPointer(5), - }, - PassiveHealthCheck: &api.PassiveHealthCheck{ - MaxFailures: 5, - Interval: 4 * time.Second, - }, - }, - }, - }, - }, - { - name: "service-defaults: kitchen sink (destination edition)", - snake: ` - kind = "service-defaults" - name = "main" - meta { - "foo" = "bar" - "gir" = "zim" - } - protocol = "grpc" - mesh_gateway { - mode = "remote" - } - mode = "transparent" - transparent_proxy = { - outbound_listener_port = 10101 - dialed_directly = true - } - destination = { - address = "10.0.0.0/16", - port = 443 - } - `, - camel: ` - Kind = "service-defaults" - Name = "main" - Meta { - "foo" = "bar" - "gir" = "zim" - } - Protocol = "grpc" - MeshGateway { - Mode = "remote" - } - Mode = "transparent" - TransparentProxy = { - outbound_listener_port = 10101 - dialed_directly = true - } - Destination = { - Address = "10.0.0.0/16", - Port = 443 - } - `, - snakeJSON: ` - { - "kind": "service-defaults", - "name": "main", - "meta" : { - "foo": "bar", - "gir": "zim" - }, - "protocol": "grpc", - "mesh_gateway": { - "mode": "remote" - }, - "mode": "transparent", - "transparent_proxy": { - "outbound_listener_port": 10101, - "dialed_directly": true - }, - "destination": { - "address": "10.0.0.0/16", - "port": 443 - } - } - `, - camelJSON: ` - { - "Kind": "service-defaults", - "Name": "main", - "Meta" : { - "foo": "bar", - "gir": "zim" - }, - "Protocol": "grpc", - "MeshGateway": { - "Mode": "remote" - }, - "Mode": "transparent", - "TransparentProxy": { - "OutboundListenerPort": 10101, - "DialedDirectly": true - }, - "Destination": { - "Address": "10.0.0.0/16", - "Port": 443 - } - } - `, - expect: &api.ServiceConfigEntry{ - Kind: "service-defaults", - Name: "main", - Meta: map[string]string{ - "foo": "bar", - "gir": "zim", - }, - Protocol: "grpc", - MeshGateway: api.MeshGatewayConfig{ - Mode: api.MeshGatewayModeRemote, - }, - Mode: api.ProxyModeTransparent, - TransparentProxy: &api.TransparentProxyConfig{ - OutboundListenerPort: 10101, - DialedDirectly: true, - }, - Destination: &api.DestinationConfig{ - Address: "10.0.0.0/16", - Port: 443, - }, - }, - }, - { - name: "service-router: kitchen sink", - snake: ` - kind = "service-router" - name = "main" - partition = "pepper" - meta { - "foo" = "bar" - "gir" = "zim" - } - routes = [ - { - match { - http { - path_exact = "/foo" - header = [ - { - name = "debug1" - present = true - }, - { - name = "debug2" - present = false - invert = true - }, - { - name = "debug3" - exact = "1" - }, - { - name = "debug4" - prefix = "aaa" - }, - { - name = "debug5" - suffix = "bbb" - }, - { - name = "debug6" - regex = "a.*z" - }, - ] - } - } - destination { - service = "carrot" - service_subset = "kale" - namespace = "leek" - partition = "chard" - prefix_rewrite = "/alternate" - request_timeout = "99s" - num_retries = 12345 - retry_on_connect_failure = true - retry_on_status_codes = [401, 209] - } - }, - { - match { - http { - path_prefix = "/foo" - methods = [ "GET", "DELETE" ] - query_param = [ - { - name = "hack1" - present = true - }, - { - name = "hack2" - exact = "1" - }, - { - name = "hack3" - regex = "a.*z" - }, - ] - } - } - }, - { - match { - http { - path_regex = "/foo" - } - } - }, - ] - `, - camel: ` - Kind = "service-router" - Name = "main" - Partition = "pepper" - Meta { - "foo" = "bar" - "gir" = "zim" - } - Routes = [ - { - Match { - HTTP { - PathExact = "/foo" - Header = [ - { - Name = "debug1" - Present = true - }, - { - Name = "debug2" - Present = false - Invert = true - }, - { - Name = "debug3" - Exact = "1" - }, - { - Name = "debug4" - Prefix = "aaa" - }, - { - Name = "debug5" - Suffix = "bbb" - }, - { - Name = "debug6" - Regex = "a.*z" - }, - ] - } - } - Destination { - Service = "carrot" - ServiceSubset = "kale" - Namespace = "leek" - Partition = "chard" - PrefixRewrite = "/alternate" - RequestTimeout = "99s" - NumRetries = 12345 - RetryOnConnectFailure = true - RetryOnStatusCodes = [401, 209] - } - }, - { - Match { - HTTP { - PathPrefix = "/foo" - Methods = [ "GET", "DELETE" ] - QueryParam = [ - { - Name = "hack1" - Present = true - }, - { - Name = "hack2" - Exact = "1" - }, - { - Name = "hack3" - Regex = "a.*z" - }, - ] - } - } - }, - { - Match { - HTTP { - PathRegex = "/foo" - } - } - }, - ] - `, - snakeJSON: ` - { - "kind": "service-router", - "name": "main", - "partition": "pepper", - "meta" : { - "foo": "bar", - "gir": "zim" - }, - "routes": [ - { - "match": { - "http": { - "path_exact": "/foo", - "header": [ - { - "name": "debug1", - "present": true - }, - { - "name": "debug2", - "present": false, - "invert": true - }, - { - "name": "debug3", - "exact": "1" - }, - { - "name": "debug4", - "prefix": "aaa" - }, - { - "name": "debug5", - "suffix": "bbb" - }, - { - "name": "debug6", - "regex": "a.*z" - } - ] - } - }, - "destination": { - "service": "carrot", - "service_subset": "kale", - "namespace": "leek", - "partition": "chard", - "prefix_rewrite": "/alternate", - "request_timeout": "99s", - "num_retries": 12345, - "retry_on_connect_failure": true, - "retry_on_status_codes": [ - 401, - 209 - ] - } - }, - { - "match": { - "http": { - "path_prefix": "/foo", - "methods": [ - "GET", - "DELETE" - ], - "query_param": [ - { - "name": "hack1", - "present": true - }, - { - "name": "hack2", - "exact": "1" - }, - { - "name": "hack3", - "regex": "a.*z" - } - ] - } - } - }, - { - "match": { - "http": { - "path_regex": "/foo" - } - } - } - ] - } - `, - camelJSON: ` - { - "Kind": "service-router", - "Name": "main", - "Partition": "pepper", - "Meta" : { - "foo": "bar", - "gir": "zim" - }, - "Routes": [ - { - "Match": { - "HTTP": { - "PathExact": "/foo", - "Header": [ - { - "Name": "debug1", - "Present": true - }, - { - "Name": "debug2", - "Present": false, - "Invert": true - }, - { - "Name": "debug3", - "Exact": "1" - }, - { - "Name": "debug4", - "Prefix": "aaa" - }, - { - "Name": "debug5", - "Suffix": "bbb" - }, - { - "Name": "debug6", - "Regex": "a.*z" - } - ] - } - }, - "Destination": { - "Service": "carrot", - "ServiceSubset": "kale", - "Namespace": "leek", - "Partition": "chard", - "PrefixRewrite": "/alternate", - "RequestTimeout": "99s", - "NumRetries": 12345, - "RetryOnConnectFailure": true, - "RetryOnStatusCodes": [ - 401, - 209 - ] - } - }, - { - "Match": { - "HTTP": { - "PathPrefix": "/foo", - "Methods": [ - "GET", - "DELETE" - ], - "QueryParam": [ - { - "Name": "hack1", - "Present": true - }, - { - "Name": "hack2", - "Exact": "1" - }, - { - "Name": "hack3", - "Regex": "a.*z" - } - ] - } - } - }, - { - "Match": { - "HTTP": { - "PathRegex": "/foo" - } - } - } - ] - } - `, - expect: &api.ServiceRouterConfigEntry{ - Kind: "service-router", - Name: "main", - Partition: "pepper", - Meta: map[string]string{ - "foo": "bar", - "gir": "zim", - }, - Routes: []api.ServiceRoute{ - { - Match: &api.ServiceRouteMatch{ - HTTP: &api.ServiceRouteHTTPMatch{ - PathExact: "/foo", - Header: []api.ServiceRouteHTTPMatchHeader{ - { - Name: "debug1", - Present: true, - }, - { - Name: "debug2", - Present: false, - Invert: true, - }, - { - Name: "debug3", - Exact: "1", - }, - { - Name: "debug4", - Prefix: "aaa", - }, - { - Name: "debug5", - Suffix: "bbb", - }, - { - Name: "debug6", - Regex: "a.*z", - }, - }, - }, - }, - Destination: &api.ServiceRouteDestination{ - Service: "carrot", - ServiceSubset: "kale", - Namespace: "leek", - Partition: "chard", - PrefixRewrite: "/alternate", - RequestTimeout: 99 * time.Second, - NumRetries: 12345, - RetryOnConnectFailure: true, - RetryOnStatusCodes: []uint32{401, 209}, - }, - }, - { - Match: &api.ServiceRouteMatch{ - HTTP: &api.ServiceRouteHTTPMatch{ - PathPrefix: "/foo", - Methods: []string{"GET", "DELETE"}, - QueryParam: []api.ServiceRouteHTTPMatchQueryParam{ - { - Name: "hack1", - Present: true, - }, - { - Name: "hack2", - Exact: "1", - }, - { - Name: "hack3", - Regex: "a.*z", - }, - }, - }, - }, - }, - { - Match: &api.ServiceRouteMatch{ - HTTP: &api.ServiceRouteHTTPMatch{ - PathRegex: "/foo", - }, - }, - }, - }, - }, - }, - { - name: "service-splitter: kitchen sink", - snake: ` - kind = "service-splitter" - name = "main" - partition = "east" - meta { - "foo" = "bar" - "gir" = "zim" - } - splits = [ - { - weight = 97.1 - service_subset = "v1" - }, - { - weight = 2 - service_subset = "v2" - }, - { - weight = 0.9 - service = "other" - namespace = "alt" - partition = "west" - }, - ] - `, - camel: ` - Kind = "service-splitter" - Name = "main" - Partition = "east" - Meta { - "foo" = "bar" - "gir" = "zim" - } - Splits = [ - { - Weight = 97.1 - ServiceSubset = "v1" - }, - { - Weight = 2, - ServiceSubset = "v2" - }, - { - Weight = 0.9 - Service = "other" - Namespace = "alt" - Partition = "west" - }, - ] - `, - snakeJSON: ` - { - "kind": "service-splitter", - "name": "main", - "partition": "east", - "meta" : { - "foo": "bar", - "gir": "zim" - }, - "splits": [ - { - "weight": 97.1, - "service_subset": "v1" - }, - { - "weight": 2, - "service_subset": "v2" - }, - { - "weight": 0.9, - "service": "other", - "namespace": "alt", - "partition": "west" - } - ] - } - `, - camelJSON: ` - { - "Kind": "service-splitter", - "Name": "main", - "Partition": "east", - "Meta" : { - "foo": "bar", - "gir": "zim" - }, - "Splits": [ - { - "Weight": 97.1, - "ServiceSubset": "v1" - }, - { - "Weight": 2, - "ServiceSubset": "v2" - }, - { - "Weight": 0.9, - "Service": "other", - "Namespace": "alt", - "Partition": "west" - } - ] - } - `, - expect: &api.ServiceSplitterConfigEntry{ - Kind: api.ServiceSplitter, - Name: "main", - Partition: "east", - Meta: map[string]string{ - "foo": "bar", - "gir": "zim", - }, - Splits: []api.ServiceSplit{ - { - Weight: 97.1, - ServiceSubset: "v1", - }, - { - Weight: 2, - ServiceSubset: "v2", - }, - { - Weight: 0.9, - Service: "other", - Namespace: "alt", - Partition: "west", - }, - }, - }, - }, - { - name: "service-resolver: subsets with failover", - snake: ` - kind = "service-resolver" - name = "main" - meta { - "foo" = "bar" - "gir" = "zim" - } - default_subset = "v1" - connect_timeout = "15s" - subsets = { - "v1" = { - filter = "Service.Meta.version == v1" - }, - "v2" = { - filter = "Service.Meta.version == v2" - only_passing = true - }, - } - failover = { - "v2" = { - service = "failcopy" - service_subset = "sure" - namespace = "neighbor" - datacenters = ["dc5", "dc14"] - }, - "*" = { - datacenters = ["dc7"] - } - }`, - camel: ` - Kind = "service-resolver" - Name = "main" - Meta { - "foo" = "bar" - "gir" = "zim" - } - DefaultSubset = "v1" - ConnectTimeout = "15s" - Subsets = { - "v1" = { - Filter = "Service.Meta.version == v1" - }, - "v2" = { - Filter = "Service.Meta.version == v2" - OnlyPassing = true - }, - } - Failover = { - "v2" = { - Service = "failcopy" - ServiceSubset = "sure" - Namespace = "neighbor" - Datacenters = ["dc5", "dc14"] - }, - "*" = { - Datacenters = ["dc7"] - } - }`, - snakeJSON: ` - { - "kind": "service-resolver", - "name": "main", - "meta" : { - "foo": "bar", - "gir": "zim" - }, - "default_subset": "v1", - "connect_timeout": "15s", - "subsets": { - "v1": { - "filter": "Service.Meta.version == v1" - }, - "v2": { - "filter": "Service.Meta.version == v2", - "only_passing": true - } - }, - "failover": { - "v2": { - "service": "failcopy", - "service_subset": "sure", - "namespace": "neighbor", - "datacenters": [ - "dc5", - "dc14" - ] - }, - "*": { - "datacenters": [ - "dc7" - ] - } - } - } - `, - camelJSON: ` - { - "Kind": "service-resolver", - "Name": "main", - "Meta" : { - "foo": "bar", - "gir": "zim" - }, - "DefaultSubset": "v1", - "ConnectTimeout": "15s", - "Subsets": { - "v1": { - "Filter": "Service.Meta.version == v1" - }, - "v2": { - "Filter": "Service.Meta.version == v2", - "OnlyPassing": true - } - }, - "Failover": { - "v2": { - "Service": "failcopy", - "ServiceSubset": "sure", - "Namespace": "neighbor", - "Datacenters": [ - "dc5", - "dc14" - ] - }, - "*": { - "Datacenters": [ - "dc7" - ] - } - } - } - `, - expect: &api.ServiceResolverConfigEntry{ - Kind: "service-resolver", - Name: "main", - Meta: map[string]string{ - "foo": "bar", - "gir": "zim", - }, - DefaultSubset: "v1", - ConnectTimeout: 15 * time.Second, - Subsets: map[string]api.ServiceResolverSubset{ - "v1": { - Filter: "Service.Meta.version == v1", - }, - "v2": { - Filter: "Service.Meta.version == v2", - OnlyPassing: true, - }, - }, - Failover: map[string]api.ServiceResolverFailover{ - "v2": { - Service: "failcopy", - ServiceSubset: "sure", - Namespace: "neighbor", - Datacenters: []string{"dc5", "dc14"}, - }, - "*": { - Datacenters: []string{"dc7"}, - }, - }, - }, - }, - { - name: "service-resolver: redirect", - snake: ` - kind = "service-resolver" - name = "main" - partition = "east" - redirect { - service = "other" - service_subset = "backup" - namespace = "alt" - partition = "west" - datacenter = "dc9" - } - `, - camel: ` - Kind = "service-resolver" - Name = "main" - Partition = "east" - Redirect { - Service = "other" - ServiceSubset = "backup" - Namespace = "alt" - Partition = "west" - Datacenter = "dc9" - } - `, - snakeJSON: ` - { - "kind": "service-resolver", - "name": "main", - "partition": "east", - "redirect": { - "service": "other", - "service_subset": "backup", - "namespace": "alt", - "partition": "west", - "datacenter": "dc9" - } - } - `, - camelJSON: ` - { - "Kind": "service-resolver", - "Name": "main", - "Partition": "east", - "Redirect": { - "Service": "other", - "ServiceSubset": "backup", - "Namespace": "alt", - "Partition": "west", - "Datacenter": "dc9" - } - } - `, - expect: &api.ServiceResolverConfigEntry{ - Kind: "service-resolver", - Name: "main", - Partition: "east", - Redirect: &api.ServiceResolverRedirect{ - Service: "other", - ServiceSubset: "backup", - Namespace: "alt", - Partition: "west", - Datacenter: "dc9", - }, - }, - }, - { - name: "service-resolver: default", - snake: ` - kind = "service-resolver" - name = "main" - `, - camel: ` - Kind = "service-resolver" - Name = "main" - `, - snakeJSON: ` - { - "kind": "service-resolver", - "name": "main" - } - `, - camelJSON: ` - { - "Kind": "service-resolver", - "Name": "main" - } - `, - expect: &api.ServiceResolverConfigEntry{ - Kind: "service-resolver", - Name: "main", - }, - }, - { - name: "service-resolver: envoy hash lb kitchen sink", - snake: ` - kind = "service-resolver" - name = "main" - load_balancer = { - policy = "ring_hash" - ring_hash_config = { - minimum_ring_size = 1 - maximum_ring_size = 2 - } - hash_policies = [ - { - field = "cookie" - field_value = "good-cookie" - cookie_config = { - ttl = "1s" - path = "/oven" - } - terminal = true - }, - { - field = "cookie" - field_value = "less-good-cookie" - cookie_config = { - session = true - path = "/toaster" - } - terminal = true - }, - { - field = "header" - field_value = "x-user-id" - }, - { - source_ip = true - } - ] - } - `, - camel: ` - Kind = "service-resolver" - Name = "main" - LoadBalancer = { - Policy = "ring_hash" - RingHashConfig = { - MinimumRingSize = 1 - MaximumRingSize = 2 - } - HashPolicies = [ - { - Field = "cookie" - FieldValue = "good-cookie" - CookieConfig = { - TTL = "1s" - Path = "/oven" - } - Terminal = true - }, - { - Field = "cookie" - FieldValue = "less-good-cookie" - CookieConfig = { - Session = true - Path = "/toaster" - } - Terminal = true - }, - { - Field = "header" - FieldValue = "x-user-id" - }, - { - SourceIP = true - } - ] - } - `, - snakeJSON: ` - { - "kind": "service-resolver", - "name": "main", - "load_balancer": { - "policy": "ring_hash", - "ring_hash_config": { - "minimum_ring_size": 1, - "maximum_ring_size": 2 - }, - "hash_policies": [ - { - "field": "cookie", - "field_value": "good-cookie", - "cookie_config": { - "ttl": "1s", - "path": "/oven" - }, - "terminal": true - }, - { - "field": "cookie", - "field_value": "less-good-cookie", - "cookie_config": { - "session": true, - "path": "/toaster" - }, - "terminal": true - }, - { - "field": "header", - "field_value": "x-user-id" - }, - { - "source_ip": true - } - ] - } - } - `, - camelJSON: ` - { - "Kind": "service-resolver", - "Name": "main", - "LoadBalancer": { - "Policy": "ring_hash", - "RingHashConfig": { - "MinimumRingSize": 1, - "MaximumRingSize": 2 - }, - "HashPolicies": [ - { - "Field": "cookie", - "FieldValue": "good-cookie", - "CookieConfig": { - "TTL": "1s", - "Path": "/oven" - }, - "Terminal": true - }, - { - "Field": "cookie", - "FieldValue": "less-good-cookie", - "CookieConfig": { - "Session": true, - "Path": "/toaster" - }, - "Terminal": true - }, - { - "Field": "header", - "FieldValue": "x-user-id" - }, - { - "SourceIP": true - } - ] - } - } - `, - expect: &api.ServiceResolverConfigEntry{ - Kind: "service-resolver", - Name: "main", - LoadBalancer: &api.LoadBalancer{ - Policy: structs.LBPolicyRingHash, - RingHashConfig: &api.RingHashConfig{ - MinimumRingSize: 1, - MaximumRingSize: 2, - }, - HashPolicies: []api.HashPolicy{ - { - Field: structs.HashPolicyCookie, - FieldValue: "good-cookie", - CookieConfig: &api.CookieConfig{ - TTL: 1 * time.Second, - Path: "/oven", - }, - Terminal: true, - }, - { - Field: structs.HashPolicyCookie, - FieldValue: "less-good-cookie", - CookieConfig: &api.CookieConfig{ - Session: true, - Path: "/toaster", - }, - Terminal: true, - }, - { - Field: structs.HashPolicyHeader, - FieldValue: "x-user-id", - }, - { - SourceIP: true, - }, - }, - }, - }, - }, - { - name: "service-resolver: envoy least request kitchen sink", - snake: ` - kind = "service-resolver" - name = "main" - load_balancer = { - policy = "least_request" - least_request_config = { - choice_count = 2 - } - } - `, - camel: ` - Kind = "service-resolver" - Name = "main" - LoadBalancer = { - Policy = "least_request" - LeastRequestConfig = { - ChoiceCount = 2 - } - } - `, - snakeJSON: ` - { - "kind": "service-resolver", - "name": "main", - "load_balancer": { - "policy": "least_request", - "least_request_config": { - "choice_count": 2 - } - } - } - `, - camelJSON: ` - { - "Kind": "service-resolver", - "Name": "main", - "LoadBalancer": { - "Policy": "least_request", - "LeastRequestConfig": { - "ChoiceCount": 2 - } - } - } - `, - expect: &api.ServiceResolverConfigEntry{ - Kind: "service-resolver", - Name: "main", - LoadBalancer: &api.LoadBalancer{ - Policy: structs.LBPolicyLeastRequest, - LeastRequestConfig: &api.LeastRequestConfig{ - ChoiceCount: 2, - }, - }, - }, - }, - { - name: "expose paths: kitchen sink proxy defaults", - snake: ` - kind = "proxy-defaults" - name = "global" - expose = { - checks = true - paths = [ - { - local_path_port = 8080 - listener_port = 21500 - path = "/healthz" - protocol = "http2" - }, - { - local_path_port = 8000 - listener_port = 21501 - path = "/metrics" - protocol = "http" - } - ] - }`, - camel: ` - Kind = "proxy-defaults" - Name = "global" - Expose = { - Checks = true - Paths = [ - { - LocalPathPort = 8080 - ListenerPort = 21500 - Path = "/healthz" - Protocol = "http2" - }, - { - LocalPathPort = 8000 - ListenerPort = 21501 - Path = "/metrics" - Protocol = "http" - } - ] - }`, - snakeJSON: ` - { - "kind": "proxy-defaults", - "name": "global", - "expose": { - "checks": true, - "paths": [ - { - "local_path_port": 8080, - "listener_port": 21500, - "path": "/healthz", - "protocol": "http2" - }, - { - "local_path_port": 8000, - "listener_port": 21501, - "path": "/metrics", - "protocol": "http" - } - ] - } - } - `, - camelJSON: ` - { - "Kind": "proxy-defaults", - "Name": "global", - "Expose": { - "Checks": true, - "Paths": [ - { - "LocalPathPort": 8080, - "ListenerPort": 21500, - "Path": "/healthz", - "Protocol": "http2" - }, - { - "LocalPathPort": 8000, - "ListenerPort": 21501, - "Path": "/metrics", - "Protocol": "http" - } - ] - } - } - `, - expect: &api.ProxyConfigEntry{ - Kind: "proxy-defaults", - Name: "global", - Expose: api.ExposeConfig{ - Checks: true, - Paths: []api.ExposePath{ - { - ListenerPort: 21500, - Path: "/healthz", - LocalPathPort: 8080, - Protocol: "http2", - }, - { - ListenerPort: 21501, - Path: "/metrics", - LocalPathPort: 8000, - Protocol: "http", - }, - }, - }, - }, - }, - { - name: "expose paths: kitchen sink service defaults", - snake: ` - kind = "service-defaults" - name = "web" - expose = { - checks = true - paths = [ - { - local_path_port = 8080 - listener_port = 21500 - path = "/healthz" - protocol = "http2" - }, - { - local_path_port = 8000 - listener_port = 21501 - path = "/metrics" - protocol = "http" - } - ] - }`, - camel: ` - Kind = "service-defaults" - Name = "web" - Expose = { - Checks = true - Paths = [ - { - LocalPathPort = 8080 - ListenerPort = 21500 - Path = "/healthz" - Protocol = "http2" - }, - { - LocalPathPort = 8000 - ListenerPort = 21501 - Path = "/metrics" - Protocol = "http" - } - ] - }`, - snakeJSON: ` - { - "kind": "service-defaults", - "name": "web", - "expose": { - "checks": true, - "paths": [ - { - "local_path_port": 8080, - "listener_port": 21500, - "path": "/healthz", - "protocol": "http2" - }, - { - "local_path_port": 8000, - "listener_port": 21501, - "path": "/metrics", - "protocol": "http" - } - ] - } - } - `, - camelJSON: ` - { - "Kind": "service-defaults", - "Name": "web", - "Expose": { - "Checks": true, - "Paths": [ - { - "LocalPathPort": 8080, - "ListenerPort": 21500, - "Path": "/healthz", - "Protocol": "http2" - }, - { - "LocalPathPort": 8000, - "ListenerPort": 21501, - "Path": "/metrics", - "Protocol": "http" - } - ] - } - } - `, - expect: &api.ServiceConfigEntry{ - Kind: "service-defaults", - Name: "web", - Expose: api.ExposeConfig{ - Checks: true, - Paths: []api.ExposePath{ - { - ListenerPort: 21500, - Path: "/healthz", - LocalPathPort: 8080, - Protocol: "http2", - }, - { - ListenerPort: 21501, - Path: "/metrics", - LocalPathPort: 8000, - Protocol: "http", - }, - }, - }, - }, - }, - { - // TODO(rb): test SDS stuff here in both places (global/service) - name: "ingress-gateway: kitchen sink", - snake: ` - kind = "ingress-gateway" - name = "ingress-web" - meta { - "foo" = "bar" - "gir" = "zim" - } - tls { - enabled = true - tls_min_version = "TLSv1_1" - tls_max_version = "TLSv1_2" - cipher_suites = [ - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" - ] - } - listeners = [ - { - port = 8080 - protocol = "http" - services = [ - { - name = "web" - hosts = ["test.example.com"] - }, - { - name = "db" - namespace = "foo" - } - ] - } - ] - `, - camel: ` - Kind = "ingress-gateway" - Name = "ingress-web" - Meta { - "foo" = "bar" - "gir" = "zim" - } - Tls { - Enabled = true - TLSMinVersion = "TLSv1_1" - TLSMaxVersion = "TLSv1_2" - CipherSuites = [ - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" - ] - } - Listeners = [ - { - Port = 8080 - Protocol = "http" - Services = [ - { - Name = "web" - Hosts = ["test.example.com"] - }, - { - Name = "db" - Namespace = "foo" - } - ] - } - ] - `, - snakeJSON: ` - { - "kind": "ingress-gateway", - "name": "ingress-web", - "meta" : { - "foo": "bar", - "gir": "zim" - }, - "tls": { - "enabled": true, - "tls_min_version": "TLSv1_1", - "tls_max_version": "TLSv1_2", - "cipher_suites": [ - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" - ] - }, - "listeners": [ - { - "port": 8080, - "protocol": "http", - "services": [ - { - "name": "web", - "hosts": ["test.example.com"] - }, - { - "name": "db", - "namespace": "foo" - } - ] - } - ] - } - `, - camelJSON: ` - { - "Kind": "ingress-gateway", - "Name": "ingress-web", - "Meta" : { - "foo": "bar", - "gir": "zim" - }, - "TLS": { - "Enabled": true, - "TLSMinVersion": "TLSv1_1", - "TLSMaxVersion": "TLSv1_2", - "CipherSuites": [ - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" - ] - }, - "Listeners": [ - { - "Port": 8080, - "Protocol": "http", - "Services": [ - { - "Name": "web", - "Hosts": ["test.example.com"] - }, - { - "Name": "db", - "Namespace": "foo" - } - ] - } - ] - } - `, - expect: &api.IngressGatewayConfigEntry{ - Kind: "ingress-gateway", - Name: "ingress-web", - Meta: map[string]string{ - "foo": "bar", - "gir": "zim", - }, - TLS: api.GatewayTLSConfig{ - Enabled: true, - TLSMinVersion: "TLSv1_1", - TLSMaxVersion: "TLSv1_2", - CipherSuites: []string{ - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - }, - }, - Listeners: []api.IngressListener{ - { - Port: 8080, - Protocol: "http", - Services: []api.IngressService{ - { - Name: "web", - Hosts: []string{"test.example.com"}, - }, - { - Name: "db", - Namespace: "foo", - }, - }, - }, - }, - }, - }, - { - name: "service-intentions: kitchen sink", - snake: ` - kind = "service-intentions" - name = "web" - meta { - "foo" = "bar" - "gir" = "zim" - } - sources = [ - { - name = "foo" - action = "deny" - type = "consul" - description = "foo desc" - }, - { - name = "bar" - action = "allow" - description = "bar desc" - }, - { - name = "l7" - permissions = [ - { - action = "deny" - http { - path_exact = "/admin" - header = [ - { - name = "hdr-present" - present = true - }, - { - name = "hdr-exact" - exact = "exact" - }, - { - name = "hdr-prefix" - prefix = "prefix" - }, - { - name = "hdr-suffix" - suffix = "suffix" - }, - { - name = "hdr-regex" - regex = "regex" - }, - { - name = "hdr-absent" - present = true - invert = true - } - ] - } - }, - { - action = "allow" - http { - path_prefix = "/v3/" - } - }, - { - action = "allow" - http { - path_regex = "/v[12]/.*" - methods = ["GET", "POST"] - } - } - ] - } - ] - sources { - name = "*" - action = "deny" - description = "wild desc" - } - `, - camel: ` - Kind = "service-intentions" - Name = "web" - Meta { - "foo" = "bar" - "gir" = "zim" - } - Sources = [ - { - Name = "foo" - Action = "deny" - Type = "consul" - Description = "foo desc" - }, - { - Name = "bar" - Action = "allow" - Description = "bar desc" - }, - { - Name = "l7" - Permissions = [ - { - Action = "deny" - HTTP { - PathExact = "/admin" - Header = [ - { - Name = "hdr-present" - Present = true - }, - { - Name = "hdr-exact" - Exact = "exact" - }, - { - Name = "hdr-prefix" - Prefix = "prefix" - }, - { - Name = "hdr-suffix" - Suffix = "suffix" - }, - { - Name = "hdr-regex" - Regex = "regex" - }, - { - Name = "hdr-absent" - Present = true - Invert = true - } - ] - } - }, - { - Action = "allow" - HTTP { - PathPrefix = "/v3/" - } - }, - { - Action = "allow" - HTTP { - PathRegex = "/v[12]/.*" - Methods = ["GET", "POST"] - } - } - ] - } - ] - Sources { - Name = "*" - Action = "deny" - Description = "wild desc" - } - `, - snakeJSON: ` - { - "kind": "service-intentions", - "name": "web", - "meta": { - "foo": "bar", - "gir": "zim" - }, - "sources": [ - { - "name": "foo", - "action": "deny", - "type": "consul", - "description": "foo desc" - }, - { - "name": "bar", - "action": "allow", - "description": "bar desc" - }, - { - "name": "l7", - "permissions": [ - { - "action": "deny", - "http": { - "path_exact": "/admin", - "header": [ - { - "name": "hdr-present", - "present": true - }, - { - "name": "hdr-exact", - "exact": "exact" - }, - { - "name": "hdr-prefix", - "prefix": "prefix" - }, - { - "name": "hdr-suffix", - "suffix": "suffix" - }, - { - "name": "hdr-regex", - "regex": "regex" - }, - { - "name": "hdr-absent", - "present": true, - "invert": true - } - ] - } - }, - { - "action": "allow", - "http": { - "path_prefix": "/v3/" - } - }, - { - "action": "allow", - "http": { - "path_regex": "/v[12]/.*", - "methods": [ - "GET", - "POST" - ] - } - } - ] - }, - { - "name": "*", - "action": "deny", - "description": "wild desc" - } - ] - } - `, - camelJSON: ` - { - "Kind": "service-intentions", - "Name": "web", - "Meta" : { - "foo": "bar", - "gir": "zim" - }, - "Sources": [ - { - "Name": "foo", - "Action": "deny", - "Type": "consul", - "Description": "foo desc" - }, - { - "Name": "bar", - "Action": "allow", - "Description": "bar desc" - }, - { - "Name": "l7", - "Permissions": [ - { - "Action": "deny", - "HTTP": { - "PathExact": "/admin", - "Header": [ - { - "Name": "hdr-present", - "Present": true - }, - { - "Name": "hdr-exact", - "Exact": "exact" - }, - { - "Name": "hdr-prefix", - "Prefix": "prefix" - }, - { - "Name": "hdr-suffix", - "Suffix": "suffix" - }, - { - "Name": "hdr-regex", - "Regex": "regex" - }, - { - "Name": "hdr-absent", - "Present": true, - "Invert": true - } - ] - } - }, - { - "Action": "allow", - "HTTP": { - "PathPrefix": "/v3/" - } - }, - { - "Action": "allow", - "HTTP": { - "PathRegex": "/v[12]/.*", - "Methods": [ - "GET", - "POST" - ] - } - } - ] - }, - { - "Name": "*", - "Action": "deny", - "Description": "wild desc" - } - ] - } - `, - expect: &api.ServiceIntentionsConfigEntry{ - Kind: "service-intentions", - Name: "web", - Meta: map[string]string{ - "foo": "bar", - "gir": "zim", - }, - Sources: []*api.SourceIntention{ - { - Name: "foo", - Action: "deny", - Type: "consul", - Description: "foo desc", - }, - { - Name: "bar", - Action: "allow", - Description: "bar desc", - }, - { - Name: "l7", - Permissions: []*api.IntentionPermission{ - { - Action: "deny", - HTTP: &api.IntentionHTTPPermission{ - PathExact: "/admin", - Header: []api.IntentionHTTPHeaderPermission{ - { - Name: "hdr-present", - Present: true, - }, - { - Name: "hdr-exact", - Exact: "exact", - }, - { - Name: "hdr-prefix", - Prefix: "prefix", - }, - { - Name: "hdr-suffix", - Suffix: "suffix", - }, - { - Name: "hdr-regex", - Regex: "regex", - }, - { - Name: "hdr-absent", - Present: true, - Invert: true, - }, - }, - }, - }, - { - Action: "allow", - HTTP: &api.IntentionHTTPPermission{ - PathPrefix: "/v3/", - }, - }, - { - Action: "allow", - HTTP: &api.IntentionHTTPPermission{ - PathRegex: "/v[12]/.*", - Methods: []string{"GET", "POST"}, - }, - }, - }, - }, - { - Name: "*", - Action: "deny", - Description: "wild desc", - }, - }, - }, - }, - { - name: "service-intentions: wildcard destination", - snake: ` - kind = "service-intentions" - name = "*" - sources { - name = "foo" - action = "deny" - # should be parsed, but we'll ignore it later - precedence = 6 - } - `, - camel: ` - Kind = "service-intentions" - Name = "*" - Sources { - Name = "foo" - Action = "deny" - # should be parsed, but we'll ignore it later - Precedence = 6 - } - `, - snakeJSON: ` - { - "kind": "service-intentions", - "name": "*", - "sources": [ - { - "name": "foo", - "action": "deny", - "precedence": 6 - } - ] - } - `, - camelJSON: ` - { - "Kind": "service-intentions", - "Name": "*", - "Sources": [ - { - "Name": "foo", - "Action": "deny", - "Precedence": 6 - } - ] - } - `, - expect: &api.ServiceIntentionsConfigEntry{ - Kind: "service-intentions", - Name: "*", - Sources: []*api.SourceIntention{ - { - Name: "foo", - Action: "deny", - Precedence: 6, - }, - }, - }, - }, - { - name: "mesh", - snake: ` - kind = "mesh" - meta { - "foo" = "bar" - "gir" = "zim" - } - transparent_proxy { - mesh_destinations_only = true - } - tls { - incoming { - tls_min_version = "TLSv1_1" - tls_max_version = "TLSv1_2" - cipher_suites = [ - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" - ] - } - outgoing { - tls_min_version = "TLSv1_1" - tls_max_version = "TLSv1_2" - cipher_suites = [ - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" - ] - } - } - `, - camel: ` - Kind = "mesh" - Meta { - "foo" = "bar" - "gir" = "zim" - } - TransparentProxy { - MeshDestinationsOnly = true - } - TLS { - Incoming { - TLSMinVersion = "TLSv1_1" - TLSMaxVersion = "TLSv1_2" - CipherSuites = [ - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" - ] - } - Outgoing { - TLSMinVersion = "TLSv1_1" - TLSMaxVersion = "TLSv1_2" - CipherSuites = [ - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" - ] - } - } - `, - snakeJSON: ` - { - "kind": "mesh", - "meta" : { - "foo": "bar", - "gir": "zim" - }, - "transparent_proxy": { - "mesh_destinations_only": true - }, - "tls": { - "incoming": { - "tls_min_version": "TLSv1_1", - "tls_max_version": "TLSv1_2", - "cipher_suites": [ - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" - ] - }, - "outgoing": { - "tls_min_version": "TLSv1_1", - "tls_max_version": "TLSv1_2", - "cipher_suites": [ - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" - ] - } - } - } - `, - camelJSON: ` - { - "Kind": "mesh", - "Meta" : { - "foo": "bar", - "gir": "zim" - }, - "TransparentProxy": { - "MeshDestinationsOnly": true - }, - "TLS": { - "Incoming": { - "TLSMinVersion": "TLSv1_1", - "TLSMaxVersion": "TLSv1_2", - "CipherSuites": [ - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" - ] - }, - "Outgoing": { - "TLSMinVersion": "TLSv1_1", - "TLSMaxVersion": "TLSv1_2", - "CipherSuites": [ - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" - ] - } - } - } - `, - expect: &api.MeshConfigEntry{ - Meta: map[string]string{ - "foo": "bar", - "gir": "zim", - }, - TransparentProxy: api.TransparentProxyMeshConfig{ - MeshDestinationsOnly: true, - }, - TLS: &api.MeshTLSConfig{ - Incoming: &api.MeshDirectionalTLSConfig{ - TLSMinVersion: "TLSv1_1", - TLSMaxVersion: "TLSv1_2", - CipherSuites: []string{ - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - }, - }, - Outgoing: &api.MeshDirectionalTLSConfig{ - TLSMinVersion: "TLSv1_1", - TLSMaxVersion: "TLSv1_2", - CipherSuites: []string{ - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - }, - }, - }, - }, - }, - { - name: "exported-services", - snake: ` - kind = "exported-services" - name = "foo" - meta { - "foo" = "bar" - "gir" = "zim" - } - services = [ - { - name = "web" - namespace = "foo" - consumers = [ - { - partition = "bar" - }, - { - partition = "baz" - }, - { - peer_name = "flarm" - } - ] - }, - { - name = "db" - namespace = "bar" - consumers = [ - { - partition = "zoo" - } - ] - } - ] - `, - camel: ` - Kind = "exported-services" - Name = "foo" - Meta { - "foo" = "bar" - "gir" = "zim" - } - Services = [ - { - Name = "web" - Namespace = "foo" - Consumers = [ - { - Partition = "bar" - }, - { - Partition = "baz" - }, - { - PeerName = "flarm" - } - ] - }, - { - Name = "db" - Namespace = "bar" - Consumers = [ - { - Partition = "zoo" - } - ] - } - ] - `, - snakeJSON: ` - { - "kind": "exported-services", - "name": "foo", - "meta": { - "foo": "bar", - "gir": "zim" - }, - "services": [ - { - "name": "web", - "namespace": "foo", - "consumers": [ - { - "partition": "bar" - }, - { - "partition": "baz" - }, - { - "peer_name": "flarm" - } - ] - }, - { - "name": "db", - "namespace": "bar", - "consumers": [ - { - "partition": "zoo" - } - ] - } - ] - } - `, - camelJSON: ` - { - "Kind": "exported-services", - "Name": "foo", - "Meta": { - "foo": "bar", - "gir": "zim" - }, - "Services": [ - { - "Name": "web", - "Namespace": "foo", - "Consumers": [ - { - "Partition": "bar" - }, - { - "Partition": "baz" - }, - { - "PeerName": "flarm" - } - ] - }, - { - "Name": "db", - "Namespace": "bar", - "Consumers": [ - { - "Partition": "zoo" - } - ] - } - ] - } - `, - expect: &api.ExportedServicesConfigEntry{ - Name: "foo", - Meta: map[string]string{ - "foo": "bar", - "gir": "zim", - }, - Services: []api.ExportedService{ - { - Name: "web", - Namespace: "foo", - Consumers: []api.ServiceConsumer{ - { - Partition: "bar", - }, - { - Partition: "baz", - }, - { - PeerName: "flarm", - }, - }, - }, - { - Name: "db", - Namespace: "bar", - Consumers: []api.ServiceConsumer{ - { - Partition: "zoo", - }, - }, - }, - }, - }, - }, - } { - tc := tc - - testbody := func(t *testing.T, body string, expect api.ConfigEntry) { - t.Helper() - got, err := parseConfigEntry(body) - if tc.expectErr != "" { - require.Nil(t, got) - require.Error(t, err) - requireContainsLower(t, err.Error(), tc.expectErr) - } else { - require.NoError(t, err) - require.Equal(t, expect, got) - } - } - - t.Run(tc.name+" (hcl snake case)", func(t *testing.T) { - testbody(t, tc.snake, tc.expect) - }) - t.Run(tc.name+" (hcl camel case)", func(t *testing.T) { - testbody(t, tc.camel, tc.expect) - }) - if tc.snakeJSON != "" { - t.Run(tc.name+" (json snake case)", func(t *testing.T) { - if tc.expectJSON != nil { - testbody(t, tc.snakeJSON, tc.expectJSON) - } else { - testbody(t, tc.snakeJSON, tc.expect) - } - }) - } - if tc.camelJSON != "" { - t.Run(tc.name+" (json camel case)", func(t *testing.T) { - if tc.expectJSON != nil { - testbody(t, tc.camelJSON, tc.expectJSON) - } else { - testbody(t, tc.camelJSON, tc.expect) - } - }) - } - } -} - func requireContainsLower(t *testing.T, haystack, needle string) { t.Helper() require.Contains(t, strings.ToLower(haystack), strings.ToLower(needle)) } - -func intPointer(v int) *int { - return &v -} diff --git a/command/config/write/decode_shim.go b/command/helpers/decode_shim.go similarity index 99% rename from command/config/write/decode_shim.go rename to command/helpers/decode_shim.go index c0ac6bf7f9..e7465ed810 100644 --- a/command/config/write/decode_shim.go +++ b/command/helpers/decode_shim.go @@ -1,4 +1,4 @@ -package write +package helpers import ( "encoding/json" diff --git a/command/helpers/helpers.go b/command/helpers/helpers.go index 965f261bd6..56ad6f7d36 100644 --- a/command/helpers/helpers.go +++ b/command/helpers/helpers.go @@ -6,6 +6,12 @@ import ( "io" "io/ioutil" "os" + "time" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/lib/decode" + "github.com/hashicorp/go-multierror" + "github.com/mitchellh/mapstructure" ) func loadFromFile(path string) (string, error) { @@ -59,3 +65,74 @@ func LoadDataSourceNoRaw(data string, testStdin io.Reader) (string, error) { return loadFromFile(data) } + +func ParseConfigEntry(data string) (api.ConfigEntry, error) { + // parse the data + var raw map[string]interface{} + if err := hclDecode(&raw, data); err != nil { + return nil, fmt.Errorf("Failed to decode config entry input: %v", err) + } + + return newDecodeConfigEntry(raw) +} + +// There is a 'structs' variation of this in +// agent/structs/config_entry.go:DecodeConfigEntry +func newDecodeConfigEntry(raw map[string]interface{}) (api.ConfigEntry, error) { + var entry api.ConfigEntry + + kindVal, ok := raw["Kind"] + if !ok { + kindVal, ok = raw["kind"] + } + if !ok { + return nil, fmt.Errorf("Payload does not contain a kind/Kind key at the top level") + } + + if kindStr, ok := kindVal.(string); ok { + newEntry, err := api.MakeConfigEntry(kindStr, "") + if err != nil { + return nil, err + } + entry = newEntry + } else { + return nil, fmt.Errorf("Kind value in payload is not a string") + } + + var md mapstructure.Metadata + decodeConf := &mapstructure.DecoderConfig{ + DecodeHook: mapstructure.ComposeDecodeHookFunc( + decode.HookWeakDecodeFromSlice, + decode.HookTranslateKeys, + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToTimeHookFunc(time.RFC3339), + ), + Metadata: &md, + Result: &entry, + WeaklyTypedInput: true, + } + + decoder, err := mapstructure.NewDecoder(decodeConf) + if err != nil { + return nil, err + } + + if err := decoder.Decode(raw); err != nil { + return nil, err + } + + for _, k := range md.Unused { + switch k { + case "kind", "Kind": + // The kind field is used to determine the target, but doesn't need + // to exist on the target. + continue + } + err = multierror.Append(err, fmt.Errorf("invalid config key %q", k)) + } + if err != nil { + return nil, err + } + + return entry, nil +} diff --git a/command/helpers/helpers_test.go b/command/helpers/helpers_test.go new file mode 100644 index 0000000000..82759159f3 --- /dev/null +++ b/command/helpers/helpers_test.go @@ -0,0 +1,3079 @@ +package helpers + +import ( + "strings" + "testing" + "time" + + "github.com/hashicorp/consul/agent/structs" + + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/api" +) + +// TestParseConfigEntry is the 'api' mirror image of +// agent/structs/config_entry_test.go:TestDecodeConfigEntry +func TestParseConfigEntry(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + camel, camelJSON string + snake, snakeJSON string + expect api.ConfigEntry + expectJSON api.ConfigEntry + expectErr string + }{ + { + name: "proxy-defaults: extra fields or typo", + snake: ` + kind = "proxy-defaults" + name = "main" + cornfig { + "foo" = 19 + } + `, + camel: ` + Kind = "proxy-defaults" + Name = "main" + Cornfig { + "foo" = 19 + } + `, + snakeJSON: ` + { + "kind": "proxy-defaults", + "name": "main", + "cornfig": { + "foo": 19 + } + } + `, + camelJSON: ` + { + "Kind": "proxy-defaults", + "Name": "main", + "Cornfig": { + "foo": 19 + } + } + `, + expectErr: `invalid config key "cornfig"`, + }, + { + name: "proxy-defaults", + snake: ` + kind = "proxy-defaults" + name = "main" + meta { + "foo" = "bar" + "gir" = "zim" + } + config { + "foo" = 19 + "bar" = "abc" + "moreconfig" { + "moar" = "config" + } + } + mesh_gateway { + mode = "remote" + } + mode = "direct" + transparent_proxy = { + outbound_listener_port = 10101 + dialed_directly = true + } + `, + camel: ` + Kind = "proxy-defaults" + Name = "main" + Meta { + "foo" = "bar" + "gir" = "zim" + } + Config { + "foo" = 19 + "bar" = "abc" + "moreconfig" { + "moar" = "config" + } + } + MeshGateway { + Mode = "remote" + } + Mode = "direct" + TransparentProxy = { + outbound_listener_port = 10101 + dialed_directly = true + } + `, + snakeJSON: ` + { + "kind": "proxy-defaults", + "name": "main", + "meta" : { + "foo": "bar", + "gir": "zim" + }, + "config": { + "foo": 19, + "bar": "abc", + "moreconfig": { + "moar": "config" + } + }, + "mesh_gateway": { + "mode": "remote" + }, + "mode": "direct", + "transparent_proxy": { + "outbound_listener_port": 10101, + "dialed_directly": true + } + } + `, + camelJSON: ` + { + "Kind": "proxy-defaults", + "Name": "main", + "Meta" : { + "foo": "bar", + "gir": "zim" + }, + "Config": { + "foo": 19, + "bar": "abc", + "moreconfig": { + "moar": "config" + } + }, + "MeshGateway": { + "Mode": "remote" + }, + "Mode": "direct", + "TransparentProxy": { + "OutboundListenerPort": 10101, + "DialedDirectly": true + } + } + `, + expect: &api.ProxyConfigEntry{ + Kind: "proxy-defaults", + Name: "main", + Meta: map[string]string{ + "foo": "bar", + "gir": "zim", + }, + Config: map[string]interface{}{ + "foo": 19, + "bar": "abc", + "moreconfig": map[string]interface{}{ + "moar": "config", + }, + }, + MeshGateway: api.MeshGatewayConfig{ + Mode: api.MeshGatewayModeRemote, + }, + Mode: api.ProxyModeDirect, + TransparentProxy: &api.TransparentProxyConfig{ + OutboundListenerPort: 10101, + DialedDirectly: true, + }, + }, + expectJSON: &api.ProxyConfigEntry{ + Kind: "proxy-defaults", + Name: "main", + Meta: map[string]string{ + "foo": "bar", + "gir": "zim", + }, + Config: map[string]interface{}{ + "foo": float64(19), // json decoding gives float64 instead of int here + "bar": "abc", + "moreconfig": map[string]interface{}{ + "moar": "config", + }, + }, + MeshGateway: api.MeshGatewayConfig{ + Mode: api.MeshGatewayModeRemote, + }, + Mode: api.ProxyModeDirect, + TransparentProxy: &api.TransparentProxyConfig{ + OutboundListenerPort: 10101, + DialedDirectly: true, + }, + }, + }, + { + name: "terminating-gateway", + snake: ` + kind = "terminating-gateway" + name = "terminating-gw-west" + namespace = "default" + meta { + "foo" = "bar" + "gir" = "zim" + } + services = [ + { + name = "billing" + namespace = "biz" + ca_file = "/etc/ca.crt" + cert_file = "/etc/client.crt" + key_file = "/etc/tls.key" + sni = "mydomain" + }, + { + name = "*" + namespace = "ops" + } + ] + `, + camel: ` + Kind = "terminating-gateway" + Name = "terminating-gw-west" + Namespace = "default" + Meta { + "foo" = "bar" + "gir" = "zim" + } + Services = [ + { + Name = "billing" + Namespace = "biz" + CAFile = "/etc/ca.crt" + CertFile = "/etc/client.crt" + KeyFile = "/etc/tls.key" + SNI = "mydomain" + }, + { + Name = "*" + Namespace = "ops" + } + ] + `, + snakeJSON: ` + { + "kind": "terminating-gateway", + "name": "terminating-gw-west", + "namespace": "default", + "meta" : { + "foo": "bar", + "gir": "zim" + }, + "services": [ + { + "name": "billing", + "namespace": "biz", + "ca_file": "/etc/ca.crt", + "cert_file": "/etc/client.crt", + "key_file": "/etc/tls.key", + "sni": "mydomain" + }, + { + "name": "*", + "namespace": "ops" + } + ] + } + `, + camelJSON: ` + { + "Kind": "terminating-gateway", + "Name": "terminating-gw-west", + "Namespace": "default", + "Meta" : { + "foo": "bar", + "gir": "zim" + }, + "Services": [ + { + "Name": "billing", + "Namespace": "biz", + "CAFile": "/etc/ca.crt", + "CertFile": "/etc/client.crt", + "KeyFile": "/etc/tls.key", + "SNI": "mydomain" + }, + { + "Name": "*", + "Namespace": "ops" + } + ] + } + `, + expect: &api.TerminatingGatewayConfigEntry{ + Kind: "terminating-gateway", + Name: "terminating-gw-west", + Namespace: "default", + Meta: map[string]string{ + "foo": "bar", + "gir": "zim", + }, + Services: []api.LinkedService{ + { + Name: "billing", + Namespace: "biz", + CAFile: "/etc/ca.crt", + CertFile: "/etc/client.crt", + KeyFile: "/etc/tls.key", + SNI: "mydomain", + }, + { + Name: "*", + Namespace: "ops", + }, + }, + }, + expectJSON: &api.TerminatingGatewayConfigEntry{ + Kind: "terminating-gateway", + Name: "terminating-gw-west", + Namespace: "default", + Meta: map[string]string{ + "foo": "bar", + "gir": "zim", + }, + Services: []api.LinkedService{ + { + Name: "billing", + Namespace: "biz", + CAFile: "/etc/ca.crt", + CertFile: "/etc/client.crt", + KeyFile: "/etc/tls.key", + SNI: "mydomain", + }, + { + Name: "*", + Namespace: "ops", + }, + }, + }, + }, + { + name: "service-defaults: kitchen sink (upstreams edition)", + snake: ` + kind = "service-defaults" + name = "main" + meta { + "foo" = "bar" + "gir" = "zim" + } + protocol = "http" + external_sni = "abc-123" + mesh_gateway { + mode = "remote" + } + mode = "direct" + transparent_proxy = { + outbound_listener_port = 10101 + dialed_directly = true + } + upstream_config { + overrides = [ + { + name = "redis" + passive_health_check { + max_failures = 3 + interval = "2s" + } + envoy_listener_json = "{ \"listener-foo\": 5 }" + envoy_cluster_json = "{ \"cluster-bar\": 5 }" + protocol = "grpc" + connect_timeout_ms = 6543 + }, + { + name = "finance--billing" + mesh_gateway { + mode = "remote" + } + limits { + max_connections = 1111 + max_pending_requests = 2222 + max_concurrent_requests = 3333 + } + } + ] + defaults { + envoy_cluster_json = "zip" + envoy_listener_json = "zop" + connect_timeout_ms = 5000 + protocol = "http" + limits { + max_connections = 3 + max_pending_requests = 4 + max_concurrent_requests = 5 + } + passive_health_check { + max_failures = 5 + interval = "4s" + } + } + } + `, + camel: ` + Kind = "service-defaults" + Name = "main" + Meta { + "foo" = "bar" + "gir" = "zim" + } + Protocol = "http" + ExternalSNI = "abc-123" + MeshGateway { + Mode = "remote" + } + Mode = "direct" + TransparentProxy = { + outbound_listener_port = 10101 + dialed_directly = true + } + UpstreamConfig { + Overrides = [ + { + Name = "redis" + PassiveHealthCheck { + MaxFailures = 3 + Interval = "2s" + } + EnvoyListenerJson = "{ \"listener-foo\": 5 }" + EnvoyClusterJson = "{ \"cluster-bar\": 5 }" + Protocol = "grpc" + ConnectTimeoutMs = 6543 + }, + { + Name = "finance--billing" + MeshGateway { + Mode = "remote" + } + Limits { + MaxConnections = 1111 + MaxPendingRequests = 2222 + MaxConcurrentRequests = 3333 + } + } + ] + Defaults { + EnvoyClusterJson = "zip" + EnvoyListenerJson = "zop" + ConnectTimeoutMs = 5000 + Protocol = "http" + Limits { + MaxConnections = 3 + MaxPendingRequests = 4 + MaxConcurrentRequests = 5 + } + PassiveHealthCheck { + MaxFailures = 5 + Interval = "4s" + } + } + } + `, + snakeJSON: ` + { + "kind": "service-defaults", + "name": "main", + "meta" : { + "foo": "bar", + "gir": "zim" + }, + "protocol": "http", + "external_sni": "abc-123", + "mesh_gateway": { + "mode": "remote" + }, + "mode": "direct", + "transparent_proxy": { + "outbound_listener_port": 10101, + "dialed_directly": true + }, + "upstream_config": { + "overrides": [ + { + "name": "redis", + "passive_health_check": { + "max_failures": 3, + "interval": "2s" + }, + "envoy_listener_json": "{ \"listener-foo\": 5 }", + "envoy_cluster_json": "{ \"cluster-bar\": 5 }", + "protocol": "grpc", + "connect_timeout_ms": 6543 + }, + { + "name": "finance--billing", + "mesh_gateway": { + "mode": "remote" + }, + "limits": { + "max_connections": 1111, + "max_pending_requests": 2222, + "max_concurrent_requests": 3333 + } + } + ], + "defaults": { + "envoy_cluster_json": "zip", + "envoy_listener_json": "zop", + "connect_timeout_ms": 5000, + "protocol": "http", + "limits": { + "max_connections": 3, + "max_pending_requests": 4, + "max_concurrent_requests": 5 + }, + "passive_health_check": { + "max_failures": 5, + "interval": "4s" + } + } + } + } + `, + camelJSON: ` + { + "Kind": "service-defaults", + "Name": "main", + "Meta" : { + "foo": "bar", + "gir": "zim" + }, + "Protocol": "http", + "ExternalSNI": "abc-123", + "MeshGateway": { + "Mode": "remote" + }, + "Mode": "direct", + "TransparentProxy": { + "OutboundListenerPort": 10101, + "DialedDirectly": true + }, + "UpstreamConfig": { + "Overrides": [ + { + "Name": "redis", + "PassiveHealthCheck": { + "MaxFailures": 3, + "Interval": "2s" + }, + "EnvoyListenerJson": "{ \"listener-foo\": 5 }", + "EnvoyClusterJson": "{ \"cluster-bar\": 5 }", + "Protocol": "grpc", + "ConnectTimeoutMs": 6543 + }, + { + "Name": "finance--billing", + "MeshGateway": { + "Mode": "remote" + }, + "Limits": { + "MaxConnections": 1111, + "MaxPendingRequests": 2222, + "MaxConcurrentRequests": 3333 + } + } + ], + "Defaults": { + "EnvoyClusterJson": "zip", + "EnvoyListenerJson": "zop", + "ConnectTimeoutMs": 5000, + "Protocol": "http", + "Limits": { + "MaxConnections": 3, + "MaxPendingRequests": 4, + "MaxConcurrentRequests": 5 + }, + "PassiveHealthCheck": { + "MaxFailures": 5, + "Interval": "4s" + } + } + } + } + `, + expect: &api.ServiceConfigEntry{ + Kind: "service-defaults", + Name: "main", + Meta: map[string]string{ + "foo": "bar", + "gir": "zim", + }, + Protocol: "http", + ExternalSNI: "abc-123", + MeshGateway: api.MeshGatewayConfig{ + Mode: api.MeshGatewayModeRemote, + }, + Mode: api.ProxyModeDirect, + TransparentProxy: &api.TransparentProxyConfig{ + OutboundListenerPort: 10101, + DialedDirectly: true, + }, + UpstreamConfig: &api.UpstreamConfiguration{ + Overrides: []*api.UpstreamConfig{ + { + Name: "redis", + PassiveHealthCheck: &api.PassiveHealthCheck{ + MaxFailures: 3, + Interval: 2 * time.Second, + }, + EnvoyListenerJSON: `{ "listener-foo": 5 }`, + EnvoyClusterJSON: `{ "cluster-bar": 5 }`, + Protocol: "grpc", + ConnectTimeoutMs: 6543, + }, + { + Name: "finance--billing", + MeshGateway: api.MeshGatewayConfig{ + Mode: "remote", + }, + Limits: &api.UpstreamLimits{ + MaxConnections: intPointer(1111), + MaxPendingRequests: intPointer(2222), + MaxConcurrentRequests: intPointer(3333), + }, + }, + }, + Defaults: &api.UpstreamConfig{ + EnvoyClusterJSON: "zip", + EnvoyListenerJSON: "zop", + Protocol: "http", + ConnectTimeoutMs: 5000, + Limits: &api.UpstreamLimits{ + MaxConnections: intPointer(3), + MaxPendingRequests: intPointer(4), + MaxConcurrentRequests: intPointer(5), + }, + PassiveHealthCheck: &api.PassiveHealthCheck{ + MaxFailures: 5, + Interval: 4 * time.Second, + }, + }, + }, + }, + }, + { + name: "service-defaults: kitchen sink (destination edition)", + snake: ` + kind = "service-defaults" + name = "main" + meta { + "foo" = "bar" + "gir" = "zim" + } + protocol = "grpc" + mesh_gateway { + mode = "remote" + } + mode = "transparent" + transparent_proxy = { + outbound_listener_port = 10101 + dialed_directly = true + } + destination = { + address = "10.0.0.0/16", + port = 443 + } + `, + camel: ` + Kind = "service-defaults" + Name = "main" + Meta { + "foo" = "bar" + "gir" = "zim" + } + Protocol = "grpc" + MeshGateway { + Mode = "remote" + } + Mode = "transparent" + TransparentProxy = { + outbound_listener_port = 10101 + dialed_directly = true + } + Destination = { + Address = "10.0.0.0/16", + Port = 443 + } + `, + snakeJSON: ` + { + "kind": "service-defaults", + "name": "main", + "meta" : { + "foo": "bar", + "gir": "zim" + }, + "protocol": "grpc", + "mesh_gateway": { + "mode": "remote" + }, + "mode": "transparent", + "transparent_proxy": { + "outbound_listener_port": 10101, + "dialed_directly": true + }, + "destination": { + "address": "10.0.0.0/16", + "port": 443 + } + } + `, + camelJSON: ` + { + "Kind": "service-defaults", + "Name": "main", + "Meta" : { + "foo": "bar", + "gir": "zim" + }, + "Protocol": "grpc", + "MeshGateway": { + "Mode": "remote" + }, + "Mode": "transparent", + "TransparentProxy": { + "OutboundListenerPort": 10101, + "DialedDirectly": true + }, + "Destination": { + "Address": "10.0.0.0/16", + "Port": 443 + } + } + `, + expect: &api.ServiceConfigEntry{ + Kind: "service-defaults", + Name: "main", + Meta: map[string]string{ + "foo": "bar", + "gir": "zim", + }, + Protocol: "grpc", + MeshGateway: api.MeshGatewayConfig{ + Mode: api.MeshGatewayModeRemote, + }, + Mode: api.ProxyModeTransparent, + TransparentProxy: &api.TransparentProxyConfig{ + OutboundListenerPort: 10101, + DialedDirectly: true, + }, + Destination: &api.DestinationConfig{ + Address: "10.0.0.0/16", + Port: 443, + }, + }, + }, + { + name: "service-router: kitchen sink", + snake: ` + kind = "service-router" + name = "main" + partition = "pepper" + meta { + "foo" = "bar" + "gir" = "zim" + } + routes = [ + { + match { + http { + path_exact = "/foo" + header = [ + { + name = "debug1" + present = true + }, + { + name = "debug2" + present = false + invert = true + }, + { + name = "debug3" + exact = "1" + }, + { + name = "debug4" + prefix = "aaa" + }, + { + name = "debug5" + suffix = "bbb" + }, + { + name = "debug6" + regex = "a.*z" + }, + ] + } + } + destination { + service = "carrot" + service_subset = "kale" + namespace = "leek" + partition = "chard" + prefix_rewrite = "/alternate" + request_timeout = "99s" + num_retries = 12345 + retry_on_connect_failure = true + retry_on_status_codes = [401, 209] + } + }, + { + match { + http { + path_prefix = "/foo" + methods = [ "GET", "DELETE" ] + query_param = [ + { + name = "hack1" + present = true + }, + { + name = "hack2" + exact = "1" + }, + { + name = "hack3" + regex = "a.*z" + }, + ] + } + } + }, + { + match { + http { + path_regex = "/foo" + } + } + }, + ] + `, + camel: ` + Kind = "service-router" + Name = "main" + Partition = "pepper" + Meta { + "foo" = "bar" + "gir" = "zim" + } + Routes = [ + { + Match { + HTTP { + PathExact = "/foo" + Header = [ + { + Name = "debug1" + Present = true + }, + { + Name = "debug2" + Present = false + Invert = true + }, + { + Name = "debug3" + Exact = "1" + }, + { + Name = "debug4" + Prefix = "aaa" + }, + { + Name = "debug5" + Suffix = "bbb" + }, + { + Name = "debug6" + Regex = "a.*z" + }, + ] + } + } + Destination { + Service = "carrot" + ServiceSubset = "kale" + Namespace = "leek" + Partition = "chard" + PrefixRewrite = "/alternate" + RequestTimeout = "99s" + NumRetries = 12345 + RetryOnConnectFailure = true + RetryOnStatusCodes = [401, 209] + } + }, + { + Match { + HTTP { + PathPrefix = "/foo" + Methods = [ "GET", "DELETE" ] + QueryParam = [ + { + Name = "hack1" + Present = true + }, + { + Name = "hack2" + Exact = "1" + }, + { + Name = "hack3" + Regex = "a.*z" + }, + ] + } + } + }, + { + Match { + HTTP { + PathRegex = "/foo" + } + } + }, + ] + `, + snakeJSON: ` + { + "kind": "service-router", + "name": "main", + "partition": "pepper", + "meta" : { + "foo": "bar", + "gir": "zim" + }, + "routes": [ + { + "match": { + "http": { + "path_exact": "/foo", + "header": [ + { + "name": "debug1", + "present": true + }, + { + "name": "debug2", + "present": false, + "invert": true + }, + { + "name": "debug3", + "exact": "1" + }, + { + "name": "debug4", + "prefix": "aaa" + }, + { + "name": "debug5", + "suffix": "bbb" + }, + { + "name": "debug6", + "regex": "a.*z" + } + ] + } + }, + "destination": { + "service": "carrot", + "service_subset": "kale", + "namespace": "leek", + "partition": "chard", + "prefix_rewrite": "/alternate", + "request_timeout": "99s", + "num_retries": 12345, + "retry_on_connect_failure": true, + "retry_on_status_codes": [ + 401, + 209 + ] + } + }, + { + "match": { + "http": { + "path_prefix": "/foo", + "methods": [ + "GET", + "DELETE" + ], + "query_param": [ + { + "name": "hack1", + "present": true + }, + { + "name": "hack2", + "exact": "1" + }, + { + "name": "hack3", + "regex": "a.*z" + } + ] + } + } + }, + { + "match": { + "http": { + "path_regex": "/foo" + } + } + } + ] + } + `, + camelJSON: ` + { + "Kind": "service-router", + "Name": "main", + "Partition": "pepper", + "Meta" : { + "foo": "bar", + "gir": "zim" + }, + "Routes": [ + { + "Match": { + "HTTP": { + "PathExact": "/foo", + "Header": [ + { + "Name": "debug1", + "Present": true + }, + { + "Name": "debug2", + "Present": false, + "Invert": true + }, + { + "Name": "debug3", + "Exact": "1" + }, + { + "Name": "debug4", + "Prefix": "aaa" + }, + { + "Name": "debug5", + "Suffix": "bbb" + }, + { + "Name": "debug6", + "Regex": "a.*z" + } + ] + } + }, + "Destination": { + "Service": "carrot", + "ServiceSubset": "kale", + "Namespace": "leek", + "Partition": "chard", + "PrefixRewrite": "/alternate", + "RequestTimeout": "99s", + "NumRetries": 12345, + "RetryOnConnectFailure": true, + "RetryOnStatusCodes": [ + 401, + 209 + ] + } + }, + { + "Match": { + "HTTP": { + "PathPrefix": "/foo", + "Methods": [ + "GET", + "DELETE" + ], + "QueryParam": [ + { + "Name": "hack1", + "Present": true + }, + { + "Name": "hack2", + "Exact": "1" + }, + { + "Name": "hack3", + "Regex": "a.*z" + } + ] + } + } + }, + { + "Match": { + "HTTP": { + "PathRegex": "/foo" + } + } + } + ] + } + `, + expect: &api.ServiceRouterConfigEntry{ + Kind: "service-router", + Name: "main", + Partition: "pepper", + Meta: map[string]string{ + "foo": "bar", + "gir": "zim", + }, + Routes: []api.ServiceRoute{ + { + Match: &api.ServiceRouteMatch{ + HTTP: &api.ServiceRouteHTTPMatch{ + PathExact: "/foo", + Header: []api.ServiceRouteHTTPMatchHeader{ + { + Name: "debug1", + Present: true, + }, + { + Name: "debug2", + Present: false, + Invert: true, + }, + { + Name: "debug3", + Exact: "1", + }, + { + Name: "debug4", + Prefix: "aaa", + }, + { + Name: "debug5", + Suffix: "bbb", + }, + { + Name: "debug6", + Regex: "a.*z", + }, + }, + }, + }, + Destination: &api.ServiceRouteDestination{ + Service: "carrot", + ServiceSubset: "kale", + Namespace: "leek", + Partition: "chard", + PrefixRewrite: "/alternate", + RequestTimeout: 99 * time.Second, + NumRetries: 12345, + RetryOnConnectFailure: true, + RetryOnStatusCodes: []uint32{401, 209}, + }, + }, + { + Match: &api.ServiceRouteMatch{ + HTTP: &api.ServiceRouteHTTPMatch{ + PathPrefix: "/foo", + Methods: []string{"GET", "DELETE"}, + QueryParam: []api.ServiceRouteHTTPMatchQueryParam{ + { + Name: "hack1", + Present: true, + }, + { + Name: "hack2", + Exact: "1", + }, + { + Name: "hack3", + Regex: "a.*z", + }, + }, + }, + }, + }, + { + Match: &api.ServiceRouteMatch{ + HTTP: &api.ServiceRouteHTTPMatch{ + PathRegex: "/foo", + }, + }, + }, + }, + }, + }, + { + name: "service-splitter: kitchen sink", + snake: ` + kind = "service-splitter" + name = "main" + partition = "east" + meta { + "foo" = "bar" + "gir" = "zim" + } + splits = [ + { + weight = 97.1 + service_subset = "v1" + }, + { + weight = 2 + service_subset = "v2" + }, + { + weight = 0.9 + service = "other" + namespace = "alt" + partition = "west" + }, + ] + `, + camel: ` + Kind = "service-splitter" + Name = "main" + Partition = "east" + Meta { + "foo" = "bar" + "gir" = "zim" + } + Splits = [ + { + Weight = 97.1 + ServiceSubset = "v1" + }, + { + Weight = 2, + ServiceSubset = "v2" + }, + { + Weight = 0.9 + Service = "other" + Namespace = "alt" + Partition = "west" + }, + ] + `, + snakeJSON: ` + { + "kind": "service-splitter", + "name": "main", + "partition": "east", + "meta" : { + "foo": "bar", + "gir": "zim" + }, + "splits": [ + { + "weight": 97.1, + "service_subset": "v1" + }, + { + "weight": 2, + "service_subset": "v2" + }, + { + "weight": 0.9, + "service": "other", + "namespace": "alt", + "partition": "west" + } + ] + } + `, + camelJSON: ` + { + "Kind": "service-splitter", + "Name": "main", + "Partition": "east", + "Meta" : { + "foo": "bar", + "gir": "zim" + }, + "Splits": [ + { + "Weight": 97.1, + "ServiceSubset": "v1" + }, + { + "Weight": 2, + "ServiceSubset": "v2" + }, + { + "Weight": 0.9, + "Service": "other", + "Namespace": "alt", + "Partition": "west" + } + ] + } + `, + expect: &api.ServiceSplitterConfigEntry{ + Kind: api.ServiceSplitter, + Name: "main", + Partition: "east", + Meta: map[string]string{ + "foo": "bar", + "gir": "zim", + }, + Splits: []api.ServiceSplit{ + { + Weight: 97.1, + ServiceSubset: "v1", + }, + { + Weight: 2, + ServiceSubset: "v2", + }, + { + Weight: 0.9, + Service: "other", + Namespace: "alt", + Partition: "west", + }, + }, + }, + }, + { + name: "service-resolver: subsets with failover", + snake: ` + kind = "service-resolver" + name = "main" + meta { + "foo" = "bar" + "gir" = "zim" + } + default_subset = "v1" + connect_timeout = "15s" + subsets = { + "v1" = { + filter = "Service.Meta.version == v1" + }, + "v2" = { + filter = "Service.Meta.version == v2" + only_passing = true + }, + } + failover = { + "v2" = { + service = "failcopy" + service_subset = "sure" + namespace = "neighbor" + datacenters = ["dc5", "dc14"] + }, + "*" = { + datacenters = ["dc7"] + } + }`, + camel: ` + Kind = "service-resolver" + Name = "main" + Meta { + "foo" = "bar" + "gir" = "zim" + } + DefaultSubset = "v1" + ConnectTimeout = "15s" + Subsets = { + "v1" = { + Filter = "Service.Meta.version == v1" + }, + "v2" = { + Filter = "Service.Meta.version == v2" + OnlyPassing = true + }, + } + Failover = { + "v2" = { + Service = "failcopy" + ServiceSubset = "sure" + Namespace = "neighbor" + Datacenters = ["dc5", "dc14"] + }, + "*" = { + Datacenters = ["dc7"] + } + }`, + snakeJSON: ` + { + "kind": "service-resolver", + "name": "main", + "meta" : { + "foo": "bar", + "gir": "zim" + }, + "default_subset": "v1", + "connect_timeout": "15s", + "subsets": { + "v1": { + "filter": "Service.Meta.version == v1" + }, + "v2": { + "filter": "Service.Meta.version == v2", + "only_passing": true + } + }, + "failover": { + "v2": { + "service": "failcopy", + "service_subset": "sure", + "namespace": "neighbor", + "datacenters": [ + "dc5", + "dc14" + ] + }, + "*": { + "datacenters": [ + "dc7" + ] + } + } + } + `, + camelJSON: ` + { + "Kind": "service-resolver", + "Name": "main", + "Meta" : { + "foo": "bar", + "gir": "zim" + }, + "DefaultSubset": "v1", + "ConnectTimeout": "15s", + "Subsets": { + "v1": { + "Filter": "Service.Meta.version == v1" + }, + "v2": { + "Filter": "Service.Meta.version == v2", + "OnlyPassing": true + } + }, + "Failover": { + "v2": { + "Service": "failcopy", + "ServiceSubset": "sure", + "Namespace": "neighbor", + "Datacenters": [ + "dc5", + "dc14" + ] + }, + "*": { + "Datacenters": [ + "dc7" + ] + } + } + } + `, + expect: &api.ServiceResolverConfigEntry{ + Kind: "service-resolver", + Name: "main", + Meta: map[string]string{ + "foo": "bar", + "gir": "zim", + }, + DefaultSubset: "v1", + ConnectTimeout: 15 * time.Second, + Subsets: map[string]api.ServiceResolverSubset{ + "v1": { + Filter: "Service.Meta.version == v1", + }, + "v2": { + Filter: "Service.Meta.version == v2", + OnlyPassing: true, + }, + }, + Failover: map[string]api.ServiceResolverFailover{ + "v2": { + Service: "failcopy", + ServiceSubset: "sure", + Namespace: "neighbor", + Datacenters: []string{"dc5", "dc14"}, + }, + "*": { + Datacenters: []string{"dc7"}, + }, + }, + }, + }, + { + name: "service-resolver: redirect", + snake: ` + kind = "service-resolver" + name = "main" + partition = "east" + redirect { + service = "other" + service_subset = "backup" + namespace = "alt" + partition = "west" + datacenter = "dc9" + } + `, + camel: ` + Kind = "service-resolver" + Name = "main" + Partition = "east" + Redirect { + Service = "other" + ServiceSubset = "backup" + Namespace = "alt" + Partition = "west" + Datacenter = "dc9" + } + `, + snakeJSON: ` + { + "kind": "service-resolver", + "name": "main", + "partition": "east", + "redirect": { + "service": "other", + "service_subset": "backup", + "namespace": "alt", + "partition": "west", + "datacenter": "dc9" + } + } + `, + camelJSON: ` + { + "Kind": "service-resolver", + "Name": "main", + "Partition": "east", + "Redirect": { + "Service": "other", + "ServiceSubset": "backup", + "Namespace": "alt", + "Partition": "west", + "Datacenter": "dc9" + } + } + `, + expect: &api.ServiceResolverConfigEntry{ + Kind: "service-resolver", + Name: "main", + Partition: "east", + Redirect: &api.ServiceResolverRedirect{ + Service: "other", + ServiceSubset: "backup", + Namespace: "alt", + Partition: "west", + Datacenter: "dc9", + }, + }, + }, + { + name: "service-resolver: default", + snake: ` + kind = "service-resolver" + name = "main" + `, + camel: ` + Kind = "service-resolver" + Name = "main" + `, + snakeJSON: ` + { + "kind": "service-resolver", + "name": "main" + } + `, + camelJSON: ` + { + "Kind": "service-resolver", + "Name": "main" + } + `, + expect: &api.ServiceResolverConfigEntry{ + Kind: "service-resolver", + Name: "main", + }, + }, + { + name: "service-resolver: envoy hash lb kitchen sink", + snake: ` + kind = "service-resolver" + name = "main" + load_balancer = { + policy = "ring_hash" + ring_hash_config = { + minimum_ring_size = 1 + maximum_ring_size = 2 + } + hash_policies = [ + { + field = "cookie" + field_value = "good-cookie" + cookie_config = { + ttl = "1s" + path = "/oven" + } + terminal = true + }, + { + field = "cookie" + field_value = "less-good-cookie" + cookie_config = { + session = true + path = "/toaster" + } + terminal = true + }, + { + field = "header" + field_value = "x-user-id" + }, + { + source_ip = true + } + ] + } + `, + camel: ` + Kind = "service-resolver" + Name = "main" + LoadBalancer = { + Policy = "ring_hash" + RingHashConfig = { + MinimumRingSize = 1 + MaximumRingSize = 2 + } + HashPolicies = [ + { + Field = "cookie" + FieldValue = "good-cookie" + CookieConfig = { + TTL = "1s" + Path = "/oven" + } + Terminal = true + }, + { + Field = "cookie" + FieldValue = "less-good-cookie" + CookieConfig = { + Session = true + Path = "/toaster" + } + Terminal = true + }, + { + Field = "header" + FieldValue = "x-user-id" + }, + { + SourceIP = true + } + ] + } + `, + snakeJSON: ` + { + "kind": "service-resolver", + "name": "main", + "load_balancer": { + "policy": "ring_hash", + "ring_hash_config": { + "minimum_ring_size": 1, + "maximum_ring_size": 2 + }, + "hash_policies": [ + { + "field": "cookie", + "field_value": "good-cookie", + "cookie_config": { + "ttl": "1s", + "path": "/oven" + }, + "terminal": true + }, + { + "field": "cookie", + "field_value": "less-good-cookie", + "cookie_config": { + "session": true, + "path": "/toaster" + }, + "terminal": true + }, + { + "field": "header", + "field_value": "x-user-id" + }, + { + "source_ip": true + } + ] + } + } + `, + camelJSON: ` + { + "Kind": "service-resolver", + "Name": "main", + "LoadBalancer": { + "Policy": "ring_hash", + "RingHashConfig": { + "MinimumRingSize": 1, + "MaximumRingSize": 2 + }, + "HashPolicies": [ + { + "Field": "cookie", + "FieldValue": "good-cookie", + "CookieConfig": { + "TTL": "1s", + "Path": "/oven" + }, + "Terminal": true + }, + { + "Field": "cookie", + "FieldValue": "less-good-cookie", + "CookieConfig": { + "Session": true, + "Path": "/toaster" + }, + "Terminal": true + }, + { + "Field": "header", + "FieldValue": "x-user-id" + }, + { + "SourceIP": true + } + ] + } + } + `, + expect: &api.ServiceResolverConfigEntry{ + Kind: "service-resolver", + Name: "main", + LoadBalancer: &api.LoadBalancer{ + Policy: structs.LBPolicyRingHash, + RingHashConfig: &api.RingHashConfig{ + MinimumRingSize: 1, + MaximumRingSize: 2, + }, + HashPolicies: []api.HashPolicy{ + { + Field: structs.HashPolicyCookie, + FieldValue: "good-cookie", + CookieConfig: &api.CookieConfig{ + TTL: 1 * time.Second, + Path: "/oven", + }, + Terminal: true, + }, + { + Field: structs.HashPolicyCookie, + FieldValue: "less-good-cookie", + CookieConfig: &api.CookieConfig{ + Session: true, + Path: "/toaster", + }, + Terminal: true, + }, + { + Field: structs.HashPolicyHeader, + FieldValue: "x-user-id", + }, + { + SourceIP: true, + }, + }, + }, + }, + }, + { + name: "service-resolver: envoy least request kitchen sink", + snake: ` + kind = "service-resolver" + name = "main" + load_balancer = { + policy = "least_request" + least_request_config = { + choice_count = 2 + } + } + `, + camel: ` + Kind = "service-resolver" + Name = "main" + LoadBalancer = { + Policy = "least_request" + LeastRequestConfig = { + ChoiceCount = 2 + } + } + `, + snakeJSON: ` + { + "kind": "service-resolver", + "name": "main", + "load_balancer": { + "policy": "least_request", + "least_request_config": { + "choice_count": 2 + } + } + } + `, + camelJSON: ` + { + "Kind": "service-resolver", + "Name": "main", + "LoadBalancer": { + "Policy": "least_request", + "LeastRequestConfig": { + "ChoiceCount": 2 + } + } + } + `, + expect: &api.ServiceResolverConfigEntry{ + Kind: "service-resolver", + Name: "main", + LoadBalancer: &api.LoadBalancer{ + Policy: structs.LBPolicyLeastRequest, + LeastRequestConfig: &api.LeastRequestConfig{ + ChoiceCount: 2, + }, + }, + }, + }, + { + name: "expose paths: kitchen sink proxy defaults", + snake: ` + kind = "proxy-defaults" + name = "global" + expose = { + checks = true + paths = [ + { + local_path_port = 8080 + listener_port = 21500 + path = "/healthz" + protocol = "http2" + }, + { + local_path_port = 8000 + listener_port = 21501 + path = "/metrics" + protocol = "http" + } + ] + }`, + camel: ` + Kind = "proxy-defaults" + Name = "global" + Expose = { + Checks = true + Paths = [ + { + LocalPathPort = 8080 + ListenerPort = 21500 + Path = "/healthz" + Protocol = "http2" + }, + { + LocalPathPort = 8000 + ListenerPort = 21501 + Path = "/metrics" + Protocol = "http" + } + ] + }`, + snakeJSON: ` + { + "kind": "proxy-defaults", + "name": "global", + "expose": { + "checks": true, + "paths": [ + { + "local_path_port": 8080, + "listener_port": 21500, + "path": "/healthz", + "protocol": "http2" + }, + { + "local_path_port": 8000, + "listener_port": 21501, + "path": "/metrics", + "protocol": "http" + } + ] + } + } + `, + camelJSON: ` + { + "Kind": "proxy-defaults", + "Name": "global", + "Expose": { + "Checks": true, + "Paths": [ + { + "LocalPathPort": 8080, + "ListenerPort": 21500, + "Path": "/healthz", + "Protocol": "http2" + }, + { + "LocalPathPort": 8000, + "ListenerPort": 21501, + "Path": "/metrics", + "Protocol": "http" + } + ] + } + } + `, + expect: &api.ProxyConfigEntry{ + Kind: "proxy-defaults", + Name: "global", + Expose: api.ExposeConfig{ + Checks: true, + Paths: []api.ExposePath{ + { + ListenerPort: 21500, + Path: "/healthz", + LocalPathPort: 8080, + Protocol: "http2", + }, + { + ListenerPort: 21501, + Path: "/metrics", + LocalPathPort: 8000, + Protocol: "http", + }, + }, + }, + }, + }, + { + name: "expose paths: kitchen sink service defaults", + snake: ` + kind = "service-defaults" + name = "web" + expose = { + checks = true + paths = [ + { + local_path_port = 8080 + listener_port = 21500 + path = "/healthz" + protocol = "http2" + }, + { + local_path_port = 8000 + listener_port = 21501 + path = "/metrics" + protocol = "http" + } + ] + }`, + camel: ` + Kind = "service-defaults" + Name = "web" + Expose = { + Checks = true + Paths = [ + { + LocalPathPort = 8080 + ListenerPort = 21500 + Path = "/healthz" + Protocol = "http2" + }, + { + LocalPathPort = 8000 + ListenerPort = 21501 + Path = "/metrics" + Protocol = "http" + } + ] + }`, + snakeJSON: ` + { + "kind": "service-defaults", + "name": "web", + "expose": { + "checks": true, + "paths": [ + { + "local_path_port": 8080, + "listener_port": 21500, + "path": "/healthz", + "protocol": "http2" + }, + { + "local_path_port": 8000, + "listener_port": 21501, + "path": "/metrics", + "protocol": "http" + } + ] + } + } + `, + camelJSON: ` + { + "Kind": "service-defaults", + "Name": "web", + "Expose": { + "Checks": true, + "Paths": [ + { + "LocalPathPort": 8080, + "ListenerPort": 21500, + "Path": "/healthz", + "Protocol": "http2" + }, + { + "LocalPathPort": 8000, + "ListenerPort": 21501, + "Path": "/metrics", + "Protocol": "http" + } + ] + } + } + `, + expect: &api.ServiceConfigEntry{ + Kind: "service-defaults", + Name: "web", + Expose: api.ExposeConfig{ + Checks: true, + Paths: []api.ExposePath{ + { + ListenerPort: 21500, + Path: "/healthz", + LocalPathPort: 8080, + Protocol: "http2", + }, + { + ListenerPort: 21501, + Path: "/metrics", + LocalPathPort: 8000, + Protocol: "http", + }, + }, + }, + }, + }, + { + // TODO(rb): test SDS stuff here in both places (global/service) + name: "ingress-gateway: kitchen sink", + snake: ` + kind = "ingress-gateway" + name = "ingress-web" + meta { + "foo" = "bar" + "gir" = "zim" + } + tls { + enabled = true + tls_min_version = "TLSv1_1" + tls_max_version = "TLSv1_2" + cipher_suites = [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + ] + } + listeners = [ + { + port = 8080 + protocol = "http" + services = [ + { + name = "web" + hosts = ["test.example.com"] + }, + { + name = "db" + namespace = "foo" + } + ] + } + ] + `, + camel: ` + Kind = "ingress-gateway" + Name = "ingress-web" + Meta { + "foo" = "bar" + "gir" = "zim" + } + Tls { + Enabled = true + TLSMinVersion = "TLSv1_1" + TLSMaxVersion = "TLSv1_2" + CipherSuites = [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + ] + } + Listeners = [ + { + Port = 8080 + Protocol = "http" + Services = [ + { + Name = "web" + Hosts = ["test.example.com"] + }, + { + Name = "db" + Namespace = "foo" + } + ] + } + ] + `, + snakeJSON: ` + { + "kind": "ingress-gateway", + "name": "ingress-web", + "meta" : { + "foo": "bar", + "gir": "zim" + }, + "tls": { + "enabled": true, + "tls_min_version": "TLSv1_1", + "tls_max_version": "TLSv1_2", + "cipher_suites": [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + ] + }, + "listeners": [ + { + "port": 8080, + "protocol": "http", + "services": [ + { + "name": "web", + "hosts": ["test.example.com"] + }, + { + "name": "db", + "namespace": "foo" + } + ] + } + ] + } + `, + camelJSON: ` + { + "Kind": "ingress-gateway", + "Name": "ingress-web", + "Meta" : { + "foo": "bar", + "gir": "zim" + }, + "TLS": { + "Enabled": true, + "TLSMinVersion": "TLSv1_1", + "TLSMaxVersion": "TLSv1_2", + "CipherSuites": [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + ] + }, + "Listeners": [ + { + "Port": 8080, + "Protocol": "http", + "Services": [ + { + "Name": "web", + "Hosts": ["test.example.com"] + }, + { + "Name": "db", + "Namespace": "foo" + } + ] + } + ] + } + `, + expect: &api.IngressGatewayConfigEntry{ + Kind: "ingress-gateway", + Name: "ingress-web", + Meta: map[string]string{ + "foo": "bar", + "gir": "zim", + }, + TLS: api.GatewayTLSConfig{ + Enabled: true, + TLSMinVersion: "TLSv1_1", + TLSMaxVersion: "TLSv1_2", + CipherSuites: []string{ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + }, + }, + Listeners: []api.IngressListener{ + { + Port: 8080, + Protocol: "http", + Services: []api.IngressService{ + { + Name: "web", + Hosts: []string{"test.example.com"}, + }, + { + Name: "db", + Namespace: "foo", + }, + }, + }, + }, + }, + }, + { + name: "service-intentions: kitchen sink", + snake: ` + kind = "service-intentions" + name = "web" + meta { + "foo" = "bar" + "gir" = "zim" + } + sources = [ + { + name = "foo" + action = "deny" + type = "consul" + description = "foo desc" + }, + { + name = "bar" + action = "allow" + description = "bar desc" + }, + { + name = "l7" + permissions = [ + { + action = "deny" + http { + path_exact = "/admin" + header = [ + { + name = "hdr-present" + present = true + }, + { + name = "hdr-exact" + exact = "exact" + }, + { + name = "hdr-prefix" + prefix = "prefix" + }, + { + name = "hdr-suffix" + suffix = "suffix" + }, + { + name = "hdr-regex" + regex = "regex" + }, + { + name = "hdr-absent" + present = true + invert = true + } + ] + } + }, + { + action = "allow" + http { + path_prefix = "/v3/" + } + }, + { + action = "allow" + http { + path_regex = "/v[12]/.*" + methods = ["GET", "POST"] + } + } + ] + } + ] + sources { + name = "*" + action = "deny" + description = "wild desc" + } + `, + camel: ` + Kind = "service-intentions" + Name = "web" + Meta { + "foo" = "bar" + "gir" = "zim" + } + Sources = [ + { + Name = "foo" + Action = "deny" + Type = "consul" + Description = "foo desc" + }, + { + Name = "bar" + Action = "allow" + Description = "bar desc" + }, + { + Name = "l7" + Permissions = [ + { + Action = "deny" + HTTP { + PathExact = "/admin" + Header = [ + { + Name = "hdr-present" + Present = true + }, + { + Name = "hdr-exact" + Exact = "exact" + }, + { + Name = "hdr-prefix" + Prefix = "prefix" + }, + { + Name = "hdr-suffix" + Suffix = "suffix" + }, + { + Name = "hdr-regex" + Regex = "regex" + }, + { + Name = "hdr-absent" + Present = true + Invert = true + } + ] + } + }, + { + Action = "allow" + HTTP { + PathPrefix = "/v3/" + } + }, + { + Action = "allow" + HTTP { + PathRegex = "/v[12]/.*" + Methods = ["GET", "POST"] + } + } + ] + } + ] + Sources { + Name = "*" + Action = "deny" + Description = "wild desc" + } + `, + snakeJSON: ` + { + "kind": "service-intentions", + "name": "web", + "meta": { + "foo": "bar", + "gir": "zim" + }, + "sources": [ + { + "name": "foo", + "action": "deny", + "type": "consul", + "description": "foo desc" + }, + { + "name": "bar", + "action": "allow", + "description": "bar desc" + }, + { + "name": "l7", + "permissions": [ + { + "action": "deny", + "http": { + "path_exact": "/admin", + "header": [ + { + "name": "hdr-present", + "present": true + }, + { + "name": "hdr-exact", + "exact": "exact" + }, + { + "name": "hdr-prefix", + "prefix": "prefix" + }, + { + "name": "hdr-suffix", + "suffix": "suffix" + }, + { + "name": "hdr-regex", + "regex": "regex" + }, + { + "name": "hdr-absent", + "present": true, + "invert": true + } + ] + } + }, + { + "action": "allow", + "http": { + "path_prefix": "/v3/" + } + }, + { + "action": "allow", + "http": { + "path_regex": "/v[12]/.*", + "methods": [ + "GET", + "POST" + ] + } + } + ] + }, + { + "name": "*", + "action": "deny", + "description": "wild desc" + } + ] + } + `, + camelJSON: ` + { + "Kind": "service-intentions", + "Name": "web", + "Meta" : { + "foo": "bar", + "gir": "zim" + }, + "Sources": [ + { + "Name": "foo", + "Action": "deny", + "Type": "consul", + "Description": "foo desc" + }, + { + "Name": "bar", + "Action": "allow", + "Description": "bar desc" + }, + { + "Name": "l7", + "Permissions": [ + { + "Action": "deny", + "HTTP": { + "PathExact": "/admin", + "Header": [ + { + "Name": "hdr-present", + "Present": true + }, + { + "Name": "hdr-exact", + "Exact": "exact" + }, + { + "Name": "hdr-prefix", + "Prefix": "prefix" + }, + { + "Name": "hdr-suffix", + "Suffix": "suffix" + }, + { + "Name": "hdr-regex", + "Regex": "regex" + }, + { + "Name": "hdr-absent", + "Present": true, + "Invert": true + } + ] + } + }, + { + "Action": "allow", + "HTTP": { + "PathPrefix": "/v3/" + } + }, + { + "Action": "allow", + "HTTP": { + "PathRegex": "/v[12]/.*", + "Methods": [ + "GET", + "POST" + ] + } + } + ] + }, + { + "Name": "*", + "Action": "deny", + "Description": "wild desc" + } + ] + } + `, + expect: &api.ServiceIntentionsConfigEntry{ + Kind: "service-intentions", + Name: "web", + Meta: map[string]string{ + "foo": "bar", + "gir": "zim", + }, + Sources: []*api.SourceIntention{ + { + Name: "foo", + Action: "deny", + Type: "consul", + Description: "foo desc", + }, + { + Name: "bar", + Action: "allow", + Description: "bar desc", + }, + { + Name: "l7", + Permissions: []*api.IntentionPermission{ + { + Action: "deny", + HTTP: &api.IntentionHTTPPermission{ + PathExact: "/admin", + Header: []api.IntentionHTTPHeaderPermission{ + { + Name: "hdr-present", + Present: true, + }, + { + Name: "hdr-exact", + Exact: "exact", + }, + { + Name: "hdr-prefix", + Prefix: "prefix", + }, + { + Name: "hdr-suffix", + Suffix: "suffix", + }, + { + Name: "hdr-regex", + Regex: "regex", + }, + { + Name: "hdr-absent", + Present: true, + Invert: true, + }, + }, + }, + }, + { + Action: "allow", + HTTP: &api.IntentionHTTPPermission{ + PathPrefix: "/v3/", + }, + }, + { + Action: "allow", + HTTP: &api.IntentionHTTPPermission{ + PathRegex: "/v[12]/.*", + Methods: []string{"GET", "POST"}, + }, + }, + }, + }, + { + Name: "*", + Action: "deny", + Description: "wild desc", + }, + }, + }, + }, + { + name: "service-intentions: wildcard destination", + snake: ` + kind = "service-intentions" + name = "*" + sources { + name = "foo" + action = "deny" + # should be parsed, but we'll ignore it later + precedence = 6 + } + `, + camel: ` + Kind = "service-intentions" + Name = "*" + Sources { + Name = "foo" + Action = "deny" + # should be parsed, but we'll ignore it later + Precedence = 6 + } + `, + snakeJSON: ` + { + "kind": "service-intentions", + "name": "*", + "sources": [ + { + "name": "foo", + "action": "deny", + "precedence": 6 + } + ] + } + `, + camelJSON: ` + { + "Kind": "service-intentions", + "Name": "*", + "Sources": [ + { + "Name": "foo", + "Action": "deny", + "Precedence": 6 + } + ] + } + `, + expect: &api.ServiceIntentionsConfigEntry{ + Kind: "service-intentions", + Name: "*", + Sources: []*api.SourceIntention{ + { + Name: "foo", + Action: "deny", + Precedence: 6, + }, + }, + }, + }, + { + name: "mesh", + snake: ` + kind = "mesh" + meta { + "foo" = "bar" + "gir" = "zim" + } + transparent_proxy { + mesh_destinations_only = true + } + tls { + incoming { + tls_min_version = "TLSv1_1" + tls_max_version = "TLSv1_2" + cipher_suites = [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + ] + } + outgoing { + tls_min_version = "TLSv1_1" + tls_max_version = "TLSv1_2" + cipher_suites = [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + ] + } + } + `, + camel: ` + Kind = "mesh" + Meta { + "foo" = "bar" + "gir" = "zim" + } + TransparentProxy { + MeshDestinationsOnly = true + } + TLS { + Incoming { + TLSMinVersion = "TLSv1_1" + TLSMaxVersion = "TLSv1_2" + CipherSuites = [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + ] + } + Outgoing { + TLSMinVersion = "TLSv1_1" + TLSMaxVersion = "TLSv1_2" + CipherSuites = [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + ] + } + } + `, + snakeJSON: ` + { + "kind": "mesh", + "meta" : { + "foo": "bar", + "gir": "zim" + }, + "transparent_proxy": { + "mesh_destinations_only": true + }, + "tls": { + "incoming": { + "tls_min_version": "TLSv1_1", + "tls_max_version": "TLSv1_2", + "cipher_suites": [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + ] + }, + "outgoing": { + "tls_min_version": "TLSv1_1", + "tls_max_version": "TLSv1_2", + "cipher_suites": [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + ] + } + } + } + `, + camelJSON: ` + { + "Kind": "mesh", + "Meta" : { + "foo": "bar", + "gir": "zim" + }, + "TransparentProxy": { + "MeshDestinationsOnly": true + }, + "TLS": { + "Incoming": { + "TLSMinVersion": "TLSv1_1", + "TLSMaxVersion": "TLSv1_2", + "CipherSuites": [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + ] + }, + "Outgoing": { + "TLSMinVersion": "TLSv1_1", + "TLSMaxVersion": "TLSv1_2", + "CipherSuites": [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + ] + } + } + } + `, + expect: &api.MeshConfigEntry{ + Meta: map[string]string{ + "foo": "bar", + "gir": "zim", + }, + TransparentProxy: api.TransparentProxyMeshConfig{ + MeshDestinationsOnly: true, + }, + TLS: &api.MeshTLSConfig{ + Incoming: &api.MeshDirectionalTLSConfig{ + TLSMinVersion: "TLSv1_1", + TLSMaxVersion: "TLSv1_2", + CipherSuites: []string{ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + }, + }, + Outgoing: &api.MeshDirectionalTLSConfig{ + TLSMinVersion: "TLSv1_1", + TLSMaxVersion: "TLSv1_2", + CipherSuites: []string{ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + }, + }, + }, + }, + }, + { + name: "exported-services", + snake: ` + kind = "exported-services" + name = "foo" + meta { + "foo" = "bar" + "gir" = "zim" + } + services = [ + { + name = "web" + namespace = "foo" + consumers = [ + { + partition = "bar" + }, + { + partition = "baz" + }, + { + peer_name = "flarm" + } + ] + }, + { + name = "db" + namespace = "bar" + consumers = [ + { + partition = "zoo" + } + ] + } + ] + `, + camel: ` + Kind = "exported-services" + Name = "foo" + Meta { + "foo" = "bar" + "gir" = "zim" + } + Services = [ + { + Name = "web" + Namespace = "foo" + Consumers = [ + { + Partition = "bar" + }, + { + Partition = "baz" + }, + { + PeerName = "flarm" + } + ] + }, + { + Name = "db" + Namespace = "bar" + Consumers = [ + { + Partition = "zoo" + } + ] + } + ] + `, + snakeJSON: ` + { + "kind": "exported-services", + "name": "foo", + "meta": { + "foo": "bar", + "gir": "zim" + }, + "services": [ + { + "name": "web", + "namespace": "foo", + "consumers": [ + { + "partition": "bar" + }, + { + "partition": "baz" + }, + { + "peer_name": "flarm" + } + ] + }, + { + "name": "db", + "namespace": "bar", + "consumers": [ + { + "partition": "zoo" + } + ] + } + ] + } + `, + camelJSON: ` + { + "Kind": "exported-services", + "Name": "foo", + "Meta": { + "foo": "bar", + "gir": "zim" + }, + "Services": [ + { + "Name": "web", + "Namespace": "foo", + "Consumers": [ + { + "Partition": "bar" + }, + { + "Partition": "baz" + }, + { + "PeerName": "flarm" + } + ] + }, + { + "Name": "db", + "Namespace": "bar", + "Consumers": [ + { + "Partition": "zoo" + } + ] + } + ] + } + `, + expect: &api.ExportedServicesConfigEntry{ + Name: "foo", + Meta: map[string]string{ + "foo": "bar", + "gir": "zim", + }, + Services: []api.ExportedService{ + { + Name: "web", + Namespace: "foo", + Consumers: []api.ServiceConsumer{ + { + Partition: "bar", + }, + { + Partition: "baz", + }, + { + PeerName: "flarm", + }, + }, + }, + { + Name: "db", + Namespace: "bar", + Consumers: []api.ServiceConsumer{ + { + Partition: "zoo", + }, + }, + }, + }, + }, + }, + } { + tc := tc + + testbody := func(t *testing.T, body string, expect api.ConfigEntry) { + t.Helper() + got, err := ParseConfigEntry(body) + if tc.expectErr != "" { + require.Nil(t, got) + require.Error(t, err) + requireContainsLower(t, err.Error(), tc.expectErr) + } else { + require.NoError(t, err) + require.Equal(t, expect, got) + } + } + + t.Run(tc.name+" (hcl snake case)", func(t *testing.T) { + testbody(t, tc.snake, tc.expect) + }) + t.Run(tc.name+" (hcl camel case)", func(t *testing.T) { + testbody(t, tc.camel, tc.expect) + }) + if tc.snakeJSON != "" { + t.Run(tc.name+" (json snake case)", func(t *testing.T) { + if tc.expectJSON != nil { + testbody(t, tc.snakeJSON, tc.expectJSON) + } else { + testbody(t, tc.snakeJSON, tc.expect) + } + }) + } + if tc.camelJSON != "" { + t.Run(tc.name+" (json camel case)", func(t *testing.T) { + if tc.expectJSON != nil { + testbody(t, tc.camelJSON, tc.expectJSON) + } else { + testbody(t, tc.camelJSON, tc.expect) + } + }) + } + } +} + +func requireContainsLower(t *testing.T, haystack, needle string) { + t.Helper() + require.Contains(t, strings.ToLower(haystack), strings.ToLower(needle)) +} + +func intPointer(v int) *int { + return &v +} diff --git a/website/content/commands/config/delete.mdx b/website/content/commands/config/delete.mdx index bee9667004..048832dda2 100644 --- a/website/content/commands/config/delete.mdx +++ b/website/content/commands/config/delete.mdx @@ -56,6 +56,8 @@ Usage: `consul config delete [options]` `proxy-defaults` config entry must be `global`, and the name of the `mesh` config entry must be `mesh`. +- `-filename` - Specifies the file describing the config entry to delete. + - `-cas` - Perform a Check-And-Set operation. Specifying this value also requires the -modify-index flag to be set. The default value is false. @@ -67,3 +69,5 @@ Usage: `consul config delete [options]` $ consul config delete -kind service-defaults -name web $ consul config delete -kind service-defaults -name web -cas -modify-index 26 + + $ consul config delete -filename service-defaults-web.hcl