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 10 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
39 changes: 31 additions & 8 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 + "/match.json"
salonichf5 marked this conversation as resolved.
Show resolved Hide resolved
)

// 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 {
func (g GeneratorImpl) generateHTTPConfig(conf dataplane.Configuration) []file.File {
files := make([]file.File, 0)
salonichf5 marked this conversation as resolved.
Show resolved Hide resolved
var c []byte

for _, execute := range g.getExecuteFuncs() {
c = append(c, execute(conf)...)
results := execute(conf)
for _, res := range results {
if res.dest == httpConfigFile {
salonichf5 marked this conversation as resolved.
Show resolved Hide resolved
c = append(c, res.data...)
} else {
files = append(files, file.File{
Path: res.dest,
Content: res.data,
Type: file.TypeRegular,
})
}
}
}

return file.File{
Content: c,
files = append(files, file.File{
Path: httpConfigFile,
Content: c,
Type: file.TypeRegular,
}
})

return files
}

func (g GeneratorImpl) getExecuteFuncs() []executeFunc {
return []executeFunc{
executeServers,
g.executeUpstreams,
executeSplitClients,
executeServers,
executeMaps,
}
}
Expand Down
21 changes: 13 additions & 8 deletions internal/mode/static/nginx/config/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,30 +70,35 @@ 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,
Path: "/etc/nginx/secrets/test-keypair.pem",
Content: []byte("test-cert\ntest-key"),
}))

g.Expect(files[1].Path).To(Equal("/etc/nginx/conf.d/match.json"))
g.Expect(files[1].Type).To(Equal(file.TypeRegular))
g.Expect(files[1].Path).To(Equal("/etc/nginx/conf.d/http.conf"))
httpCfg := string(files[1].Content) // converting to string so that on failure gomega prints strings not byte arrays
expString := "{}"
g.Expect(string(files[1].Content)).To(Equal(expString))

g.Expect(files[2].Type).To(Equal(file.TypeRegular))
g.Expect(files[2].Path).To(Equal("/etc/nginx/conf.d/http.conf"))
httpCfg := string(files[2].Content) // converting to string so that on failure gomega prints strings not byte arrays
// Note: this only verifies that Generate() returns a byte array with upstream, server, and split_client blocks.
// It does not test the correctness of those blocks. That functionality is covered by other tests in this package.
g.Expect(httpCfg).To(ContainSubstring("listen 80"))
g.Expect(httpCfg).To(ContainSubstring("listen 443"))
g.Expect(httpCfg).To(ContainSubstring("upstream"))
g.Expect(httpCfg).To(ContainSubstring("split_clients"))

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)
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
3 changes: 2 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,8 @@ 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
for expSubStr, expCount := range expSubStrings {
g.Expect(expCount).To(Equal(strings.Count(maps, expSubStr)))
}
Expand Down
112 changes: 69 additions & 43 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, matchPair := createServer(s, serverID)
salonichf5 marked this conversation as resolved.
Show resolved Hide resolved
servers = append(servers, httpServer)
maps.Copy(finalMatchPairs, matchPair)
}

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 @@ -397,7 +436,7 @@
// 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.
// If the request satisfies the httpMatch, NGINX will redirect the request to the location RedirectPath.
type httpMatch struct {
type RouteMatch struct {
salonichf5 marked this conversation as resolved.
Show resolved Hide resolved
// 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