|
1 | 1 | package config
|
2 | 2 |
|
3 | 3 | import (
|
4 |
| - "encoding/json" |
5 |
| - "fmt" |
6 |
| - "strings" |
7 |
| - |
8 |
| - "sigs.k8s.io/gateway-api/apis/v1beta1" |
9 |
| - |
10 | 4 | "github.com/nginxinc/nginx-kubernetes-gateway/internal/state"
|
11 | 5 | )
|
12 | 6 |
|
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 |
| - |
16 | 7 | //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . Generator
|
17 | 8 |
|
18 | 9 | // Generator generates NGINX configuration.
|
| 10 | +// This interface is used for testing purposes only. |
19 | 11 | type Generator interface {
|
20 | 12 | // Generate generates NGINX configuration from internal representation.
|
21 | 13 | Generate(configuration state.Configuration) []byte
|
22 | 14 | }
|
23 | 15 |
|
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{} |
28 | 18 |
|
29 | 19 | // 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{} |
193 | 22 | }
|
194 | 23 |
|
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 |
198 | 26 |
|
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)...) |
201 | 32 | }
|
202 | 33 |
|
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 |
248 | 35 | }
|
249 | 36 |
|
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, |
254 | 42 | }
|
255 | 43 | }
|
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