Skip to content

Commit 95fd648

Browse files
author
Kate Osborn
committed
Refactor nginx/config package and add split clients
1 parent 7961bd3 commit 95fd648

18 files changed

+2236
-1395
lines changed

internal/nginx/config/generator.go

Lines changed: 18 additions & 316 deletions
Original file line numberDiff line numberDiff line change
@@ -1,341 +1,43 @@
11
package config
22

33
import (
4-
"encoding/json"
5-
"fmt"
6-
"strings"
7-
8-
"sigs.k8s.io/gateway-api/apis/v1beta1"
9-
104
"github.com/nginxinc/nginx-kubernetes-gateway/internal/state"
115
)
126

13-
// nginx502Server is used as a backend for services that cannot be resolved (have no IP address).
14-
const nginx502Server = "unix:/var/lib/nginx/nginx-502-server.sock"
15-
167
//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . Generator
178

189
// Generator generates NGINX configuration.
10+
// This interface is used for testing purposes only.
1911
type Generator interface {
2012
// Generate generates NGINX configuration from internal representation.
2113
Generate(configuration state.Configuration) []byte
2214
}
2315

24-
// GeneratorImpl is an implementation of Generator
25-
type GeneratorImpl struct {
26-
executor *templateExecutor
27-
}
16+
// GeneratorImpl is an implementation of Generator.
17+
type GeneratorImpl struct{}
2818

2919
// NewGeneratorImpl creates a new GeneratorImpl.
30-
func NewGeneratorImpl() *GeneratorImpl {
31-
return &GeneratorImpl{
32-
executor: newTemplateExecutor(),
33-
}
34-
}
35-
36-
func (g *GeneratorImpl) Generate(conf state.Configuration) []byte {
37-
httpServers := generateHTTPServers(conf)
38-
39-
httpUpstreams := generateHTTPUpstreams(conf.Upstreams)
40-
41-
retVal := append(g.executor.ExecuteForHTTPUpstreams(httpUpstreams), g.executor.ExecuteForHTTPServers(httpServers)...)
42-
43-
return retVal
44-
}
45-
46-
func generateHTTPServers(conf state.Configuration) HTTPServers {
47-
confServers := append(conf.HTTPServers, conf.SSLServers...)
48-
49-
servers := make([]Server, 0, len(confServers)+2)
50-
51-
if len(conf.HTTPServers) > 0 {
52-
defaultHTTPServer := generateDefaultHTTPServer()
53-
54-
servers = append(servers, defaultHTTPServer)
55-
}
56-
57-
if len(conf.SSLServers) > 0 {
58-
defaultSSLServer := generateDefaultSSLServer()
59-
60-
servers = append(servers, defaultSSLServer)
61-
}
62-
63-
for _, s := range confServers {
64-
servers = append(servers, generateServer(s))
65-
}
66-
67-
return HTTPServers{Servers: servers}
68-
}
69-
70-
func generateDefaultSSLServer() Server {
71-
return Server{IsDefaultSSL: true}
72-
}
73-
74-
func generateDefaultHTTPServer() Server {
75-
return Server{IsDefaultHTTP: true}
76-
}
77-
78-
func generateServer(virtualServer state.VirtualServer) Server {
79-
s := Server{ServerName: virtualServer.Hostname}
80-
81-
listenerPort := 80
82-
83-
if virtualServer.SSL != nil {
84-
s.SSL = &SSL{
85-
Certificate: virtualServer.SSL.CertificatePath,
86-
CertificateKey: virtualServer.SSL.CertificatePath,
87-
}
88-
89-
listenerPort = 443
90-
}
91-
92-
if len(virtualServer.PathRules) == 0 {
93-
// generate default "/" 404 location
94-
s.Locations = []Location{{Path: "/", Return: &Return{Code: StatusNotFound}}}
95-
return s
96-
}
97-
98-
locs := make([]Location, 0, len(virtualServer.PathRules)) // FIXME(pleshakov): expand with rule.Routes
99-
for _, rule := range virtualServer.PathRules {
100-
matches := make([]httpMatch, 0, len(rule.MatchRules))
101-
102-
for matchRuleIdx, r := range rule.MatchRules {
103-
m := r.GetMatch()
104-
105-
var loc Location
106-
107-
// handle case where the only route is a path-only match
108-
// generate a standard location block without http_matches.
109-
if len(rule.MatchRules) == 1 && isPathOnlyMatch(m) {
110-
loc = Location{
111-
Path: rule.Path,
112-
}
113-
locs = append(locs, Location{
114-
Path: rule.Path,
115-
ProxyPass: generateProxyPass(r.UpstreamName),
116-
})
117-
} else {
118-
path := createPathForMatch(rule.Path, matchRuleIdx)
119-
loc = generateMatchLocation(path)
120-
matches = append(matches, createHTTPMatch(m, path))
121-
}
122-
123-
// FIXME(pleshakov): There could be a case when the filter has the type set but not the corresponding field.
124-
// For example, type is v1beta1.HTTPRouteFilterRequestRedirect, but RequestRedirect field is nil.
125-
// The validation webhook catches that.
126-
// If it doesn't work as expected, such situation is silently handled below in findFirstFilters.
127-
// Consider reporting an error. But that should be done in a separate validation layer.
128-
129-
// RequestRedirect and proxying are mutually exclusive.
130-
if r.Filters.RequestRedirect != nil {
131-
loc.Return = generateReturnValForRedirectFilter(r.Filters.RequestRedirect, listenerPort)
132-
} else {
133-
loc.ProxyPass = generateProxyPass(r.UpstreamName)
134-
}
135-
136-
locs = append(locs, loc)
137-
}
138-
139-
if len(matches) > 0 {
140-
b, err := json.Marshal(matches)
141-
if err != nil {
142-
// panic is safe here because we should never fail to marshal the match unless we constructed it incorrectly.
143-
panic(fmt.Errorf("could not marshal http match: %w", err))
144-
}
145-
146-
pathLoc := Location{
147-
Path: rule.Path,
148-
HTTPMatchVar: string(b),
149-
}
150-
151-
locs = append(locs, pathLoc)
152-
}
153-
}
154-
155-
s.Locations = locs
156-
157-
return s
158-
}
159-
160-
func generateReturnValForRedirectFilter(filter *v1beta1.HTTPRequestRedirectFilter, listenerPort int) *Return {
161-
if filter == nil {
162-
return nil
163-
}
164-
165-
hostname := "$host"
166-
if filter.Hostname != nil {
167-
hostname = string(*filter.Hostname)
168-
}
169-
170-
// FIXME(pleshakov): Unknown values here must result in the implementation setting the Attached Condition for
171-
// the Route to `status: False`, with a Reason of `UnsupportedValue`. In that case, all routes of the Route will be
172-
// ignored. NGINX will return 500. This should be implemented in the validation layer.
173-
code := StatusFound
174-
if filter.StatusCode != nil {
175-
code = StatusCode(*filter.StatusCode)
176-
}
177-
178-
port := listenerPort
179-
if filter.Port != nil {
180-
port = int(*filter.Port)
181-
}
182-
183-
// FIXME(pleshakov): Same as the FIXME about StatusCode above.
184-
scheme := "$scheme"
185-
if filter.Scheme != nil {
186-
scheme = *filter.Scheme
187-
}
188-
189-
return &Return{
190-
Code: code,
191-
URL: fmt.Sprintf("%s://%s:%d$request_uri", scheme, hostname, port),
192-
}
20+
func NewGeneratorImpl() GeneratorImpl {
21+
return GeneratorImpl{}
19322
}
19423

195-
func generateHTTPUpstreams(upstreams []state.Upstream) HTTPUpstreams {
196-
// capacity is the number of upstreams + 1 for the invalid backend ref upstream
197-
ups := make([]Upstream, 0, len(upstreams)+1)
24+
// executeFunc is a function that generates NGINX configuration from internal representation.
25+
type executeFunc func(configuration state.Configuration) []byte
19826

199-
for _, u := range upstreams {
200-
ups = append(ups, generateUpstream(u))
27+
// Generate generates NGINX configuration from internal representation.
28+
func (g GeneratorImpl) Generate(configuration state.Configuration) []byte {
29+
var generated []byte
30+
for _, execute := range getExecuteFuncs() {
31+
generated = append(generated, execute(configuration)...)
20132
}
20233

203-
ups = append(ups, generateInvalidBackendRefUpstream())
204-
205-
return HTTPUpstreams{
206-
Upstreams: ups,
207-
}
208-
}
209-
210-
func generateUpstream(up state.Upstream) Upstream {
211-
if len(up.Endpoints) == 0 {
212-
return Upstream{
213-
Name: up.Name,
214-
Servers: []UpstreamServer{
215-
{
216-
Address: nginx502Server,
217-
},
218-
},
219-
}
220-
}
221-
222-
upstreamServers := make([]UpstreamServer, len(up.Endpoints))
223-
for idx, ep := range up.Endpoints {
224-
upstreamServers[idx] = UpstreamServer{
225-
Address: fmt.Sprintf("%s:%d", ep.Address, ep.Port),
226-
}
227-
}
228-
229-
return Upstream{
230-
Name: up.Name,
231-
Servers: upstreamServers,
232-
}
233-
}
234-
235-
func generateInvalidBackendRefUpstream() Upstream {
236-
return Upstream{
237-
Name: state.InvalidBackendRef,
238-
Servers: []UpstreamServer{
239-
{
240-
Address: nginx502Server,
241-
},
242-
},
243-
}
244-
}
245-
246-
func generateProxyPass(address string) string {
247-
return "http://" + address
34+
return generated
24835
}
24936

250-
func generateMatchLocation(path string) Location {
251-
return Location{
252-
Path: path,
253-
Internal: true,
37+
func getExecuteFuncs() []executeFunc {
38+
return []executeFunc{
39+
executeUpstreams,
40+
executeSplitClients,
41+
executeServers,
25442
}
25543
}
256-
257-
func createPathForMatch(path string, routeIdx int) string {
258-
return fmt.Sprintf("%s_route%d", path, routeIdx)
259-
}
260-
261-
// httpMatch is an internal representation of an HTTPRouteMatch.
262-
// This struct is marshaled into a string and stored as a variable in the nginx location block for the route's path.
263-
// The NJS httpmatches module will lookup this variable on the request object and compare the request against the Method, Headers, and QueryParams contained in httpMatch.
264-
// If the request satisfies the httpMatch, the request will be internally redirected to the location RedirectPath by NGINX.
265-
type httpMatch struct {
266-
// Any represents a match with no match conditions.
267-
Any bool `json:"any,omitempty"`
268-
// Method is the HTTPMethod of the HTTPRouteMatch.
269-
Method v1beta1.HTTPMethod `json:"method,omitempty"`
270-
// Headers is a list of HTTPHeaders name value pairs with the format "{name}:{value}".
271-
Headers []string `json:"headers,omitempty"`
272-
// QueryParams is a list of HTTPQueryParams name value pairs with the format "{name}={value}".
273-
QueryParams []string `json:"params,omitempty"`
274-
// RedirectPath is the path to redirect the request to if the request satisfies the match conditions.
275-
RedirectPath string `json:"redirectPath,omitempty"`
276-
}
277-
278-
func createHTTPMatch(match v1beta1.HTTPRouteMatch, redirectPath string) httpMatch {
279-
hm := httpMatch{
280-
RedirectPath: redirectPath,
281-
}
282-
283-
if isPathOnlyMatch(match) {
284-
hm.Any = true
285-
return hm
286-
}
287-
288-
if match.Method != nil {
289-
hm.Method = *match.Method
290-
}
291-
292-
if match.Headers != nil {
293-
headers := make([]string, 0, len(match.Headers))
294-
headerNames := make(map[string]struct{})
295-
296-
// FIXME(kate-osborn): For now we only support type "Exact".
297-
for _, h := range match.Headers {
298-
if *h.Type == v1beta1.HeaderMatchExact {
299-
// duplicate header names are not permitted by the spec
300-
// only configure the first entry for every header name (case-insensitive)
301-
lowerName := strings.ToLower(string(h.Name))
302-
if _, ok := headerNames[lowerName]; !ok {
303-
headers = append(headers, createHeaderKeyValString(h))
304-
headerNames[lowerName] = struct{}{}
305-
}
306-
}
307-
}
308-
hm.Headers = headers
309-
}
310-
311-
if match.QueryParams != nil {
312-
params := make([]string, 0, len(match.QueryParams))
313-
314-
// FIXME(kate-osborn): For now we only support type "Exact".
315-
for _, p := range match.QueryParams {
316-
if *p.Type == v1beta1.QueryParamMatchExact {
317-
params = append(params, createQueryParamKeyValString(p))
318-
}
319-
}
320-
hm.QueryParams = params
321-
}
322-
323-
return hm
324-
}
325-
326-
// The name and values are delimited by "=". A name and value can always be recovered using strings.SplitN(arg,"=", 2).
327-
// Query Parameters are case-sensitive so case is preserved.
328-
func createQueryParamKeyValString(p v1beta1.HTTPQueryParamMatch) string {
329-
return p.Name + "=" + p.Value
330-
}
331-
332-
// The name and values are delimited by ":". A name and value can always be recovered using strings.Split(arg, ":").
333-
// Header names are case-insensitive while header values are case-sensitive (e.g. foo:bar == FOO:bar, but foo:bar != foo:BAR).
334-
// We preserve the case of the name here because NGINX allows us to lookup the header names in a case-insensitive manner.
335-
func createHeaderKeyValString(h v1beta1.HTTPHeaderMatch) string {
336-
return string(h.Name) + ":" + h.Value
337-
}
338-
339-
func isPathOnlyMatch(match v1beta1.HTTPRouteMatch) bool {
340-
return match.Method == nil && match.Headers == nil && match.QueryParams == nil
341-
}

0 commit comments

Comments
 (0)