Skip to content

Commit c70940d

Browse files
committed
Support GatewayClass resource (nginx#136)
This commit brings support for a GatewayClass resource. NGINX Gateway supports a single GatewayClass resource that must be configured via a new cli argument -- gatewayclass. The GatewayClass resource is required - no traffic will go through NGINX if the GatewayClass is missing.
1 parent 12581b7 commit c70940d

27 files changed

+1233
-294
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ You can deploy NGINX Kubernetes Gateway on an existing Kubernetes 1.16+ cluster.
7272
kubectl create configmap njs-modules --from-file=internal/nginx/modules/src/httpmatches.js -n nginx-gateway
7373
```
7474
75+
1. Create the GatewayClass resource:
76+
77+
```
78+
kubectl apply -f deploy/manifests/gatewayclass.yaml
79+
```
80+
7581
1. Deploy the NGINX Kubernetes Gateway:
7682
7783
Before deploying, make sure to update the Deployment spec in `nginx-gateway.yaml` to reference the image you built.

cmd/gateway/main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ var (
2828
"",
2929
fmt.Sprintf("The name of the Gateway controller. The controller name must be of the form: DOMAIN/NAMESPACE/NAME. The controller's domain is '%s'.", domain),
3030
)
31+
32+
gatewayClassName = flag.String(
33+
"gatewayclass",
34+
"",
35+
"The name of the GatewayClass resource. Every NGINX Gateway must have a unique corresponding GatewayClass resource")
3136
)
3237

3338
func main() {
@@ -42,11 +47,13 @@ func main() {
4247
Namespace: "nginx-gateway",
4348
Name: "gateway",
4449
},
50+
GatewayClassName: *gatewayClassName,
4551
}
4652

4753
MustValidateArguments(
4854
flag.CommandLine,
4955
GatewayControllerParam(domain, "nginx-gateway" /* FIXME(f5yacobucci) dynamically set */),
56+
GatewayClassParam(),
5057
)
5158

5259
logger.Info("Starting NGINX Kubernetes Gateway",

cmd/gateway/setup.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88

99
flag "github.com/spf13/pflag"
10+
"k8s.io/apimachinery/pkg/util/validation"
1011
)
1112

1213
const (
@@ -65,6 +66,32 @@ func GatewayControllerParam(domain string, namespace string) ValidatorContext {
6566
}
6667
}
6768

69+
func GatewayClassParam() ValidatorContext {
70+
name := "gatewayclass"
71+
return ValidatorContext{
72+
name,
73+
func(flagset *flag.FlagSet) error {
74+
param, err := flagset.GetString(name)
75+
if err != nil {
76+
return err
77+
}
78+
79+
if len(param) == 0 {
80+
return errors.New("flag must be set")
81+
}
82+
83+
// used by Kubernetes to validate resource names
84+
messages := validation.IsDNS1123Subdomain(param)
85+
if len(messages) > 0 {
86+
msg := strings.Join(messages, "; ")
87+
return fmt.Errorf("invalid format: %s", msg)
88+
}
89+
90+
return nil
91+
},
92+
}
93+
}
94+
6895
func ValidateArguments(flagset *flag.FlagSet, validators ...ValidatorContext) []string {
6996
var msgs []string
7097
for _, v := range validators {

cmd/gateway/setup_test.go

Lines changed: 122 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import (
1010
. "github.com/nginxinc/nginx-kubernetes-gateway/cmd/gateway"
1111
)
1212

13-
var domain string
14-
1513
func MockValidator(name string, called *int, succeed bool) ValidatorContext {
1614
return ValidatorContext{
1715
name,
@@ -122,22 +120,25 @@ var _ = Describe("Main", func() {
122120

123121
Describe("CLI argument validation", func() {
124122
type testCase struct {
125-
Param string
126-
Domain string
127-
ExpError bool
123+
Flag string
124+
Value string
125+
ValidatorContext ValidatorContext
126+
ExpError bool
128127
}
129128

129+
const (
130+
expectError = true
131+
expectSuccess = false
132+
)
133+
130134
var mockFlags *flag.FlagSet
131-
var gatewayCtlrName string
132135

133136
tester := func(t testCase) {
134-
err := mockFlags.Set(gatewayCtlrName, t.Param)
137+
err := mockFlags.Set(t.Flag, t.Value)
135138
Expect(err).ToNot(HaveOccurred())
136139

137-
v := GatewayControllerParam(domain, t.Domain)
138-
Expect(v.V).ToNot(BeNil())
140+
err = t.ValidatorContext.V(mockFlags)
139141

140-
err = v.V(mockFlags)
141142
if t.ExpError {
142143
Expect(err).To(HaveOccurred())
143144
} else {
@@ -150,88 +151,122 @@ var _ = Describe("Main", func() {
150151
}
151152
}
152153

153-
BeforeEach(func() {
154-
domain = "k8s-gateway.nginx.org"
155-
gatewayCtlrName = "gateway-ctlr-name"
156-
157-
mockFlags = flag.NewFlagSet("mock", flag.PanicOnError)
158-
_ = mockFlags.String("gateway-ctlr-name", "", "mock gateway-ctlr-name")
159-
err := mockFlags.Parse([]string{})
160-
Expect(err).ToNot(HaveOccurred())
161-
})
162-
AfterEach(func() {
163-
mockFlags = nil
164-
})
165-
It("should parse full gateway-ctlr-name", func() {
166-
t := testCase{
167-
"k8s-gateway.nginx.org/nginx-gateway/my-gateway",
168-
"nginx-gateway",
169-
false,
170-
}
171-
tester(t)
172-
}) // should parse full gateway-ctlr-name
173-
174-
It("should fail with too many path elements", func() {
175-
t := testCase{
176-
"k8s-gateway.nginx.org/nginx-gateway/my-gateway/broken",
177-
"nginx-gateway",
178-
true,
154+
Describe("gateway-ctlr-name validation", func() {
155+
prepareTestCase := func(value string, expError bool) testCase {
156+
return testCase{
157+
Flag: "gateway-ctlr-name",
158+
Value: value,
159+
ValidatorContext: GatewayControllerParam("k8s-gateway.nginx.org", "nginx-gateway"),
160+
ExpError: expError,
161+
}
179162
}
180-
tester(t)
181-
}) // should fail with too many path elements
182163

183-
It("should fail with too few path elements", func() {
184-
table := []testCase{
185-
{
186-
Param: "nginx-gateway/my-gateway",
187-
Domain: "nginx-gateway",
188-
ExpError: true,
189-
},
190-
{
191-
Param: "my-gateway",
192-
Domain: "nginx-gateway",
193-
ExpError: true,
194-
},
195-
}
164+
BeforeEach(func() {
165+
mockFlags = flag.NewFlagSet("mock", flag.PanicOnError)
166+
_ = mockFlags.String("gateway-ctlr-name", "", "mock gateway-ctlr-name")
167+
err := mockFlags.Parse([]string{})
168+
Expect(err).ToNot(HaveOccurred())
169+
})
170+
AfterEach(func() {
171+
mockFlags = nil
172+
})
196173

197-
runner(table)
198-
}) // should fail with too few path elements
174+
It("should parse full gateway-ctlr-name", func() {
175+
t := prepareTestCase(
176+
"k8s-gateway.nginx.org/nginx-gateway/my-gateway",
177+
expectSuccess,
178+
)
179+
tester(t)
180+
}) // should parse full gateway-ctlr-name
199181

200-
It("should verify constraints", func() {
201-
table := []testCase{
202-
{
203-
// bad domain
204-
Param: "invalid-domain/nginx-gateway/my-gateway",
205-
Domain: "nginx-gateway",
206-
ExpError: true,
207-
},
208-
{
209-
// bad domain
210-
Param: "/default/my-gateway",
211-
Domain: "nginx-gateway",
212-
ExpError: true,
213-
},
214-
{
215-
// bad namespace
216-
Param: "k8s-gateway.nginx.org/default/my-gateway",
217-
Domain: "nginx-gateway",
218-
ExpError: true,
219-
},
220-
{
221-
// bad namespace
222-
Param: "k8s-gateway.nginx.org//my-gateway",
223-
Domain: "nginx-gateway",
224-
ExpError: true,
225-
},
226-
{
227-
// bad name
228-
Param: "k8s-gateway.nginx.org/default/",
229-
Domain: "nginx-gateway",
230-
ExpError: true,
231-
},
182+
It("should fail with too many path elements", func() {
183+
t := prepareTestCase(
184+
"k8s-gateway.nginx.org/nginx-gateway/my-gateway/broken",
185+
expectError)
186+
tester(t)
187+
}) // should fail with too many path elements
188+
189+
It("should fail with too few path elements", func() {
190+
table := []testCase{
191+
prepareTestCase(
192+
"nginx-gateway/my-gateway",
193+
expectError,
194+
),
195+
prepareTestCase(
196+
"my-gateway",
197+
expectError,
198+
),
199+
}
200+
201+
runner(table)
202+
}) // should fail with too few path elements
203+
204+
It("should verify constraints", func() {
205+
table := []testCase{
206+
prepareTestCase(
207+
// bad domain
208+
"invalid-domain/nginx-gateway/my-gateway",
209+
expectError,
210+
),
211+
prepareTestCase(
212+
// bad domain
213+
"/default/my-gateway",
214+
expectError,
215+
),
216+
prepareTestCase(
217+
// bad namespace
218+
"k8s-gateway.nginx.org/default/my-gateway",
219+
expectError),
220+
prepareTestCase(
221+
// bad namespace
222+
"k8s-gateway.nginx.org//my-gateway",
223+
expectError,
224+
),
225+
prepareTestCase(
226+
// bad name
227+
"k8s-gateway.nginx.org/default/",
228+
expectError,
229+
),
230+
}
231+
232+
runner(table)
233+
}) // should verify constraints
234+
}) // gateway-ctlr-name validation
235+
236+
Describe("gatewayclass validation", func() {
237+
prepareTestCase := func(value string, expError bool) testCase {
238+
return testCase{
239+
Flag: "gatewayclass",
240+
Value: value,
241+
ValidatorContext: GatewayClassParam(),
242+
ExpError: expError,
243+
}
232244
}
233245

234-
runner(table)
235-
}) // should verify constraints
246+
BeforeEach(func() {
247+
mockFlags = flag.NewFlagSet("mock", flag.PanicOnError)
248+
_ = mockFlags.String("gatewayclass", "", "mock gatewayclass")
249+
err := mockFlags.Parse([]string{})
250+
Expect(err).ToNot(HaveOccurred())
251+
})
252+
AfterEach(func() {
253+
mockFlags = nil
254+
})
255+
256+
It("should succeed on valid name", func() {
257+
t := prepareTestCase(
258+
"nginx",
259+
expectSuccess,
260+
)
261+
tester(t)
262+
}) // should succeed on valid name
263+
264+
It("should fail with invalid name", func() {
265+
t := prepareTestCase(
266+
"$nginx",
267+
expectError)
268+
tester(t)
269+
}) // should fail with invalid name"
270+
}) // gatewayclass validation
236271
}) // CLI argument validation
237272
}) // end Main

deploy/manifests/nginx-gateway.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ rules:
3737
resources:
3838
- httproutes/status
3939
- gateways/status
40+
- gatewayclasses/status
4041
verbs:
4142
- update
4243
---
@@ -97,6 +98,7 @@ spec:
9798
# Note: CAP_KILL is needed for sending HUP signal to NGINX main process
9899
args:
99100
- --gateway-ctlr-name=k8s-gateway.nginx.org/nginx-gateway/gateway
101+
- --gatewayclass=nginx
100102
- image: nginx:1.21.3
101103
imagePullPolicy: IfNotPresent
102104
name: nginx

internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ type Config struct {
1111
// GatewayNsName is the namespaced name of a Gateway resource that the Gateway will use.
1212
// The Gateway will ignore all other Gateway resources.
1313
GatewayNsName types.NamespacedName
14+
// GatewayClassName is the name of the GatewayClass resource that the Gateway will use.
15+
GatewayClassName string
1416
}

internal/events/loop.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ func (el *EventLoop) updateNginx(ctx context.Context, conf state.Configuration)
118118

119119
func (el *EventLoop) propagateUpsert(e *UpsertEvent) {
120120
switch r := e.Resource.(type) {
121+
case *v1alpha2.GatewayClass:
122+
el.processor.CaptureUpsertChange(r)
121123
case *v1alpha2.Gateway:
122124
el.processor.CaptureUpsertChange(r)
123125
case *v1alpha2.HTTPRoute:
@@ -132,6 +134,8 @@ func (el *EventLoop) propagateUpsert(e *UpsertEvent) {
132134

133135
func (el *EventLoop) propagateDelete(e *DeleteEvent) {
134136
switch e.Type.(type) {
137+
case *v1alpha2.GatewayClass:
138+
el.processor.CaptureDeleteChange(e.Type, e.NamespacedName)
135139
case *v1alpha2.Gateway:
136140
el.processor.CaptureDeleteChange(e.Type, e.NamespacedName)
137141
case *v1alpha2.HTTPRoute:

internal/events/loop_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ var _ = Describe("EventLoop", func() {
112112
},
113113
Entry("HTTPRoute", &events.UpsertEvent{Resource: &v1alpha2.HTTPRoute{}}),
114114
Entry("Gateway", &events.UpsertEvent{Resource: &v1alpha2.Gateway{}}),
115+
Entry("GatewayClass", &events.UpsertEvent{Resource: &v1alpha2.GatewayClass{}}),
115116
)
116117

117118
DescribeTable("Delete events",
@@ -141,6 +142,7 @@ var _ = Describe("EventLoop", func() {
141142
},
142143
Entry("HTTPRoute", &events.DeleteEvent{Type: &v1alpha2.HTTPRoute{}, NamespacedName: types.NamespacedName{Namespace: "test", Name: "route"}}),
143144
Entry("Gateway", &events.DeleteEvent{Type: &v1alpha2.Gateway{}, NamespacedName: types.NamespacedName{Namespace: "test", Name: "gateway"}}),
145+
Entry("GatewayClass", &events.DeleteEvent{Type: &v1alpha2.GatewayClass{}, NamespacedName: types.NamespacedName{Name: "class"}}),
144146
)
145147
})
146148

0 commit comments

Comments
 (0)