Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fix reload errors due long matching conditions #1829

Merged
merged 29 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3cbc1ec
fix: fix reload errors due long matching conditions
salonichf5 Apr 9, 2024
26d766a
add redirect to export list
salonichf5 Apr 17, 2024
f0da36d
update based on reviews
salonichf5 Apr 22, 2024
a0e31b0
remove print statements
salonichf5 Apr 22, 2024
44c632d
fix unit test
salonichf5 Apr 22, 2024
e40a5cc
update unit tests
salonichf5 Apr 22, 2024
d96fcaa
Update internal/mode/static/nginx/config/generator.go
salonichf5 Apr 22, 2024
7897819
Update internal/mode/static/nginx/config/servers.go
salonichf5 Apr 22, 2024
81b5655
update key for match.json
salonichf5 Apr 22, 2024
6153ddb
format njs code
salonichf5 Apr 24, 2024
75cdb8f
Update internal/mode/static/nginx/config/generator.go
salonichf5 Apr 24, 2024
4ad2de1
Update internal/mode/static/nginx/config/servers.go
salonichf5 Apr 24, 2024
d0afa69
review comments
salonichf5 Apr 24, 2024
8f117c7
improve httpmatches.js
salonichf5 Apr 25, 2024
5ca49f7
address reviews
salonichf5 Apr 25, 2024
3683dd7
add more unit test coverage for njs unit tests
salonichf5 Apr 25, 2024
6e7e79e
Update internal/mode/static/nginx/config/servers.go
salonichf5 Apr 25, 2024
b459205
Update internal/mode/static/nginx/config/servers.go
salonichf5 Apr 25, 2024
02653ce
Update internal/mode/static/nginx/config/servers_test.go
salonichf5 Apr 25, 2024
7bf61f3
Update internal/mode/static/nginx/config/servers_test.go
salonichf5 Apr 25, 2024
0a4a7ac
Update internal/mode/static/nginx/config/servers.go
salonichf5 Apr 25, 2024
f63f317
Update internal/mode/static/nginx/config/servers.go
salonichf5 Apr 25, 2024
cc9a19e
update based on reviews
salonichf5 Apr 25, 2024
27f9e53
rearrange expect statements to avoid panic
salonichf5 Apr 25, 2024
c637c6d
fix unit test
salonichf5 Apr 25, 2024
07fbf39
fix httpmatches
salonichf5 Apr 29, 2024
2dcb7c4
update error message
salonichf5 Apr 29, 2024
b744b70
Merge branch 'main' into bug/http-match
salonichf5 Apr 29, 2024
377d9f7
Merge branch 'main' into bug/http-match
salonichf5 Apr 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 33 additions & 10 deletions internal/mode/static/nginx/config/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ const (

// configVersionFile is the path to the config version configuration file.
configVersionFile = httpFolder + "/config-version.conf"

// httpMatchVarsFile is the path to the http_match pairs configuration file.
httpMatchVarsFile = httpFolder + "/matches.json"
)

// ConfigFolders is a list of folders where NGINX configuration files are stored.
Expand Down Expand Up @@ -52,8 +55,13 @@ func NewGeneratorImpl(plus bool) GeneratorImpl {
return GeneratorImpl{plus: plus}
}

type executeResult struct {
dest string
data []byte
}

// executeFunc is a function that generates NGINX configuration from internal representation.
type executeFunc func(configuration dataplane.Configuration) []byte
type executeFunc func(configuration dataplane.Configuration) []executeResult

// Generate generates NGINX configuration files from internal representation.
// It is the responsibility of the caller to validate the configuration before calling this function.
Expand All @@ -66,7 +74,7 @@ func (g GeneratorImpl) Generate(conf dataplane.Configuration) []file.File {
files = append(files, generatePEM(id, pair.Cert, pair.Key))
}

files = append(files, g.generateHTTPConfig(conf))
files = append(files, g.generateHTTPConfig(conf)...)

files = append(files, generateConfigVersion(conf.Version))

Expand Down Expand Up @@ -106,24 +114,39 @@ func generateCertBundleFileName(id dataplane.CertBundleID) string {
return filepath.Join(secretsFolder, string(id)+".crt")
}

func (g GeneratorImpl) generateHTTPConfig(conf dataplane.Configuration) file.File {
var c []byte
func (g GeneratorImpl) generateHTTPConfig(conf dataplane.Configuration) []file.File {
files := make([]file.File, 0)
salonichf5 marked this conversation as resolved.
Show resolved Hide resolved
fileBytes := make(map[string][]byte)

for _, execute := range g.getExecuteFuncs() {
c = append(c, execute(conf)...)
results := execute(conf)
for _, res := range results {
b, ok := fileBytes[res.dest]
salonichf5 marked this conversation as resolved.
Show resolved Hide resolved
if ok {
b = append(b, res.data...)
fileBytes[res.dest] = b
} else {
fileBytes[res.dest] = res.data
}
}
}

return file.File{
Content: c,
Path: httpConfigFile,
Type: file.TypeRegular,
for filepath, bytes := range fileBytes {
files = append(files, file.File{
Path: filepath,
Content: bytes,
Type: file.TypeRegular,
})
}

return files
}

func (g GeneratorImpl) getExecuteFuncs() []executeFunc {
return []executeFunc{
executeServers,
g.executeUpstreams,
executeSplitClients,
executeServers,
executeMaps,
}
}
Expand Down
15 changes: 10 additions & 5 deletions internal/mode/static/nginx/config/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func TestGenerate(t *testing.T) {

files := generator.Generate(conf)

g.Expect(files).To(HaveLen(4))
g.Expect(files).To(HaveLen(5))

g.Expect(files[0]).To(Equal(file.File{
Type: file.TypeSecret,
Expand All @@ -88,12 +88,17 @@ func TestGenerate(t *testing.T) {
g.Expect(httpCfg).To(ContainSubstring("upstream"))
g.Expect(httpCfg).To(ContainSubstring("split_clients"))

g.Expect(files[2].Path).To(Equal("/etc/nginx/conf.d/matches.json"))
salonichf5 marked this conversation as resolved.
Show resolved Hide resolved
g.Expect(files[2].Type).To(Equal(file.TypeRegular))
g.Expect(files[2].Path).To(Equal("/etc/nginx/conf.d/config-version.conf"))
configVersion := string(files[2].Content)
expString := "{}"
g.Expect(string(files[2].Content)).To(Equal(expString))

g.Expect(files[3].Type).To(Equal(file.TypeRegular))
g.Expect(files[3].Path).To(Equal("/etc/nginx/conf.d/config-version.conf"))
configVersion := string(files[3].Content)
g.Expect(configVersion).To(ContainSubstring(fmt.Sprintf("return 200 %d", conf.Version)))

g.Expect(files[3].Path).To(Equal("/etc/nginx/secrets/test-certbundle.crt"))
certBundle := string(files[3].Content)
g.Expect(files[4].Path).To(Equal("/etc/nginx/secrets/test-certbundle.crt"))
certBundle := string(files[4].Content)
g.Expect(certBundle).To(Equal("test-cert"))
}
8 changes: 4 additions & 4 deletions internal/mode/static/nginx/config/http/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ type Server struct {

// Location holds all configuration for an HTTP location.
type Location struct {
Return *Return
ProxySSLVerify *ProxySSLVerify
Path string
ProxyPass string
HTTPMatchVar string
Rewrites []string
HTTPMatchKey string
ProxySetHeaders []Header
ProxySSLVerify *ProxySSLVerify
Return *Return
Rewrites []string
}

// Header defines a HTTP header to be passed to the proxied server.
Expand Down
8 changes: 6 additions & 2 deletions internal/mode/static/nginx/config/maps.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ import (

var mapsTemplate = gotemplate.Must(gotemplate.New("maps").Parse(mapsTemplateText))

func executeMaps(conf dataplane.Configuration) []byte {
func executeMaps(conf dataplane.Configuration) []executeResult {
maps := buildAddHeaderMaps(append(conf.HTTPServers, conf.SSLServers...))
return execute(mapsTemplate, maps)
result := executeResult{
dest: httpConfigFile,
data: execute(mapsTemplate, maps),
}
return []executeResult{result}
}

func buildAddHeaderMaps(servers []dataplane.VirtualServer) []http.Map {
Expand Down
5 changes: 4 additions & 1 deletion internal/mode/static/nginx/config/maps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ func TestExecuteMaps(t *testing.T) {
"map $http_upgrade $connection_upgrade {": 1,
}

maps := string(executeMaps(conf))
mapResult := executeMaps(conf)
maps := string(mapResult[0].data)
salonichf5 marked this conversation as resolved.
Show resolved Hide resolved
salonichf5 marked this conversation as resolved.
Show resolved Hide resolved
g.Expect(mapResult).To(HaveLen(1))
g.Expect(mapResult[0].dest).To(Equal(httpConfigFile))
for expSubStr, expCount := range expSubStrings {
g.Expect(expCount).To(Equal(strings.Count(maps, expSubStr)))
}
Expand Down
120 changes: 73 additions & 47 deletions internal/mode/static/nginx/config/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import (
"encoding/json"
"fmt"
"maps"
"strconv"
"strings"
gotemplate "text/template"

Expand Down Expand Up @@ -38,58 +40,84 @@
},
}

func executeServers(conf dataplane.Configuration) []byte {
servers := createServers(conf.HTTPServers, conf.SSLServers)
func executeServers(conf dataplane.Configuration) []executeResult {
servers, httpMatchPairs := createServers(conf.HTTPServers, conf.SSLServers)

return execute(serversTemplate, servers)
serverResult := executeResult{
dest: httpConfigFile,
data: execute(serversTemplate, servers),
}

// create httpMatchPair conf
httpMatchConf, err := json.Marshal(httpMatchPairs)
if err != nil {
// panic is safe here because we should never fail to marshal the match unless we constructed it incorrectly.
panic(fmt.Errorf("could not marshal http match pairs: %w", err))

Check warning on line 55 in internal/mode/static/nginx/config/servers.go

View check run for this annotation

Codecov / codecov/patch

internal/mode/static/nginx/config/servers.go#L55

Added line #L55 was not covered by tests
}

httpMatchResult := executeResult{
dest: httpMatchVarsFile,
data: httpMatchConf,
}

return []executeResult{serverResult, httpMatchResult}
}

func createServers(httpServers, sslServers []dataplane.VirtualServer) []http.Server {
func createServers(httpServers, sslServers []dataplane.VirtualServer) ([]http.Server, httpMatchPairs) {
servers := make([]http.Server, 0, len(httpServers)+len(sslServers))
finalMatchPairs := make(httpMatchPairs)

for _, s := range httpServers {
servers = append(servers, createServer(s))
for serverID, s := range httpServers {
httpServer, matchPairs := createServer(s, serverID)
servers = append(servers, httpServer)
maps.Copy(finalMatchPairs, matchPairs)
}

for _, s := range sslServers {
servers = append(servers, createSSLServer(s))
for serverID, s := range sslServers {
sslServer, matchPair := createSSLServer(s, serverID)
servers = append(servers, sslServer)
maps.Copy(finalMatchPairs, matchPair)
}

return servers
return servers, finalMatchPairs
}

func createSSLServer(virtualServer dataplane.VirtualServer) http.Server {
func createSSLServer(virtualServer dataplane.VirtualServer, serverID int) (http.Server, httpMatchPairs) {
if virtualServer.IsDefault {
return http.Server{
IsDefaultSSL: true,
Port: virtualServer.Port,
}
}, nil
}

locs, matchPairs := createLocations(&virtualServer, serverID)

return http.Server{
ServerName: virtualServer.Hostname,
SSL: &http.SSL{
Certificate: generatePEMFileName(virtualServer.SSL.KeyPairID),
CertificateKey: generatePEMFileName(virtualServer.SSL.KeyPairID),
},
Locations: createLocations(virtualServer.PathRules, virtualServer.Port),
Locations: locs,
Port: virtualServer.Port,
}
}, matchPairs
}

func createServer(virtualServer dataplane.VirtualServer) http.Server {
func createServer(virtualServer dataplane.VirtualServer, serverID int) (http.Server, httpMatchPairs) {
if virtualServer.IsDefault {
return http.Server{
IsDefaultHTTP: true,
Port: virtualServer.Port,
}
}, nil
}

locs, matchPairs := createLocations(&virtualServer, serverID)

return http.Server{
ServerName: virtualServer.Hostname,
Locations: createLocations(virtualServer.PathRules, virtualServer.Port),
Locations: locs,
Port: virtualServer.Port,
}
}, matchPairs
}

// rewriteConfig contains the configuration for a location to rewrite paths,
Expand All @@ -99,13 +127,16 @@
Rewrite string
}

func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http.Location {
maxLocs, pathsAndTypes := getMaxLocationCountAndPathMap(pathRules)
type httpMatchPairs map[string][]routeMatch

func createLocations(server *dataplane.VirtualServer, serverID int) ([]http.Location, httpMatchPairs) {
maxLocs, pathsAndTypes := getMaxLocationCountAndPathMap(server.PathRules)
locs := make([]http.Location, 0, maxLocs)
matchPairs := make(httpMatchPairs)
var rootPathExists bool

for pathRuleIdx, rule := range pathRules {
matches := make([]httpMatch, 0, len(rule.MatchRules))
for pathRuleIdx, rule := range server.PathRules {
matches := make([]routeMatch, 0, len(rule.MatchRules))

if rule.Path == rootPath {
rootPathExists = true
Expand All @@ -121,14 +152,22 @@
matches = append(matches, match)
}

buildLocations = updateLocationsForFilters(r.Filters, buildLocations, r, listenerPort, rule.Path)
buildLocations = updateLocationsForFilters(r.Filters, buildLocations, r, server.Port, rule.Path)
locs = append(locs, buildLocations...)
}

if len(matches) > 0 {
matchesStr := convertMatchesToString(matches)
for i := range extLocations {
extLocations[i].HTTPMatchVar = matchesStr
// FIXME(sberman): De-dupe matches and associated locations
// so we don't need nginx/njs to perform unnecessary matching.
// https://github.com/nginxinc/nginx-gateway-fabric/issues/662
var key string
if server.SSL != nil {
key = "SSL"
}
key += strconv.Itoa(serverID) + "_" + strconv.Itoa(pathRuleIdx)
extLocations[i].HTTPMatchKey = key
matchPairs[extLocations[i].HTTPMatchKey] = matches
}
locs = append(locs, extLocations...)
}
Expand All @@ -138,7 +177,7 @@
locs = append(locs, createDefaultRootLocation())
}

return locs
return locs, matchPairs
}

// pathAndTypeMap contains a map of paths and any path types defined for that path
Expand Down Expand Up @@ -217,9 +256,9 @@
pathruleIdx,
matchRuleIdx int,
match dataplane.Match,
) (http.Location, httpMatch) {
) (http.Location, routeMatch) {
path := fmt.Sprintf("@rule%d-route%d", pathruleIdx, matchRuleIdx)
return createMatchLocation(path), createHTTPMatch(match, path)
return createMatchLocation(path), createRouteMatch(match, path)
}

// updateLocationsForFilters updates the existing locations with any relevant filters.
Expand Down Expand Up @@ -392,12 +431,12 @@
return rewrites
}

// httpMatch is an internal representation of an HTTPRouteMatch.
// This struct is marshaled into a string and stored as a variable in the nginx location block for the route's path.
// The NJS httpmatches module will look up this variable on the request object and compare the request against the
// Method, Headers, and QueryParams contained in httpMatch.
// routeMatch is an internal representation of an HTTPRouteMatch.
salonichf5 marked this conversation as resolved.
Show resolved Hide resolved
// This struct is stored as a key-value pair in /etc/nginx/conf.d/matches.json with a key for the route's path.
// The NJS httpmatches module will look up key specified in the nginx location on the request object
// and compare the request against the Method, Headers, and QueryParams contained in httpMatch.
// If the request satisfies the httpMatch, NGINX will redirect the request to the location RedirectPath.
type httpMatch struct {
type routeMatch struct {
// Method is the HTTPMethod of the HTTPRouteMatch.
Method string `json:"method,omitempty"`
// RedirectPath is the path to redirect the request to if the request satisfies the match conditions.
Expand All @@ -410,8 +449,8 @@
Any bool `json:"any,omitempty"`
}

func createHTTPMatch(match dataplane.Match, redirectPath string) httpMatch {
hm := httpMatch{
func createRouteMatch(match dataplane.Match, redirectPath string) routeMatch {
hm := routeMatch{
RedirectPath: redirectPath,
}

Expand Down Expand Up @@ -558,19 +597,6 @@
return locHeaders
}

func convertMatchesToString(matches []httpMatch) string {
// FIXME(sberman): De-dupe matches and associated locations
// so we don't need nginx/njs to perform unnecessary matching.
// https://github.com/nginxinc/nginx-gateway-fabric/issues/662
salonichf5 marked this conversation as resolved.
Show resolved Hide resolved
b, err := json.Marshal(matches)
if err != nil {
// panic is safe here because we should never fail to marshal the match unless we constructed it incorrectly.
panic(fmt.Errorf("could not marshal http match: %w", err))
}

return string(b)
}

func exactPath(path string) string {
return fmt.Sprintf("= %s", path)
}
Expand Down
Loading
Loading