@@ -75,24 +75,9 @@ func createServer(virtualServer dataplane.VirtualServer) http.Server {
7575}
7676
7777func createLocations (pathRules []dataplane.PathRule , listenerPort int32 ) []http.Location {
78- lenPathRules := len (pathRules )
79-
80- if lenPathRules == 0 {
81- return []http.Location {createDefaultRootLocation ()}
82- }
83-
84- // To calculate the maximum number of locations, we need to take into account the following:
85- // 1. Each match rule for a path rule will have one location.
86- // 2. Each path rule may have an additional location if it contains non-path-only matches.
87- // 3. There may be an additional location for the default root path.
88- maxLocs := 1
89- for _ , rules := range pathRules {
90- maxLocs += len (rules .MatchRules ) + 1
91- }
92-
78+ maxLocs , pathsAndTypes := getMaxLocationCountAndPathMap (pathRules )
9379 locs := make ([]http.Location , 0 , maxLocs )
94-
95- rootPathExists := false
80+ var rootPathExists bool
9681
9782 for _ , rule := range pathRules {
9883 matches := make ([]httpMatch , 0 , len (rule .MatchRules ))
@@ -101,27 +86,23 @@ func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http.
10186 rootPathExists = true
10287 }
10388
89+ extLocations := initializeExternalLocations (rule , pathsAndTypes )
90+
10491 for matchRuleIdx , r := range rule .MatchRules {
10592 m := r .GetMatch ()
10693
107- var loc http.Location
108-
109- // handle case where the only route is a path-only match
110- // generate a standard location block without http_matches.
111- if len (rule .MatchRules ) == 1 && isPathOnlyMatch (m ) {
112- loc = http.Location {
113- Path : rule .Path ,
114- Exact : rule .PathType == dataplane .PathTypeExact ,
115- }
116- } else {
117- path := createPathForMatch (rule .Path , rule .PathType , matchRuleIdx )
118- loc = createMatchLocation (path )
119- matches = append (matches , createHTTPMatch (m , path ))
94+ buildLocations := extLocations
95+ if len (rule .MatchRules ) != 1 || ! isPathOnlyMatch (m ) {
96+ intLocation , match := initializeInternalLocation (rule , matchRuleIdx , m )
97+ buildLocations = []http.Location {intLocation }
98+ matches = append (matches , match )
12099 }
121100
122101 if r .Filters .InvalidFilter != nil {
123- loc .Return = & http.Return {Code : http .StatusInternalServerError }
124- locs = append (locs , loc )
102+ for i := range buildLocations {
103+ buildLocations [i ].Return = & http.Return {Code : http .StatusInternalServerError }
104+ }
105+ locs = append (locs , buildLocations ... )
125106 continue
126107 }
127108
@@ -136,40 +117,32 @@ func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http.
136117
137118 // RequestRedirect and proxying are mutually exclusive.
138119 if r .Filters .RequestRedirect != nil {
139- loc .Return = createReturnValForRedirectFilter (r .Filters .RequestRedirect , listenerPort )
140- locs = append (locs , loc )
120+ ret := createReturnValForRedirectFilter (r .Filters .RequestRedirect , listenerPort )
121+ for i := range buildLocations {
122+ buildLocations [i ].Return = ret
123+ }
124+ locs = append (locs , buildLocations ... )
141125 continue
142126 }
143127
144- backendName := backendGroupName (r .BackendGroup )
145- loc .ProxySetHeaders = generateProxySetHeaders (r .Filters .RequestHeaderModifiers )
146-
147- if backendGroupNeedsSplit (r .BackendGroup ) {
148- loc .ProxyPass = createProxyPassForVar (backendName )
149- } else {
150- loc .ProxyPass = createProxyPass (backendName )
128+ proxySetHeaders := generateProxySetHeaders (r .Filters .RequestHeaderModifiers )
129+ for i := range buildLocations {
130+ buildLocations [i ].ProxySetHeaders = proxySetHeaders
151131 }
152132
153- locs = append (locs , loc )
133+ proxyPass := createProxyPass (r .BackendGroup )
134+ for i := range buildLocations {
135+ buildLocations [i ].ProxyPass = proxyPass
136+ }
137+ locs = append (locs , buildLocations ... )
154138 }
155139
156140 if len (matches ) > 0 {
157- // FIXME(sberman): De-dupe matches and associated locations
158- // so we don't need nginx/njs to perform unnecessary matching.
159- // https://github.com/nginxinc/nginx-kubernetes-gateway/issues/662
160- b , err := json .Marshal (matches )
161- if err != nil {
162- // panic is safe here because we should never fail to marshal the match unless we constructed it incorrectly.
163- panic (fmt .Errorf ("could not marshal http match: %w" , err ))
141+ matchesStr := convertMatchesToString (matches )
142+ for i := range extLocations {
143+ extLocations [i ].HTTPMatchVar = matchesStr
164144 }
165-
166- pathLoc := http.Location {
167- Path : rule .Path ,
168- Exact : rule .PathType == dataplane .PathTypeExact ,
169- HTTPMatchVar : string (b ),
170- }
171-
172- locs = append (locs , pathLoc )
145+ locs = append (locs , extLocations ... )
173146 }
174147 }
175148
@@ -180,6 +153,87 @@ func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http.
180153 return locs
181154}
182155
156+ // pathAndTypeMap contains a map of paths and any path types defined for that path
157+ // for example, {/foo: {exact: {}, prefix: {}}}
158+ type pathAndTypeMap map [string ]map [dataplane.PathType ]struct {}
159+
160+ // To calculate the maximum number of locations, we need to take into account the following:
161+ // 1. Each match rule for a path rule will have one location.
162+ // 2. Each path rule may have an additional location if it contains non-path-only matches.
163+ // 3. Each prefix path rule may have an additional location if it doesn't contain trailing slash.
164+ // 4. There may be an additional location for the default root path.
165+ // We also return a map of all paths and their types.
166+ func getMaxLocationCountAndPathMap (pathRules []dataplane.PathRule ) (int , pathAndTypeMap ) {
167+ maxLocs := 1
168+ pathsAndTypes := make (pathAndTypeMap )
169+ for _ , rule := range pathRules {
170+ maxLocs += len (rule .MatchRules ) + 2
171+ if pathsAndTypes [rule .Path ] == nil {
172+ pathsAndTypes [rule .Path ] = map [dataplane.PathType ]struct {}{
173+ rule .PathType : {},
174+ }
175+ } else {
176+ pathsAndTypes [rule.Path ][rule.PathType ] = struct {}{}
177+ }
178+ }
179+
180+ return maxLocs , pathsAndTypes
181+ }
182+
183+ func initializeExternalLocations (
184+ rule dataplane.PathRule ,
185+ pathsAndTypes pathAndTypeMap ,
186+ ) []http.Location {
187+ extLocations := make ([]http.Location , 0 , 2 )
188+ externalLocPath := createPath (rule )
189+ // If the path type is Prefix and doesn't contain a trailing slash, then we need a second location
190+ // that handles the Exact prefix case (if it doesn't already exist), and the first location is updated
191+ // to handle the trailing slash prefix case (if it doesn't already exist)
192+ if isNonSlashedPrefixPath (rule .PathType , externalLocPath ) {
193+ // if Exact path and/or trailing slash Prefix path already exists, this means some routing rule
194+ // configures it. The routing rule location has priority over this location, so we don't try to
195+ // overwrite it and we don't add a duplicate location to NGINX because that will cause an NGINX config error.
196+ _ , exactPathExists := pathsAndTypes [rule.Path ][dataplane.PathTypeExact ]
197+ var trailingSlashPrefixPathExists bool
198+ if pathTypes , exists := pathsAndTypes [rule .Path + "/" ]; exists {
199+ _ , trailingSlashPrefixPathExists = pathTypes [dataplane .PathTypePrefix ]
200+ }
201+
202+ if exactPathExists && trailingSlashPrefixPathExists {
203+ return []http.Location {}
204+ }
205+
206+ if ! trailingSlashPrefixPathExists {
207+ externalLocTrailing := http.Location {
208+ Path : externalLocPath + "/" ,
209+ }
210+ extLocations = append (extLocations , externalLocTrailing )
211+ }
212+ if ! exactPathExists {
213+ externalLocExact := http.Location {
214+ Path : exactPath (externalLocPath ),
215+ }
216+ extLocations = append (extLocations , externalLocExact )
217+ }
218+ } else {
219+ externalLoc := http.Location {
220+ Path : externalLocPath ,
221+ }
222+ extLocations = []http.Location {externalLoc }
223+ }
224+
225+ return extLocations
226+ }
227+
228+ func initializeInternalLocation (
229+ rule dataplane.PathRule ,
230+ matchRuleIdx int ,
231+ match v1beta1.HTTPRouteMatch ,
232+ ) (http.Location , httpMatch ) {
233+ path := createPathForMatch (rule .Path , rule .PathType , matchRuleIdx )
234+ return createMatchLocation (path ), createHTTPMatch (match , path )
235+ }
236+
183237func createReturnValForRedirectFilter (filter * v1beta1.HTTPRequestRedirectFilter , listenerPort int32 ) * http.Return {
184238 if filter == nil {
185239 return nil
@@ -308,12 +362,13 @@ func isPathOnlyMatch(match v1beta1.HTTPRouteMatch) bool {
308362 return match .Method == nil && match .Headers == nil && match .QueryParams == nil
309363}
310364
311- func createProxyPass (address string ) string {
312- return "http://" + address
313- }
365+ func createProxyPass (backendGroup dataplane.BackendGroup ) string {
366+ backendName := backendGroupName (backendGroup )
367+ if backendGroupNeedsSplit (backendGroup ) {
368+ return "http://$" + convertStringToSafeVariableName (backendName )
369+ }
314370
315- func createProxyPassForVar (variable string ) string {
316- return "http://$" + convertStringToSafeVariableName (variable )
371+ return "http://" + backendName
317372}
318373
319374func createMatchLocation (path string ) http.Location {
@@ -369,6 +424,33 @@ func convertSetHeaders(headers []dataplane.HTTPHeader) []http.Header {
369424 return locHeaders
370425}
371426
427+ func convertMatchesToString (matches []httpMatch ) string {
428+ // FIXME(sberman): De-dupe matches and associated locations
429+ // so we don't need nginx/njs to perform unnecessary matching.
430+ // https://github.com/nginxinc/nginx-kubernetes-gateway/issues/662
431+ b , err := json .Marshal (matches )
432+ if err != nil {
433+ // panic is safe here because we should never fail to marshal the match unless we constructed it incorrectly.
434+ panic (fmt .Errorf ("could not marshal http match: %w" , err ))
435+ }
436+
437+ return string (b )
438+ }
439+
440+ func exactPath (path string ) string {
441+ return fmt .Sprintf ("= %s" , path )
442+ }
443+
444+ // createPath builds the location path depending on the path type.
445+ func createPath (rule dataplane.PathRule ) string {
446+ switch rule .PathType {
447+ case dataplane .PathTypeExact :
448+ return exactPath (rule .Path )
449+ default :
450+ return rule .Path
451+ }
452+ }
453+
372454func createPathForMatch (path string , pathType dataplane.PathType , routeIdx int ) string {
373455 return fmt .Sprintf ("%s_%s_route%d" , path , pathType , routeIdx )
374456}
@@ -379,3 +461,8 @@ func createDefaultRootLocation() http.Location {
379461 Return : & http.Return {Code : http .StatusNotFound },
380462 }
381463}
464+
465+ // isNonSlashedPrefixPath returns whether or not a path is of type Prefix and does not contain a trailing slash
466+ func isNonSlashedPrefixPath (pathType dataplane.PathType , path string ) bool {
467+ return pathType == dataplane .PathTypePrefix && ! strings .HasSuffix (path , "/" )
468+ }
0 commit comments