From 7c0d9996d4915a853e8a65193c22035fd6587954 Mon Sep 17 00:00:00 2001 From: Horacio Monsalvo Date: Fri, 31 Mar 2023 18:24:26 -0300 Subject: [PATCH 1/5] add sds to tls struct --- api/consul.go | 26 ++++++++-- command/agent/job_endpoint.go | 12 +++++ jobspec/parse_service.go | 49 +++++++++++++++++++ nomad/consul.go | 27 +++++++++- nomad/structs/diff.go | 5 ++ nomad/structs/services.go | 31 +++++++++++- .../docs/job-specification/gateway.mdx | 6 +++ 7 files changed, 150 insertions(+), 6 deletions(-) diff --git a/api/consul.go b/api/consul.go index 5c8677580e7..4f1dc94868c 100644 --- a/api/consul.go +++ b/api/consul.go @@ -376,12 +376,29 @@ func (p *ConsulGatewayProxy) Copy() *ConsulGatewayProxy { } } +type ConsulGatewayTLSSDSConfig struct { + ClusterName string `hcl:"cluster_name,optional" mapstructure:"cluster_name"` + CertResource string `hcl:"cert_resource,optional" mapstructure:"cert_resource"` +} + +func (c *ConsulGatewayTLSSDSConfig) Copy() *ConsulGatewayTLSSDSConfig { + if c == nil { + return nil + } + + return &ConsulGatewayTLSSDSConfig{ + ClusterName: c.ClusterName, + CertResource: c.CertResource, + } +} + // ConsulGatewayTLSConfig is used to configure TLS for a gateway. type ConsulGatewayTLSConfig struct { - Enabled bool `hcl:"enabled,optional"` - TLSMinVersion string `hcl:"tls_min_version,optional" mapstructure:"tls_min_version"` - TLSMaxVersion string `hcl:"tls_max_version,optional" mapstructure:"tls_max_version"` - CipherSuites []string `hcl:"cipher_suites,optional" mapstructure:"cipher_suites"` + Enabled bool `hcl:"enabled,optional"` + TLSMinVersion string `hcl:"tls_min_version,optional" mapstructure:"tls_min_version"` + TLSMaxVersion string `hcl:"tls_max_version,optional" mapstructure:"tls_max_version"` + CipherSuites []string `hcl:"cipher_suites,optional" mapstructure:"cipher_suites"` + SDS *ConsulGatewayTLSSDSConfig `hcl:"sds_config,block" mapstructure:"sds_config"` } func (tc *ConsulGatewayTLSConfig) Canonicalize() { @@ -396,6 +413,7 @@ func (tc *ConsulGatewayTLSConfig) Copy() *ConsulGatewayTLSConfig { Enabled: tc.Enabled, TLSMinVersion: tc.TLSMinVersion, TLSMaxVersion: tc.TLSMaxVersion, + SDS: tc.SDS.Copy(), } if len(tc.CipherSuites) != 0 { cipherSuites := make([]string, len(tc.CipherSuites)) diff --git a/command/agent/job_endpoint.go b/command/agent/job_endpoint.go index e998b9c0302..dbc37b6e371 100644 --- a/command/agent/job_endpoint.go +++ b/command/agent/job_endpoint.go @@ -1533,6 +1533,17 @@ func apiConnectIngressGatewayToStructs(in *api.ConsulIngressConfigEntry) *struct } } +func apiConnectGatewayTLSSDSConfig(in *api.ConsulGatewayTLSSDSConfig) *structs.ConsulGatewayTLSSDSConfig { + if in == nil { + return nil + } + + return &structs.ConsulGatewayTLSSDSConfig{ + ClusterName: in.ClusterName, + CertResource: in.CertResource, + } +} + func apiConnectGatewayTLSConfig(in *api.ConsulGatewayTLSConfig) *structs.ConsulGatewayTLSConfig { if in == nil { return nil @@ -1543,6 +1554,7 @@ func apiConnectGatewayTLSConfig(in *api.ConsulGatewayTLSConfig) *structs.ConsulG TLSMinVersion: in.TLSMinVersion, TLSMaxVersion: in.TLSMaxVersion, CipherSuites: slices.Clone(in.CipherSuites), + SDS: apiConnectGatewayTLSSDSConfig(in.SDS), } } diff --git a/jobspec/parse_service.go b/jobspec/parse_service.go index 360382185a6..2bb11f68a1c 100644 --- a/jobspec/parse_service.go +++ b/jobspec/parse_service.go @@ -538,12 +538,43 @@ func parseConsulIngressListener(o *ast.ObjectItem) (*api.ConsulIngressListener, return &listener, nil } +func parseConsulGatewayTLSSDS(o *ast.ObjectItem) (*api.ConsulGatewayTLSSDSConfig, error) { + valid := []string{ + "cluster_name", + "cert_resource", + } + + if err := checkHCLKeys(o.Val, valid); err != nil { + return nil, multierror.Prefix(err, "sds ->") + } + + var sds api.ConsulGatewayTLSSDSConfig + var m map[string]interface{} + if err := hcl.DecodeObject(&m, o.Val); err != nil { + return nil, err + } + + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &sds, + }) + if err != nil { + return nil, err + } + + if err := dec.Decode(m); err != nil { + return nil, err + } + + return &sds, nil +} + func parseConsulGatewayTLS(o *ast.ObjectItem) (*api.ConsulGatewayTLSConfig, error) { valid := []string{ "enabled", "tls_min_version", "tls_max_version", "cipher_suites", + "sds_config", } if err := checkHCLKeys(o.Val, valid); err != nil { @@ -556,6 +587,8 @@ func parseConsulGatewayTLS(o *ast.ObjectItem) (*api.ConsulGatewayTLSConfig, erro return nil, err } + delete(m, "sds_config") + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ Result: &tls, }) @@ -567,6 +600,22 @@ func parseConsulGatewayTLS(o *ast.ObjectItem) (*api.ConsulGatewayTLSConfig, erro return nil, err } + // Parse SDS + var listVal *ast.ObjectList + if ot, ok := o.Val.(*ast.ObjectType); ok { + listVal = ot.List + } else { + return nil, fmt.Errorf("tls: should be an object") + } + + so := listVal.Filter("sds_config") + if len(so.Items) > 0 { + tls.SDS, err = parseConsulGatewayTLSSDS(so.Items[0]) + if err != nil { + return nil, err + } + } + return &tls, nil } diff --git a/nomad/consul.go b/nomad/consul.go index a20074ab1f3..b201eefa6b4 100644 --- a/nomad/consul.go +++ b/nomad/consul.go @@ -605,11 +605,36 @@ func convertIngressCE(namespace, service string, entry *structs.ConsulIngressCon Namespace: namespace, Kind: api.IngressGateway, Name: service, - TLS: tls, + TLS: *convertGatewayTLSConfig(entry.TLS), Listeners: listeners, } } +func convertGatewayTLSConfig(in *structs.ConsulGatewayTLSConfig) *api.GatewayTLSConfig { + if in != nil { + return &api.GatewayTLSConfig{ + Enabled: in.Enabled, + TLSMinVersion: in.TLSMinVersion, + TLSMaxVersion: in.TLSMaxVersion, + CipherSuites: slices.Clone(in.CipherSuites), + SDS: convertGatewayTLSSDSConfig(in.SDS), + } + } else { + return &api.GatewayTLSConfig{} + } +} + +func convertGatewayTLSSDSConfig(in *structs.ConsulGatewayTLSSDSConfig) *api.GatewayTLSSDSConfig { + if in != nil { + return &api.GatewayTLSSDSConfig{ + ClusterName: in.ClusterName, + CertResource: in.CertResource, + } + } else { + return &api.GatewayTLSSDSConfig{} + } +} + func convertTerminatingCE(namespace, service string, entry *structs.ConsulTerminatingConfigEntry) api.ConfigEntry { var linked []api.LinkedService = nil for _, s := range entry.Services { diff --git a/nomad/structs/diff.go b/nomad/structs/diff.go index 086517faae1..45e462d92b6 100644 --- a/nomad/structs/diff.go +++ b/nomad/structs/diff.go @@ -1156,6 +1156,11 @@ func connectGatewayTLSConfigDiff(prev, next *ConsulGatewayTLSConfig, contextual // Diff the primitive field. diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual) + // Diff SDS object + if sdsDiff := primitiveObjectDiff(prev.SDS, next.SDS, nil, "SDS", contextual); sdsDiff != nil { + diff.Objects = append(diff.Objects, sdsDiff) + } + return diff } diff --git a/nomad/structs/services.go b/nomad/structs/services.go index 3bbbdb11291..97a65aa5e8d 100644 --- a/nomad/structs/services.go +++ b/nomad/structs/services.go @@ -1833,12 +1833,39 @@ func (p *ConsulGatewayProxy) Validate() error { return nil } +type ConsulGatewayTLSSDSConfig struct { + ClusterName string + CertResource string +} + +func (c *ConsulGatewayTLSSDSConfig) Copy() *ConsulGatewayTLSSDSConfig { + if c == nil { + return nil + } + + return &ConsulGatewayTLSSDSConfig{ + ClusterName: c.ClusterName, + CertResource: c.CertResource, + } +} + +func (c *ConsulGatewayTLSSDSConfig) Equal(o *ConsulGatewayTLSSDSConfig) bool { + if c == nil || o == nil { + return c == o + } + + return c.ClusterName == o.ClusterName && + c.CertResource == o.CertResource +} + // ConsulGatewayTLSConfig is used to configure TLS for a gateway. type ConsulGatewayTLSConfig struct { Enabled bool TLSMinVersion string TLSMaxVersion string CipherSuites []string + // SDS allows configuring TLS certificate from an SDS service. + SDS *ConsulGatewayTLSSDSConfig } func (c *ConsulGatewayTLSConfig) Copy() *ConsulGatewayTLSConfig { @@ -1851,6 +1878,7 @@ func (c *ConsulGatewayTLSConfig) Copy() *ConsulGatewayTLSConfig { TLSMinVersion: c.TLSMinVersion, TLSMaxVersion: c.TLSMaxVersion, CipherSuites: slices.Clone(c.CipherSuites), + SDS: c.SDS.Copy(), } } @@ -1862,7 +1890,8 @@ func (c *ConsulGatewayTLSConfig) Equal(o *ConsulGatewayTLSConfig) bool { return c.Enabled == o.Enabled && c.TLSMinVersion == o.TLSMinVersion && c.TLSMaxVersion == o.TLSMaxVersion && - helper.SliceSetEq(c.CipherSuites, o.CipherSuites) + helper.SliceSetEq(c.CipherSuites, o.CipherSuites) && + c.SDS.Equal(o.SDS) } // ConsulIngressService is used to configure a service fronted by the ingress gateway. diff --git a/website/content/docs/job-specification/gateway.mdx b/website/content/docs/job-specification/gateway.mdx index 16889f41140..93951ab1622 100644 --- a/website/content/docs/job-specification/gateway.mdx +++ b/website/content/docs/job-specification/gateway.mdx @@ -114,6 +114,12 @@ envoy_gateway_bind_addresses "" { [`CipherSuites`](/consul/docs/connect/config-entries/ingress-gateway#ciphersuites) in the Consul documentation for the supported cipher suites. +- `sds` `(SDSConfig: )` - Defines a set of parameters that configures the listener to load TLS certificates from an external SDS service. + + - `cluster_name` `(string)` - The SDS cluster name to connect to to retrieve certificates. + + - `cert_resource` `(string)` - The SDS resource name to request when fetching the certificate from the SDS service. + #### `listener` Parameters - `port` `(int: required)` - The port that the listener should receive traffic on. From 1506d64b69ba2f0577c9bfe168464c2756565aec Mon Sep 17 00:00:00 2001 From: Horacio Monsalvo Date: Fri, 31 Mar 2023 20:08:31 -0300 Subject: [PATCH 2/5] add fields --- api/consul.go | 71 +++++++++++++++++++-- command/agent/job_endpoint.go | 22 ++++++- jobspec/parse_service.go | 109 +++++++++++++++++++++++++++++++ nomad/consul.go | 29 ++++++++- nomad/structs/diff.go | 81 +++++++++++++++++++++++ nomad/structs/services.go | 117 ++++++++++++++++++++++++++++++++-- 6 files changed, 413 insertions(+), 16 deletions(-) diff --git a/api/consul.go b/api/consul.go index 4f1dc94868c..6ec8d24e8ed 100644 --- a/api/consul.go +++ b/api/consul.go @@ -424,13 +424,54 @@ func (tc *ConsulGatewayTLSConfig) Copy() *ConsulGatewayTLSConfig { return result } +// ConsulHTTPHeaderModifiers is a set of rules for HTTP header modification that +// should be performed by proxies as the request passes through them. It can +// operate on either request or response headers depending on the context in +// which it is used. +type ConsulHTTPHeaderModifiers struct { + // Add is a set of name -> value pairs that should be appended to the request + // or response (i.e. allowing duplicates if the same header already exists). + Add map[string]string `hcl:"add,block" mapstructure:"add"` + + // Set is a set of name -> value pairs that should be added to the request or + // response, overwriting any existing header values of the same name. + Set map[string]string `hcl:"set,block" mapstructure:"set"` + + // Remove is the set of header names that should be stripped from the request + // or response. + Remove []string `hcl:"remove,optional" mapstructure:"remove"` +} + +func (h *ConsulHTTPHeaderModifiers) Copy() *ConsulHTTPHeaderModifiers { + if h == nil { + return nil + } + + var remove []string + if n := len(h.Remove); n > 0 { + remove = make([]string, n) + copy(remove, h.Remove) + } + + return &ConsulHTTPHeaderModifiers{ + Add: maps.Clone(h.Add), + Set: maps.Clone(h.Set), + Remove: remove, + } +} + // ConsulIngressService is used to configure a service fronted by the ingress gateway. type ConsulIngressService struct { // Namespace is not yet supported. // Namespace string - Name string `hcl:"name,optional"` - - Hosts []string `hcl:"hosts,optional"` + Name string `hcl:"name,optional"` + Hosts []string `hcl:"hosts,optional"` + TLS *ConsulGatewayTLSConfig `hcl:"tls,block" mapstructure:"tls"` + RequestHeaders *ConsulHTTPHeaderModifiers `hcl:"request_headers,block" mapstructure:"request_headers"` + ResponseHeaders *ConsulHTTPHeaderModifiers `hcl:"response_headers,block" mapstructure:"response_headers"` + MaxConnections *uint32 `hcl:"max_connections,optional" mapstructure:"max_connections"` + MaxPendingRequests *uint32 `hcl:"max_pending_requests,optional" mapstructure:"max_pending_requests"` + MaxConcurrentRequests *uint32 `hcl:"max_concurrent_requests,optional" mapstructure:"max_concurrent_requests"` } func (s *ConsulIngressService) Canonicalize() { @@ -448,16 +489,34 @@ func (s *ConsulIngressService) Copy() *ConsulIngressService { return nil } + ns := new(ConsulIngressService) + *ns = *s + var hosts []string = nil if n := len(s.Hosts); n > 0 { hosts = make([]string, n) copy(hosts, s.Hosts) } - return &ConsulIngressService{ - Name: s.Name, - Hosts: hosts, + ns.Name = s.Name + ns.Hosts = hosts + ns.RequestHeaders = s.RequestHeaders.Copy() + ns.ResponseHeaders = s.ResponseHeaders.Copy() + ns.TLS = s.TLS.Copy() + + if s.MaxConnections != nil { + ns.MaxConnections = pointerOf(*s.MaxConnections) + } + + if s.MaxPendingRequests != nil { + ns.MaxPendingRequests = pointerOf(*s.MaxPendingRequests) + } + + if s.MaxConcurrentRequests != nil { + ns.MaxConcurrentRequests = pointerOf(*s.MaxConcurrentRequests) } + + return ns } const ( diff --git a/command/agent/job_endpoint.go b/command/agent/job_endpoint.go index dbc37b6e371..b065317661f 100644 --- a/command/agent/job_endpoint.go +++ b/command/agent/job_endpoint.go @@ -1594,14 +1594,32 @@ func apiConnectIngressServicesToStructs(in []*api.ConsulIngressService) []*struc return services } +func apiConsulHTTPHeaderModifiersToStructs(in *api.ConsulHTTPHeaderModifiers) *structs.ConsulHTTPHeaderModifiers { + if in == nil { + return nil + } + + return &structs.ConsulHTTPHeaderModifiers{ + Add: maps.Clone(in.Add), + Set: maps.Clone(in.Set), + Remove: slices.Clone(in.Remove), + } +} + func apiConnectIngressServiceToStructs(in *api.ConsulIngressService) *structs.ConsulIngressService { if in == nil { return nil } return &structs.ConsulIngressService{ - Name: in.Name, - Hosts: slices.Clone(in.Hosts), + Name: in.Name, + Hosts: slices.Clone(in.Hosts), + TLS: apiConnectGatewayTLSConfig(in.TLS), + RequestHeaders: apiConsulHTTPHeaderModifiersToStructs(in.RequestHeaders), + ResponseHeaders: apiConsulHTTPHeaderModifiersToStructs(in.ResponseHeaders), + MaxConnections: in.MaxConnections, + MaxPendingRequests: in.MaxPendingRequests, + MaxConcurrentRequests: in.MaxConcurrentRequests, } } diff --git a/jobspec/parse_service.go b/jobspec/parse_service.go index 2bb11f68a1c..ef1d988c41b 100644 --- a/jobspec/parse_service.go +++ b/jobspec/parse_service.go @@ -422,10 +422,84 @@ func parseGatewayProxy(o *ast.ObjectItem) (*api.ConsulGatewayProxy, error) { return &proxy, nil } +func parseConsulHTTPHeaderModifiers(o *ast.ObjectItem) (*api.ConsulHTTPHeaderModifiers, error) { + valid := []string{ + "add", + "set", + "remove", + } + + if err := checkHCLKeys(o.Val, valid); err != nil { + return nil, multierror.Prefix(err, "httpHeaderModifiers ->") + } + + var httpHeaderModifiers api.ConsulHTTPHeaderModifiers + var m map[string]interface{} + if err := hcl.DecodeObject(&m, o.Val); err != nil { + return nil, err + } + + delete(m, "add") + delete(m, "set") + + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &httpHeaderModifiers, + }) + if err != nil { + return nil, err + } + + if err := dec.Decode(m); err != nil { + return nil, err + } + + // Filter list + var listVal *ast.ObjectList + if ot, ok := o.Val.(*ast.ObjectType); ok { + listVal = ot.List + } else { + return nil, fmt.Errorf("'httpHeaderModifiers: should be an object") + } + + // Parse Add + if addO := listVal.Filter("add"); len(addO.Items) > 0 { + for _, o := range addO.Elem().Items { + var m map[string]interface{} + if err := hcl.DecodeObject(&m, o.Val); err != nil { + return nil, err + } + if err := mapstructure.WeakDecode(m, &httpHeaderModifiers.Add); err != nil { + return nil, err + } + } + } + + // Parse Set + if setO := listVal.Filter("set"); len(setO.Items) > 0 { + for _, o := range setO.Elem().Items { + var m map[string]interface{} + if err := hcl.DecodeObject(&m, o.Val); err != nil { + return nil, err + } + if err := mapstructure.WeakDecode(m, &httpHeaderModifiers.Set); err != nil { + return nil, err + } + } + } + + return &httpHeaderModifiers, nil +} + func parseConsulIngressService(o *ast.ObjectItem) (*api.ConsulIngressService, error) { valid := []string{ "name", "hosts", + "tls", + "request_headers", + "response_headers", + "max_connections", + "max_pending_requests", + "max_concurrent_requests", } if err := checkHCLKeys(o.Val, valid); err != nil { @@ -438,6 +512,10 @@ func parseConsulIngressService(o *ast.ObjectItem) (*api.ConsulIngressService, er return nil, err } + delete(m, "tls") + delete(m, "request_headers") + delete(m, "response_headers") + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ Result: &service, }) @@ -449,6 +527,37 @@ func parseConsulIngressService(o *ast.ObjectItem) (*api.ConsulIngressService, er return nil, err } + var listVal *ast.ObjectList + if ot, ok := o.Val.(*ast.ObjectType); ok { + listVal = ot.List + } else { + return nil, fmt.Errorf("service: should be an object") + } + + // Parse TLS + if tlsO := listVal.Filter("tls"); len(tlsO.Items) > 0 { + service.TLS, err = parseConsulGatewayTLS(tlsO.Items[0]) + if err != nil { + return nil, err + } + } + + // Parse Request Headers + if rqstHO := listVal.Filter("request_headers"); len(rqstHO.Items) > 0 { + service.RequestHeaders, err = parseConsulHTTPHeaderModifiers(rqstHO.Items[0]) + if err != nil { + return nil, err + } + } + + // Parse Response Headers + if rspHO := listVal.Filter("response_headers"); len(rspHO.Items) > 0 { + service.ResponseHeaders, err = parseConsulHTTPHeaderModifiers(rspHO.Items[0]) + if err != nil { + return nil, err + } + } + return &service, nil } diff --git a/nomad/consul.go b/nomad/consul.go index b201eefa6b4..5ea4041d322 100644 --- a/nomad/consul.go +++ b/nomad/consul.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/nomad/command/agent/consul" "github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/nomad/structs" + "golang.org/x/exp/maps" "golang.org/x/exp/slices" "golang.org/x/sync/errgroup" "golang.org/x/time/rate" @@ -581,9 +582,21 @@ func convertIngressCE(namespace, service string, entry *structs.ConsulIngressCon for _, listener := range entry.Listeners { var services []api.IngressService = nil for _, s := range listener.Services { + var sds *api.GatewayTLSSDSConfig = nil + if s.TLS != nil { + sds = convertGatewayTLSSDSConfig(s.TLS.SDS) + } services = append(services, api.IngressService{ - Name: s.Name, - Hosts: slices.Clone(s.Hosts), + Name: s.Name, + Hosts: slices.Clone(s.Hosts), + RequestHeaders: convertHTTPHeaderModifiers(s.RequestHeaders), + ResponseHeaders: convertHTTPHeaderModifiers(s.ResponseHeaders), + MaxConnections: s.MaxConnections, + MaxPendingRequests: s.MaxPendingRequests, + MaxConcurrentRequests: s.MaxConcurrentRequests, + TLS: &api.GatewayServiceTLSConfig{ + SDS: sds, + }, }) } listeners = append(listeners, api.IngressListener{ @@ -610,6 +623,18 @@ func convertIngressCE(namespace, service string, entry *structs.ConsulIngressCon } } +func convertHTTPHeaderModifiers(in *structs.ConsulHTTPHeaderModifiers) *api.HTTPHeaderModifiers { + if in != nil { + return &api.HTTPHeaderModifiers{ + Add: maps.Clone(in.Add), + Set: maps.Clone(in.Set), + Remove: slices.Clone(in.Remove), + } + } else { + return &api.HTTPHeaderModifiers{} + } +} + func convertGatewayTLSConfig(in *structs.ConsulGatewayTLSConfig) *api.GatewayTLSConfig { if in != nil { return &api.GatewayTLSConfig{ diff --git a/nomad/structs/diff.go b/nomad/structs/diff.go index 45e462d92b6..ca358ec81ab 100644 --- a/nomad/structs/diff.go +++ b/nomad/structs/diff.go @@ -1290,6 +1290,38 @@ func connectGatewayIngressServiceDiff(prev, next *ConsulIngressService, contextu newPrimitiveFlat = flatmap.Flatten(next, nil, true) } + // Diff pointer types. + if prev != nil { + if prev.MaxConnections != nil { + oldPrimitiveFlat["MaxConnections"] = fmt.Sprintf("%v", *prev.MaxConnections) + } + } + if next != nil { + if next.MaxConnections != nil { + newPrimitiveFlat["MaxConnections"] = fmt.Sprintf("%v", *next.MaxConnections) + } + } + if prev != nil { + if prev.MaxPendingRequests != nil { + oldPrimitiveFlat["MaxPendingRequests"] = fmt.Sprintf("%v", *prev.MaxPendingRequests) + } + } + if next != nil { + if next.MaxPendingRequests != nil { + newPrimitiveFlat["MaxPendingRequests"] = fmt.Sprintf("%v", *next.MaxPendingRequests) + } + } + if prev != nil { + if prev.MaxConcurrentRequests != nil { + oldPrimitiveFlat["MaxConcurrentRequests"] = fmt.Sprintf("%v", *prev.MaxConcurrentRequests) + } + } + if next != nil { + if next.MaxConcurrentRequests != nil { + newPrimitiveFlat["MaxConcurrentRequests"] = fmt.Sprintf("%v", *next.MaxConcurrentRequests) + } + } + // Diff the primitive fields. diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual) @@ -1298,6 +1330,55 @@ func connectGatewayIngressServiceDiff(prev, next *ConsulIngressService, contextu diff.Objects = append(diff.Objects, hDiffs) } + // Diff the ConsulGatewayTLSConfig objects. + tlsConfigDiff := connectGatewayTLSConfigDiff(prev.TLS, next.TLS, contextual) + if tlsConfigDiff != nil { + diff.Objects = append(diff.Objects, tlsConfigDiff) + } + + // Diff the ConsulHTTPHeaderModifiers objects (RequestHeaders). + reqModifiersDiff := connectGatewayHTTPHeaderModifiersDiff(prev.RequestHeaders, next.RequestHeaders, "RequestHeaders", contextual) + if reqModifiersDiff != nil { + diff.Objects = append(diff.Objects, reqModifiersDiff) + } + + // Diff the ConsulHTTPHeaderModifiers objects (ResponseHeaders). + respModifiersDiff := connectGatewayHTTPHeaderModifiersDiff(prev.ResponseHeaders, next.ResponseHeaders, "ResponseHeaders", contextual) + if respModifiersDiff != nil { + diff.Objects = append(diff.Objects, respModifiersDiff) + } + + return diff +} + +func connectGatewayHTTPHeaderModifiersDiff(prev, next *ConsulHTTPHeaderModifiers, name string, contextual bool) *ObjectDiff { + diff := &ObjectDiff{Type: DiffTypeNone, Name: name} + var oldPrimitiveFlat, newPrimitiveFlat map[string]string + + if reflect.DeepEqual(prev, next) { + return nil + } else if prev == nil { + prev = new(ConsulHTTPHeaderModifiers) + diff.Type = DiffTypeAdded + newPrimitiveFlat = flatmap.Flatten(next, nil, true) + } else if next == nil { + next = new(ConsulHTTPHeaderModifiers) + diff.Type = DiffTypeDeleted + oldPrimitiveFlat = flatmap.Flatten(prev, nil, true) + } else { + diff.Type = DiffTypeEdited + oldPrimitiveFlat = flatmap.Flatten(prev, nil, true) + newPrimitiveFlat = flatmap.Flatten(next, nil, true) + } + + // Diff the primitive fields. + diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual) + + // Diff the Remove Headers. + if rDiffs := stringSetDiff(prev.Remove, next.Remove, "Remove", contextual); rDiffs != nil { + diff.Objects = append(diff.Objects, rDiffs) + } + return diff } diff --git a/nomad/structs/services.go b/nomad/structs/services.go index 97a65aa5e8d..150938dd299 100644 --- a/nomad/structs/services.go +++ b/nomad/structs/services.go @@ -1894,10 +1894,66 @@ func (c *ConsulGatewayTLSConfig) Equal(o *ConsulGatewayTLSConfig) bool { c.SDS.Equal(o.SDS) } +// ConsulHTTPHeaderModifiers is a set of rules for HTTP header modification that +// should be performed by proxies as the request passes through them. It can +// operate on either request or response headers depending on the context in +// which it is used. +type ConsulHTTPHeaderModifiers struct { + // Add is a set of name -> value pairs that should be appended to the request + // or response (i.e. allowing duplicates if the same header already exists). + Add map[string]string + + // Set is a set of name -> value pairs that should be added to the request or + // response, overwriting any existing header values of the same name. + Set map[string]string + + // Remove is the set of header names that should be stripped from the request + // or response. + Remove []string +} + +func (h *ConsulHTTPHeaderModifiers) Copy() *ConsulHTTPHeaderModifiers { + if h == nil { + return nil + } + + return &ConsulHTTPHeaderModifiers{ + Add: maps.Clone(h.Add), + Set: maps.Clone(h.Set), + Remove: slices.Clone(h.Remove), + } +} + +func (h *ConsulHTTPHeaderModifiers) Equal(o *ConsulHTTPHeaderModifiers) bool { + if h == nil || o == nil { + return h == o + } + + if !maps.Equal(h.Add, o.Add) { + return false + } + + if !maps.Equal(h.Set, o.Set) { + return false + } + + if !helper.SliceSetEq(h.Remove, o.Remove) { + return false + } + + return true +} + // ConsulIngressService is used to configure a service fronted by the ingress gateway. type ConsulIngressService struct { - Name string - Hosts []string + Name string + Hosts []string + TLS *ConsulGatewayTLSConfig + RequestHeaders *ConsulHTTPHeaderModifiers + ResponseHeaders *ConsulHTTPHeaderModifiers + MaxConnections *uint32 + MaxPendingRequests *uint32 + MaxConcurrentRequests *uint32 } func (s *ConsulIngressService) Copy() *ConsulIngressService { @@ -1905,16 +1961,37 @@ func (s *ConsulIngressService) Copy() *ConsulIngressService { return nil } + ns := new(ConsulIngressService) + *ns = *s + var hosts []string = nil if n := len(s.Hosts); n > 0 { hosts = make([]string, n) copy(hosts, s.Hosts) } - return &ConsulIngressService{ - Name: s.Name, - Hosts: hosts, + ns.Name = s.Name + ns.Hosts = hosts + ns.RequestHeaders = s.RequestHeaders.Copy() + ns.ResponseHeaders = s.ResponseHeaders.Copy() + + if s.TLS != nil { + ns.TLS = s.TLS.Copy() + } + + if s.MaxConnections != nil { + ns.MaxConnections = pointer.Of(*s.MaxConnections) + } + + if s.MaxPendingRequests != nil { + ns.MaxPendingRequests = pointer.Of(*s.MaxPendingRequests) } + + if s.MaxConcurrentRequests != nil { + ns.MaxConcurrentRequests = pointer.Of(*s.MaxConcurrentRequests) + } + + return ns } func (s *ConsulIngressService) Equal(o *ConsulIngressService) bool { @@ -1926,7 +2003,35 @@ func (s *ConsulIngressService) Equal(o *ConsulIngressService) bool { return false } - return helper.SliceSetEq(s.Hosts, o.Hosts) + if !helper.SliceSetEq(s.Hosts, o.Hosts) { + return false + } + + if !s.TLS.Equal(o.TLS) { + return false + } + + if !s.RequestHeaders.Equal(o.RequestHeaders) { + return false + } + + if !s.ResponseHeaders.Equal(o.ResponseHeaders) { + return false + } + + if !pointer.Eq(s.MaxConnections, o.MaxConnections) { + return false + } + + if !pointer.Eq(s.MaxPendingRequests, o.MaxPendingRequests) { + return false + } + + if !pointer.Eq(s.MaxConcurrentRequests, o.MaxConcurrentRequests) { + return false + } + + return true } func (s *ConsulIngressService) Validate(protocol string) error { From 57c1ebddf4411783cff3c93884dbf680f18f4d9d Mon Sep 17 00:00:00 2001 From: Horacio Monsalvo Date: Fri, 31 Mar 2023 20:08:49 -0300 Subject: [PATCH 3/5] update tests --- api/consul_test.go | 27 ++++++++++ command/agent/job_endpoint_test.go | 54 +++++++++++++++++++ jobspec/parse_test.go | 18 +++++++ .../tg-service-connect-gateway-ingress.hcl | 17 ++++++ 4 files changed, 116 insertions(+) diff --git a/api/consul_test.go b/api/consul_test.go index a0cc39e42b9..a7024dccf24 100644 --- a/api/consul_test.go +++ b/api/consul_test.go @@ -400,6 +400,33 @@ func TestConsulIngressConfigEntry_Copy(t *testing.T) { Services: []*ConsulIngressService{{ Name: "service1", Hosts: []string{"1.1.1.1", "1.1.1.1:9000"}, + TLS: &ConsulGatewayTLSConfig{ + SDS: &ConsulGatewayTLSSDSConfig{ + ClusterName: "foo", + CertResource: "bar", + }, + }, + RequestHeaders: &ConsulHTTPHeaderModifiers{ + Add: map[string]string{ + "test": "testvalue", + }, + Set: map[string]string{ + "test1": "testvalue1", + }, + Remove: []string{"test2"}, + }, + ResponseHeaders: &ConsulHTTPHeaderModifiers{ + Add: map[string]string{ + "test": "testvalue", + }, + Set: map[string]string{ + "test1": "testvalue1", + }, + Remove: []string{"test2"}, + }, + MaxConnections: pointerOf(uint32(5120)), + MaxPendingRequests: pointerOf(uint32(512)), + MaxConcurrentRequests: pointerOf(uint32(2048)), }, { Name: "service2", Hosts: []string{"2.2.2.2"}, diff --git a/command/agent/job_endpoint_test.go b/command/agent/job_endpoint_test.go index 9076391022e..ee9092b0913 100644 --- a/command/agent/job_endpoint_test.go +++ b/command/agent/job_endpoint_test.go @@ -3861,6 +3861,33 @@ func TestConversion_ApiConsulConnectToStructs(t *testing.T) { Services: []*structs.ConsulIngressService{{ Name: "ingress1", Hosts: []string{"host1"}, + TLS: &structs.ConsulGatewayTLSConfig{ + SDS: &structs.ConsulGatewayTLSSDSConfig{ + ClusterName: "foo", + CertResource: "bar", + }, + }, + RequestHeaders: &structs.ConsulHTTPHeaderModifiers{ + Add: map[string]string{ + "test": "testvalue", + }, + Set: map[string]string{ + "test1": "testvalue1", + }, + Remove: []string{"test2"}, + }, + ResponseHeaders: &structs.ConsulHTTPHeaderModifiers{ + Add: map[string]string{ + "test": "testvalue", + }, + Set: map[string]string{ + "test1": "testvalue1", + }, + Remove: []string{"test2"}, + }, + MaxConnections: pointer.Of(uint32(5120)), + MaxPendingRequests: pointer.Of(uint32(512)), + MaxConcurrentRequests: pointer.Of(uint32(2048)), }}, }}, }, @@ -3881,6 +3908,33 @@ func TestConversion_ApiConsulConnectToStructs(t *testing.T) { Services: []*api.ConsulIngressService{{ Name: "ingress1", Hosts: []string{"host1"}, + TLS: &api.ConsulGatewayTLSConfig{ + SDS: &api.ConsulGatewayTLSSDSConfig{ + ClusterName: "foo", + CertResource: "bar", + }, + }, + RequestHeaders: &api.ConsulHTTPHeaderModifiers{ + Add: map[string]string{ + "test": "testvalue", + }, + Set: map[string]string{ + "test1": "testvalue1", + }, + Remove: []string{"test2"}, + }, + ResponseHeaders: &api.ConsulHTTPHeaderModifiers{ + Add: map[string]string{ + "test": "testvalue", + }, + Set: map[string]string{ + "test1": "testvalue1", + }, + Remove: []string{"test2"}, + }, + MaxConnections: pointer.Of(uint32(5120)), + MaxPendingRequests: pointer.Of(uint32(512)), + MaxConcurrentRequests: pointer.Of(uint32(2048)), }}, }}, }, diff --git a/jobspec/parse_test.go b/jobspec/parse_test.go index 173b338cacf..298e00390f8 100644 --- a/jobspec/parse_test.go +++ b/jobspec/parse_test.go @@ -9,6 +9,7 @@ import ( capi "github.com/hashicorp/consul/api" "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/ci" + "github.com/hashicorp/nomad/helper/pointer" "github.com/stretchr/testify/require" ) @@ -1645,6 +1646,23 @@ func TestParse(t *testing.T) { Hosts: []string{ "2.2.2.2:8080", }, + TLS: &api.ConsulGatewayTLSConfig{ + SDS: &api.ConsulGatewayTLSSDSConfig{ + ClusterName: "foo", + CertResource: "bar", + }, + }, + RequestHeaders: &api.ConsulHTTPHeaderModifiers{ + Add: map[string]string{ + "test": "testvalue", + }, + }, + ResponseHeaders: &api.ConsulHTTPHeaderModifiers{ + Remove: []string{"test2"}, + }, + MaxConnections: pointer.Of(uint32(5120)), + MaxPendingRequests: pointer.Of(uint32(512)), + MaxConcurrentRequests: pointer.Of(uint32(2048)), }}, }, }, diff --git a/jobspec/test-fixtures/tg-service-connect-gateway-ingress.hcl b/jobspec/test-fixtures/tg-service-connect-gateway-ingress.hcl index 2474a0fcd84..f15668b856e 100644 --- a/jobspec/test-fixtures/tg-service-connect-gateway-ingress.hcl +++ b/jobspec/test-fixtures/tg-service-connect-gateway-ingress.hcl @@ -47,6 +47,23 @@ job "connect_gateway_ingress" { service { name = "nginx" hosts = ["2.2.2.2:8080"] + tls { + sds_config { + cluster_name = "foo" + cert_resource = "bar" + } + } + request_headers { + add { + test = "testvalue" + } + } + response_headers { + remove = ["test2"] + } + max_connections = 5120 + max_pending_requests = 512 + max_concurrent_requests = 2048 } } } From f4909d7774c2f025d455be903bd500f0737c7765 Mon Sep 17 00:00:00 2001 From: Horacio Monsalvo Date: Fri, 31 Mar 2023 20:09:10 -0300 Subject: [PATCH 4/5] update docs --- .../content/docs/job-specification/gateway.mdx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/website/content/docs/job-specification/gateway.mdx b/website/content/docs/job-specification/gateway.mdx index 93951ab1622..f1a51c4d753 100644 --- a/website/content/docs/job-specification/gateway.mdx +++ b/website/content/docs/job-specification/gateway.mdx @@ -180,8 +180,25 @@ envoy_gateway_bind_addresses "" { - `key_file` `(string: )` - A file path to a PEM-encoded private key. The file must be accessible by the gateway task. The key is used with the certificate to verify the gateway's authenticity. It must be provided if a `cert_file` is provided. +- `max_concurrent_requests` `(int: 0)` - overrides for the Defaults field +- `max_connections` `(int: 0)` - overrides for the Defaults field +- `max_pending_requests` `(int: 0)` - overrides for the Defaults field +- `response_headers` `(HTTPHeaderModifiers: )` - A set of HTTP-specific header modification rules that + will be applied to responses from this service. This cannot be used with a tcp listener. +- `request_headers` `(HTTPHeaderModifiers: )` - A set of HTTP-specific header modification rules that + will be applied to requests routed to this service. This cannot be used with a tcp listener. - `sni` `(string: )` - An optional hostname or domain name to specify during the TLS handshake. +- `tls` `(ServiceTLSConfig: )` - TLS configuration for this service. + + - `sds` `(SDSConfig: )` - Defines a set of parameters that configures the SDS source + for the certificate for this specific service. At least one custom host must be specified in Hosts. + The certificate retrieved from SDS will be served for all requests identifying one of the Hosts values + in the TLS Server Name Indication (SNI) header. + + - `cluster_name` `(string)` - The SDS cluster name to connect to to retrieve certificates. + + - `cert_resource` `(string)` - The SDS resource name to request when fetching the certificate from the SDS service. ### `mesh` Parameters From 0948f53916319c5a1a50e53e27206e0653ad5b9b Mon Sep 17 00:00:00 2001 From: Horacio Monsalvo Date: Mon, 3 Apr 2023 10:12:19 -0300 Subject: [PATCH 5/5] add changelog file --- .changelog/16753.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/16753.txt diff --git a/.changelog/16753.txt b/.changelog/16753.txt new file mode 100644 index 00000000000..be5b23cf84a --- /dev/null +++ b/.changelog/16753.txt @@ -0,0 +1,3 @@ +```release-note:improvement +connect: Added support for TLS RequestHeaders ResponeHeaders Config on ingress service block +```