From c911174327557bdab4aa6330749b211615ad72ff Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Tue, 31 Mar 2020 09:59:10 -0700 Subject: [PATCH] Add config entry/state for Ingress Gateways (#7483) * Add Ingress gateway config entry and other relevant structs * Add api package tests for ingress gateways * Embed EnterpriseMeta into ingress service struct * Add namespace fields to api module and test consul config write decoding * Don't require a port for ingress gateways * Add snakeJSON and camelJSON cases in command test * Run Normalize on service's ent metadata Sadly cannot think of a way to test this in OSS. * Every protocol requires at least 1 service * Validate ingress protocols * Update agent/structs/config_entry_gateways.go Co-authored-by: Chris Piraino Co-authored-by: Freddy --- agent/consul/state/config_entry.go | 1 + agent/structs/config_entry.go | 14 ++ agent/structs/config_entry_gateways.go | 161 +++++++++++++++++ agent/structs/config_entry_gateways_test.go | 187 ++++++++++++++++++++ agent/structs/config_entry_test.go | 115 ++++++++++++ agent/structs/connect_proxy_config.go | 2 +- agent/structs/structs.go | 13 +- agent/structs/structs_oss.go | 2 +- agent/structs/structs_test.go | 54 ++++++ agent/structs/testing_catalog.go | 8 + api/config_entry.go | 3 + api/config_entry_gateways.go | 84 +++++++++ api/config_entry_gateways_test.go | 127 +++++++++++++ api/config_entry_test.go | 61 +++++++ command/config/write/config_write_test.go | 108 +++++++++++ 15 files changed, 935 insertions(+), 5 deletions(-) create mode 100644 agent/structs/config_entry_gateways.go create mode 100644 agent/structs/config_entry_gateways_test.go create mode 100644 api/config_entry_gateways.go create mode 100644 api/config_entry_gateways_test.go diff --git a/agent/consul/state/config_entry.go b/agent/consul/state/config_entry.go index 327514f9f0..ddab4b08ed 100644 --- a/agent/consul/state/config_entry.go +++ b/agent/consul/state/config_entry.go @@ -325,6 +325,7 @@ func (s *Store) validateProposedConfigEntryInGraph( case structs.ServiceRouter: case structs.ServiceSplitter: case structs.ServiceResolver: + case structs.IngressGateway: default: return fmt.Errorf("unhandled kind %q during validation of %q", kind, name) } diff --git a/agent/structs/config_entry.go b/agent/structs/config_entry.go index 08a88e4bd1..dea8f5d66c 100644 --- a/agent/structs/config_entry.go +++ b/agent/structs/config_entry.go @@ -20,6 +20,7 @@ const ( ServiceRouter string = "service-router" ServiceSplitter string = "service-splitter" ServiceResolver string = "service-resolver" + IngressGateway string = "ingress-gateway" ProxyConfigGlobal string = "global" @@ -376,6 +377,15 @@ func ConfigEntryDecodeRulesForKind(kind string) (skipWhenPatching []string, tran "only_passing": "onlypassing", "service_subset": "servicesubset", }, nil + case IngressGateway: + return []string{ + "listeners", + "Listeners", + "listeners.services", + "Listeners.Services", + }, map[string]string{ + "service_subset": "servicesubset", + }, nil default: return nil, nil, fmt.Errorf("kind %q should be explicitly handled here", kind) } @@ -466,6 +476,8 @@ func MakeConfigEntry(kind, name string) (ConfigEntry, error) { return &ServiceSplitterConfigEntry{Name: name}, nil case ServiceResolver: return &ServiceResolverConfigEntry{Name: name}, nil + case IngressGateway: + return &IngressGatewayConfigEntry{Name: name}, nil default: return nil, fmt.Errorf("invalid config entry kind: %s", kind) } @@ -477,6 +489,8 @@ func ValidateConfigEntryKind(kind string) bool { return true case ServiceRouter, ServiceSplitter, ServiceResolver: return true + case IngressGateway: + return true default: return false } diff --git a/agent/structs/config_entry_gateways.go b/agent/structs/config_entry_gateways.go new file mode 100644 index 0000000000..ae9e21f7b5 --- /dev/null +++ b/agent/structs/config_entry_gateways.go @@ -0,0 +1,161 @@ +package structs + +import ( + "fmt" + "strings" + + "github.com/hashicorp/consul/acl" +) + +// IngressGatewayConfigEntry manages the configuration for an ingress service +// with the given name. +type IngressGatewayConfigEntry struct { + // Kind of the config entry. This will be set to structs.IngressGateway. + Kind string + + // Name is used to match the config entry with its associated ingress gateway + // service. This should match the name provided in the service definition. + Name string + + // Listeners declares what ports the ingress gateway should listen on, and + // what services to associated to those ports. + Listeners []IngressListener + + EnterpriseMeta `hcl:",squash" mapstructure:",squash"` + RaftIndex +} + +type IngressListener struct { + // Port declares the port on which the ingress gateway should listen for traffic. + Port int + + // Protocol declares what type of traffic this listener is expected to + // receive. Depending on the protocol, a listener might support multiplexing + // services over a single port, or additional discovery chain features. The + // current supported values are: (tcp | http). + Protocol string + + // Services declares the set of services to which the listener forwards + // traffic. + // + // For "tcp" protocol listeners, only a single service is allowed. + // For "http" listeners, multiple services can be declared. + Services []IngressService +} + +type IngressService struct { + // Name declares the service to which traffic should be forwarded. + // + // This can either be a specific service, or the wildcard specifier, + // "*". If the wildcard specifier is provided, the listener must be of "http" + // protocol and means that the listener will forward traffic to all services. + Name string + + // ServiceSubset declares the specific service subset to which traffic should + // be sent. This must match an existing service subset declared in a + // service-resolver config entry. + ServiceSubset string + + EnterpriseMeta `hcl:",squash" mapstructure:",squash"` +} + +func (e *IngressGatewayConfigEntry) GetKind() string { + return IngressGateway +} + +func (e *IngressGatewayConfigEntry) GetName() string { + if e == nil { + return "" + } + + return e.Name +} + +func (e *IngressGatewayConfigEntry) Normalize() error { + if e == nil { + return fmt.Errorf("config entry is nil") + } + + e.Kind = IngressGateway + for _, listener := range e.Listeners { + listener.Protocol = strings.ToLower(listener.Protocol) + for i := range listener.Services { + listener.Services[i].EnterpriseMeta.Normalize() + } + } + + e.EnterpriseMeta.Normalize() + + return nil +} + +func (e *IngressGatewayConfigEntry) Validate() error { + validProtocols := map[string]bool{ + "http": true, + "tcp": true, + } + declaredPorts := make(map[int]bool) + + for _, listener := range e.Listeners { + if _, ok := declaredPorts[listener.Port]; ok { + return fmt.Errorf("port %d declared on two listeners", listener.Port) + } + declaredPorts[listener.Port] = true + + if _, ok := validProtocols[listener.Protocol]; !ok { + return fmt.Errorf("Protocol must be either 'http' or 'tcp', '%s' is an unsupported protocol.", listener.Protocol) + } + + for _, s := range listener.Services { + if s.Name == WildcardSpecifier && listener.Protocol != "http" { + return fmt.Errorf("Wildcard service name is only valid for protocol = 'http' (listener on port %d)", listener.Port) + } + if s.Name == "" { + return fmt.Errorf("Service name cannot be blank (listener on port %d)", listener.Port) + } + if s.NamespaceOrDefault() == WildcardSpecifier { + return fmt.Errorf("Wildcard namespace is not supported for ingress services (listener on port %d)", listener.Port) + } + } + + if len(listener.Services) == 0 { + return fmt.Errorf("No service declared for listener with port %d", listener.Port) + } + + // Validate that http features aren't being used with tcp or another non-supported protocol. + if listener.Protocol != "http" && len(listener.Services) > 1 { + return fmt.Errorf("Multiple services per listener are only supported for protocol = 'http' (listener on port %d)", + listener.Port) + } + } + + return nil +} + +func (e *IngressGatewayConfigEntry) CanRead(authz acl.Authorizer) bool { + var authzContext acl.AuthorizerContext + e.FillAuthzContext(&authzContext) + return authz.OperatorRead(&authzContext) == acl.Allow +} + +func (e *IngressGatewayConfigEntry) CanWrite(authz acl.Authorizer) bool { + var authzContext acl.AuthorizerContext + e.FillAuthzContext(&authzContext) + return authz.OperatorWrite(&authzContext) == acl.Allow +} + +func (e *IngressGatewayConfigEntry) GetRaftIndex() *RaftIndex { + if e == nil { + return &RaftIndex{} + } + + return &e.RaftIndex +} + +func (e *IngressGatewayConfigEntry) GetEnterpriseMeta() *EnterpriseMeta { + if e == nil { + return nil + } + + return &e.EnterpriseMeta +} diff --git a/agent/structs/config_entry_gateways_test.go b/agent/structs/config_entry_gateways_test.go new file mode 100644 index 0000000000..f9065e857c --- /dev/null +++ b/agent/structs/config_entry_gateways_test.go @@ -0,0 +1,187 @@ +package structs + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIngressConfigEntry_Validate(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + entry IngressGatewayConfigEntry + expectErr string + }{ + { + name: "port conflict", + entry: IngressGatewayConfigEntry{ + Kind: "ingress-gateway", + Name: "ingress-web", + Listeners: []IngressListener{ + { + Port: 1111, + Protocol: "tcp", + Services: []IngressService{ + { + Name: "mysql", + }, + }, + }, + { + Port: 1111, + Protocol: "tcp", + Services: []IngressService{ + { + Name: "postgres", + }, + }, + }, + }, + }, + expectErr: "port 1111 declared on two listeners", + }, + { + name: "http features: wildcard", + entry: IngressGatewayConfigEntry{ + Kind: "ingress-gateway", + Name: "ingress-web", + Listeners: []IngressListener{ + { + Port: 1111, + Protocol: "http", + Services: []IngressService{ + { + Name: "*", + }, + }, + }, + }, + }, + }, + { + name: "http features: wildcard service on invalid protocol", + entry: IngressGatewayConfigEntry{ + Kind: "ingress-gateway", + Name: "ingress-web", + Listeners: []IngressListener{ + { + Port: 1111, + Protocol: "tcp", + Services: []IngressService{ + { + Name: "*", + }, + }, + }, + }, + }, + expectErr: "Wildcard service name is only valid for protocol", + }, + { + name: "http features: multiple services", + entry: IngressGatewayConfigEntry{ + Kind: "ingress-gateway", + Name: "ingress-web", + Listeners: []IngressListener{ + { + Port: 1111, + Protocol: "tcp", + Services: []IngressService{ + { + Name: "db1", + }, + { + Name: "db2", + }, + }, + }, + }, + }, + expectErr: "multiple services per listener are only supported for protocol", + }, + { + name: "tcp listener requires a defined service", + entry: IngressGatewayConfigEntry{ + Kind: "ingress-gateway", + Name: "ingress-web", + Listeners: []IngressListener{ + { + Port: 1111, + Protocol: "tcp", + Services: []IngressService{}, + }, + }, + }, + expectErr: "no service declared for listener with port 1111", + }, + { + name: "http listener requires a defined service", + entry: IngressGatewayConfigEntry{ + Kind: "ingress-gateway", + Name: "ingress-web", + Listeners: []IngressListener{ + { + Port: 1111, + Protocol: "http", + Services: []IngressService{}, + }, + }, + }, + expectErr: "no service declared for listener with port 1111", + }, + { + name: "empty service name not supported", + entry: IngressGatewayConfigEntry{ + Kind: "ingress-gateway", + Name: "ingress-web", + Listeners: []IngressListener{ + { + Port: 1111, + Protocol: "tcp", + Services: []IngressService{ + {}, + }, + }, + }, + }, + expectErr: "Service name cannot be blank", + }, + { + name: "protocol validation", + entry: IngressGatewayConfigEntry{ + Kind: "ingress-gateway", + Name: "ingress-web", + Listeners: []IngressListener{ + { + Port: 1111, + Protocol: "asdf", + Services: []IngressService{ + { + Name: "db", + }, + }, + }, + }, + }, + expectErr: "Protocol must be either 'http' or 'tcp', 'asdf' is an unsupported protocol.", + }, + } + + for _, test := range cases { + // We explicitly copy the variable for the range statement so that can run + // tests in parallel. + tc := test + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := tc.entry.Validate() + if tc.expectErr != "" { + require.Error(t, err) + requireContainsLower(t, err.Error(), tc.expectErr) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/agent/structs/config_entry_test.go b/agent/structs/config_entry_test.go index f654fa4480..4de8288e3d 100644 --- a/agent/structs/config_entry_test.go +++ b/agent/structs/config_entry_test.go @@ -532,6 +532,121 @@ func TestDecodeConfigEntry(t *testing.T) { Name: "main", }, }, + { + name: "ingress-gateway: kitchen sink", + snake: ` + kind = "ingress-gateway" + name = "ingress-web" + + listeners = [ + { + port = 8080 + protocol = "http" + services = [ + { + name = "web" + }, + { + name = "db" + } + ] + }, + { + port = 9999 + protocol = "tcp" + services = [ + { + name = "mysql" + } + ] + }, + { + port = 2234 + protocol = "tcp" + services = [ + { + name = "postgres" + service_subset = "v1" + } + ] + } + ] + `, + camel: ` + Kind = "ingress-gateway" + Name = "ingress-web" + Listeners = [ + { + Port = 8080 + Protocol = "http" + Services = [ + { + Name = "web" + }, + { + Name = "db" + } + ] + }, + { + Port = 9999 + Protocol = "tcp" + Services = [ + { + Name = "mysql" + } + ] + }, + { + Port = 2234 + Protocol = "tcp" + Services = [ + { + Name = "postgres" + ServiceSubset = "v1" + } + ] + } + ] + `, + expect: &IngressGatewayConfigEntry{ + Kind: "ingress-gateway", + Name: "ingress-web", + Listeners: []IngressListener{ + IngressListener{ + Port: 8080, + Protocol: "http", + Services: []IngressService{ + IngressService{ + Name: "web", + }, + IngressService{ + Name: "db", + }, + }, + }, + IngressListener{ + Port: 9999, + Protocol: "tcp", + Services: []IngressService{ + IngressService{ + Name: "mysql", + }, + }, + }, + IngressListener{ + Port: 2234, + Protocol: "tcp", + Services: []IngressService{ + IngressService{ + Name: "postgres", + ServiceSubset: "v1", + }, + }, + }, + }, + }, + }, } { tc := tc diff --git a/agent/structs/connect_proxy_config.go b/agent/structs/connect_proxy_config.go index 3ead018d1a..8344225f43 100644 --- a/agent/structs/connect_proxy_config.go +++ b/agent/structs/connect_proxy_config.go @@ -358,7 +358,7 @@ func (u *Upstream) Identifier() string { name := u.DestinationName typ := u.DestinationType - if typ != UpstreamDestTypePreparedQuery && u.DestinationNamespace != "" && u.DestinationNamespace != "default" { + if typ != UpstreamDestTypePreparedQuery && u.DestinationNamespace != "" && u.DestinationNamespace != IntentionDefaultNamespace { name = u.DestinationNamespace + "/" + u.DestinationName } if u.Datacenter != "" { diff --git a/agent/structs/structs.go b/agent/structs/structs.go index 18c6681f66..0f4708e278 100644 --- a/agent/structs/structs.go +++ b/agent/structs/structs.go @@ -894,6 +894,11 @@ const ( // ServiceKindTerminatingGateway is a Terminating Gateway for the Connect // feature. This service will proxy connections to services outside the mesh. ServiceKindTerminatingGateway ServiceKind = "terminating-gateway" + + // ServiceKindIngressGateway is an Ingress Gateway for the Connect feature. + // This service allows external traffic to enter the mesh based on + // centralized configuration. + ServiceKindIngressGateway ServiceKind = "ingress-gateway" ) // Type to hold a address and port of a service @@ -1039,7 +1044,9 @@ func (s *NodeService) IsSidecarProxy() bool { } func (s *NodeService) IsGateway() bool { - return s.Kind == ServiceKindMeshGateway || s.Kind == ServiceKindTerminatingGateway + return s.Kind == ServiceKindMeshGateway || + s.Kind == ServiceKindTerminatingGateway || + s.Kind == ServiceKindIngressGateway } // Validate validates the node service configuration. @@ -1138,8 +1145,8 @@ func (s *NodeService) Validate() error { // Gateway validation if s.IsGateway() { - // Gateways must have a port - if s.Port == 0 { + // Non-ingress gateways must have a port + if s.Port == 0 && s.Kind != ServiceKindIngressGateway { result = multierror.Append(result, fmt.Errorf("Port must be non-zero for a %s", s.Kind)) } diff --git a/agent/structs/structs_oss.go b/agent/structs/structs_oss.go index 8534301103..0cad0f3cfb 100644 --- a/agent/structs/structs_oss.go +++ b/agent/structs/structs_oss.go @@ -43,7 +43,7 @@ func (m *EnterpriseMeta) LessThan(_ *EnterpriseMeta) bool { } func (m *EnterpriseMeta) NamespaceOrDefault() string { - return "default" + return IntentionDefaultNamespace } func EnterpriseMetaInitializer(_ string) EnterpriseMeta { diff --git a/agent/structs/structs_test.go b/agent/structs/structs_test.go index 2c51ca6d02..96cb8081b9 100644 --- a/agent/structs/structs_test.go +++ b/agent/structs/structs_test.go @@ -420,6 +420,7 @@ func TestStructs_NodeService_ValidateTerminatingGateway(t *testing.T) { Modify func(*NodeService) Err string } + cases := map[string]testCase{ "valid": testCase{ func(x *NodeService) {}, @@ -467,6 +468,59 @@ func TestStructs_NodeService_ValidateTerminatingGateway(t *testing.T) { } } +func TestStructs_NodeService_ValidateIngressGateway(t *testing.T) { + type testCase struct { + Modify func(*NodeService) + Err string + } + + cases := map[string]testCase{ + "valid": testCase{ + func(x *NodeService) {}, + "", + }, + "sidecar-service": testCase{ + func(x *NodeService) { x.Connect.SidecarService = &ServiceDefinition{} }, + "cannot have a sidecar service", + }, + "proxy-destination-name": testCase{ + func(x *NodeService) { x.Proxy.DestinationServiceName = "foo" }, + "Proxy.DestinationServiceName configuration is invalid", + }, + "proxy-destination-id": testCase{ + func(x *NodeService) { x.Proxy.DestinationServiceID = "foo" }, + "Proxy.DestinationServiceID configuration is invalid", + }, + "proxy-local-address": testCase{ + func(x *NodeService) { x.Proxy.LocalServiceAddress = "127.0.0.1" }, + "Proxy.LocalServiceAddress configuration is invalid", + }, + "proxy-local-port": testCase{ + func(x *NodeService) { x.Proxy.LocalServicePort = 36 }, + "Proxy.LocalServicePort configuration is invalid", + }, + "proxy-upstreams": testCase{ + func(x *NodeService) { x.Proxy.Upstreams = []Upstream{Upstream{}} }, + "Proxy.Upstreams configuration is invalid", + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + ns := TestNodeServiceIngressGateway(t, "10.0.0.5") + tc.Modify(ns) + + err := ns.Validate() + if tc.Err == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err)) + } + }) + } +} + func TestStructs_NodeService_ValidateExposeConfig(t *testing.T) { type testCase struct { Modify func(*NodeService) diff --git a/agent/structs/testing_catalog.go b/agent/structs/testing_catalog.go index dfc526ee49..772b1ae4eb 100644 --- a/agent/structs/testing_catalog.go +++ b/agent/structs/testing_catalog.go @@ -112,6 +112,14 @@ func TestNodeServiceMeshGatewayWithAddrs(t testing.T, address string, port int, } } +func TestNodeServiceIngressGateway(t testing.T, address string) *NodeService { + return &NodeService{ + Kind: ServiceKindIngressGateway, + Service: "ingress-gateway", + Address: address, + } +} + // TestNodeServiceSidecar returns a *NodeService representing a service // registration with a nested Sidecar registration. func TestNodeServiceSidecar(t testing.T) *NodeService { diff --git a/api/config_entry.go b/api/config_entry.go index ae0d42797e..17e75000fa 100644 --- a/api/config_entry.go +++ b/api/config_entry.go @@ -17,6 +17,7 @@ const ( ServiceRouter string = "service-router" ServiceSplitter string = "service-splitter" ServiceResolver string = "service-resolver" + IngressGateway string = "ingress-gateway" ProxyConfigGlobal string = "global" ) @@ -157,6 +158,8 @@ func makeConfigEntry(kind, name string) (ConfigEntry, error) { return &ServiceSplitterConfigEntry{Kind: kind, Name: name}, nil case ServiceResolver: return &ServiceResolverConfigEntry{Kind: kind, Name: name}, nil + case IngressGateway: + return &IngressGatewayConfigEntry{Kind: kind, Name: name}, nil default: return nil, fmt.Errorf("invalid config entry kind: %s", kind) } diff --git a/api/config_entry_gateways.go b/api/config_entry_gateways.go new file mode 100644 index 0000000000..1812032d2c --- /dev/null +++ b/api/config_entry_gateways.go @@ -0,0 +1,84 @@ +package api + +// IngressGatewayConfigEntry manages the configuration for an ingress service +// with the given name. +type IngressGatewayConfigEntry struct { + // Kind of the config entry. This should be set to api.IngressGateway. + Kind string + + // Name is used to match the config entry with its associated ingress gateway + // service. This should match the name provided in the service definition. + Name string + + // Namespace is the namespace the IngressGateway is associated with + // Namespacing is a Consul Enterprise feature. + Namespace string `json:",omitempty"` + + // Listeners declares what ports the ingress gateway should listen on, and + // what services to associated to those ports. + Listeners []IngressListener + + // CreateIndex is the Raft index this entry was created at. This is a + // read-only field. + CreateIndex uint64 + + // ModifyIndex is used for the Check-And-Set operations and can also be fed + // back into the WaitIndex of the QueryOptions in order to perform blocking + // queries. + ModifyIndex uint64 +} + +// IngressListener manages the configuration for a listener on a specific port. +type IngressListener struct { + // Port declares the port on which the ingress gateway should listen for traffic. + Port int + + // Protocol declares what type of traffic this listener is expected to + // receive. Depending on the protocol, a listener might support multiplexing + // services over a single port, or additional discovery chain features. The + // current supported values are: (tcp | http). + Protocol string + + // Services declares the set of services to which the listener forwards + // traffic. + // + // For "tcp" protocol listeners, only a single service is allowed. + // For "http" listeners, multiple services can be declared. + Services []IngressService +} + +// IngressService manages configuration for services that are exposed to +// ingress traffic. +type IngressService struct { + // Name declares the service to which traffic should be forwarded. + // + // This can either be a specific service instance, or the wildcard specifier, + // "*". If the wildcard specifier is provided, the listener must be of "http" + // protocol and means that the listener will forward traffic to all services. + Name string + + // Namespace is the namespace where the service is located. + // Namespacing is a Consul Enterprise feature. + Namespace string `json:",omitempty"` + + // ServiceSubset declares the specific service subset to which traffic should + // be sent. This must match an existing service subset declared in a + // service-resolver config entry. + ServiceSubset string +} + +func (i *IngressGatewayConfigEntry) GetKind() string { + return i.Kind +} + +func (i *IngressGatewayConfigEntry) GetName() string { + return i.Name +} + +func (i *IngressGatewayConfigEntry) GetCreateIndex() uint64 { + return i.CreateIndex +} + +func (i *IngressGatewayConfigEntry) GetModifyIndex() uint64 { + return i.ModifyIndex +} diff --git a/api/config_entry_gateways_test.go b/api/config_entry_gateways_test.go new file mode 100644 index 0000000000..71caaf92dc --- /dev/null +++ b/api/config_entry_gateways_test.go @@ -0,0 +1,127 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAPI_ConfigEntries_IngressGateway(t *testing.T) { + t.Parallel() + c, s := makeClient(t) + defer s.Stop() + + config_entries := c.ConfigEntries() + + ingress1 := &IngressGatewayConfigEntry{ + Kind: IngressGateway, + Name: "foo", + } + + ingress2 := &IngressGatewayConfigEntry{ + Kind: IngressGateway, + Name: "bar", + } + + // set it + _, wm, err := config_entries.Set(ingress1, nil) + require.NoError(t, err) + require.NotNil(t, wm) + require.NotEqual(t, 0, wm.RequestTime) + + // also set the second one + _, wm, err = config_entries.Set(ingress2, nil) + require.NoError(t, err) + require.NotNil(t, wm) + require.NotEqual(t, 0, wm.RequestTime) + + // get it + entry, qm, err := config_entries.Get(IngressGateway, "foo", nil) + require.NoError(t, err) + require.NotNil(t, qm) + require.NotEqual(t, 0, qm.RequestTime) + + // verify it + readIngress, ok := entry.(*IngressGatewayConfigEntry) + require.True(t, ok) + require.Equal(t, ingress1.Kind, readIngress.Kind) + require.Equal(t, ingress1.Name, readIngress.Name) + + // update it + ingress1.Listeners = []IngressListener{ + { + Port: 2222, + Protocol: "tcp", + Services: []IngressService{ + { + Name: "asdf", + }, + }, + }, + } + + // CAS fail + written, _, err := config_entries.CAS(ingress1, 0, nil) + require.NoError(t, err) + require.False(t, written) + + // CAS success + written, wm, err = config_entries.CAS(ingress1, readIngress.ModifyIndex, nil) + require.NoError(t, err) + require.NotNil(t, wm) + require.NotEqual(t, 0, wm.RequestTime) + require.True(t, written) + + // update no cas + ingress2.Listeners = []IngressListener{ + { + Port: 3333, + Protocol: "http", + Services: []IngressService{ + { + Name: "qwer", + }, + }, + }, + } + _, wm, err = config_entries.Set(ingress2, nil) + require.NoError(t, err) + require.NotNil(t, wm) + require.NotEqual(t, 0, wm.RequestTime) + + // list them + entries, qm, err := config_entries.List(IngressGateway, nil) + require.NoError(t, err) + require.NotNil(t, qm) + require.NotEqual(t, 0, qm.RequestTime) + require.Len(t, entries, 2) + + for _, entry = range entries { + switch entry.GetName() { + case "foo": + // this also verifies that the update value was persisted and + // the updated values are seen + readIngress, ok = entry.(*IngressGatewayConfigEntry) + require.True(t, ok) + require.Equal(t, ingress1.Kind, readIngress.Kind) + require.Equal(t, ingress1.Name, readIngress.Name) + require.Equal(t, ingress1.Listeners, readIngress.Listeners) + case "bar": + readIngress, ok = entry.(*IngressGatewayConfigEntry) + require.True(t, ok) + require.Equal(t, ingress2.Kind, readIngress.Kind) + require.Equal(t, ingress2.Name, readIngress.Name) + require.Equal(t, ingress2.Listeners, readIngress.Listeners) + } + } + + // delete it + wm, err = config_entries.Delete(IngressGateway, "foo", nil) + require.NoError(t, err) + require.NotNil(t, wm) + require.NotEqual(t, 0, wm.RequestTime) + + // verify deletion + entry, qm, err = config_entries.Get(IngressGateway, "foo", nil) + require.Error(t, err) +} diff --git a/api/config_entry_test.go b/api/config_entry_test.go index 893d024a2b..946ed3feb5 100644 --- a/api/config_entry_test.go +++ b/api/config_entry_test.go @@ -613,6 +613,67 @@ func TestDecodeConfigEntry(t *testing.T) { Name: "main", }, }, + { + name: "ingress-gateway", + body: ` + { + "Kind": "ingress-gateway", + "Name": "ingress-web", + "Listeners": [ + { + "Port": 8080, + "Protocol": "http", + "Services": [ + { + "Name": "web", + "Namespace": "foo" + }, + { + "Name": "db" + } + ] + }, + { + "Port": 9999, + "Protocol": "tcp", + "Services": [ + { + "Name": "mysql" + } + ] + } + ] + } + `, + expect: &IngressGatewayConfigEntry{ + Kind: "ingress-gateway", + Name: "ingress-web", + Listeners: []IngressListener{ + IngressListener{ + Port: 8080, + Protocol: "http", + Services: []IngressService{ + IngressService{ + Name: "web", + Namespace: "foo", + }, + IngressService{ + Name: "db", + }, + }, + }, + IngressListener{ + Port: 9999, + Protocol: "tcp", + Services: []IngressService{ + IngressService{ + Name: "mysql", + }, + }, + }, + }, + }, + }, } { tc := tc diff --git a/command/config/write/config_write_test.go b/command/config/write/config_write_test.go index 1e1bc2e617..5ae4413610 100644 --- a/command/config/write/config_write_test.go +++ b/command/config/write/config_write_test.go @@ -1122,6 +1122,114 @@ func TestParseConfigEntry(t *testing.T) { }, }, }, + { + name: "ingress-gateway: kitchen sink", + snake: ` + kind = "ingress-gateway" + name = "ingress-web" + + listeners = [ + { + port = 8080 + protocol = "http" + services = [ + { + name = "web" + service_subset = "v1" + }, + { + name = "db" + namespace = "foo" + } + ] + } + ] + `, + camel: ` + Kind = "ingress-gateway" + Name = "ingress-web" + Listeners = [ + { + Port = 8080 + Protocol = "http" + Services = [ + { + Name = "web" + ServiceSubset = "v1" + }, + { + Name = "db" + Namespace = "foo" + } + ] + } + ] + `, + snakeJSON: ` + { + "kind": "ingress-gateway", + "name": "ingress-web", + "listeners": [ + { + "port": 8080, + "protocol": "http", + "services": [ + { + "name": "web", + "service_subset": "v1" + }, + { + "name": "db", + "namespace": "foo" + } + ] + } + ] + } + `, + camelJSON: ` + { + "Kind": "ingress-gateway", + "Name": "ingress-web", + "Listeners": [ + { + "Port": 8080, + "Protocol": "http", + "Services": [ + { + "Name": "web", + "ServiceSubset": "v1" + }, + { + "Name": "db", + "Namespace": "foo" + } + ] + } + ] + } + `, + expect: &api.IngressGatewayConfigEntry{ + Kind: "ingress-gateway", + Name: "ingress-web", + Listeners: []api.IngressListener{ + { + Port: 8080, + Protocol: "http", + Services: []api.IngressService{ + { + Name: "web", + ServiceSubset: "v1", + }, + { + Name: "db", + Namespace: "foo", + }, + }, + }, + }, + }, + }, } { tc := tc