Skip to content

Commit fdbe668

Browse files
authored
Add basic system test with utilities (#1274)
Problem: In order to test a full NGF system running in k8s, we need a framework that can easily deploy apps and send traffic. Solution: Enhance the framework with functions to create apps and send traffic using port forwarding. Also added a basic test to utilize these functions as a proof of concept.
1 parent 9ecd5fe commit fdbe668

12 files changed

+753
-18
lines changed

Diff for: Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ lint: ## Run golangci-lint against code
123123

124124
.PHONY: unit-test
125125
unit-test: ## Run unit tests for the go code
126-
go test ./... -tags unit -race -coverprofile cover.out
126+
go test ./internal/... -race -coverprofile cover.out
127127
go tool cover -html=cover.out -o cover.html
128128

129129
.PHONY: njs-unit-test

Diff for: tests/Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ load-images: ## Load NGF and NGINX images on configured kind cluster
2222
kind load docker-image $(PREFIX):$(TAG) $(NGINX_PREFIX):$(TAG)
2323

2424
test: ## Run the system tests against your default k8s cluster
25-
go test -v . -args --gateway-api-version=$(GW_API_VERSION) --image-tag=$(TAG) \
25+
go test -v ./suite -args --gateway-api-version=$(GW_API_VERSION) --image-tag=$(TAG) \
2626
--ngf-image-repo=$(PREFIX) --nginx-image-repo=$(NGINX_PREFIX) --pull-policy=$(PULL_POLICY) \
2727
--k8s-version=$(K8S_VERSION)
2828

Diff for: tests/README.md

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# System Testing
2+
3+
The tests in this directory are meant to be run on a live Kubernetes environment to verify a real system. These
4+
are similar to the existing [conformance tests](../conformance/README.md), but will verify things such as:
5+
6+
- NGF-specific functionality
7+
- Non-Functional requirements testing (such as performance, scale, etc.)
8+
9+
When running, the tests create a port-forward from your NGF Pod to localhost using a port chosen by the
10+
test framework. Traffic is sent over this port.
11+
12+
Directory structure is as follows:
13+
14+
- `framework`: contains utility functions for running the tests
15+
- `suite`: contains the test files
16+
17+
**Note**: Existing NFR tests will be migrated into this testing `suite` and results stored in a `results` directory.
18+
19+
## Prerequisites
20+
21+
- Kubernetes cluster.
22+
- Docker.
23+
- Golang.
24+
25+
**Note**: all commands in steps below are executed from the `tests` directory
26+
27+
```shell
28+
make
29+
```
30+
31+
```text
32+
build-images Build NGF and NGINX images
33+
create-kind-cluster Create a kind cluster
34+
delete-kind-cluster Delete kind cluster
35+
help Display this help
36+
load-images Load NGF and NGINX images on configured kind cluster
37+
test Run the system tests against your default k8s cluster
38+
```
39+
40+
**Note:** The following variables are configurable when running the below `make` commands:
41+
42+
| Variable | Default | Description |
43+
|----------|---------|-------------|
44+
| TAG | edge | tag for the locally built NGF images |
45+
| PREFIX | nginx-gateway-fabric | prefix for the locally built NGF image |
46+
| NGINX_PREFIX | nginx-gateway-fabric/nginx | prefix for the locally built NGINX image |
47+
| PULL_POLICY | Never | NGF image pull policy |
48+
| GW_API_VERSION | 1.0.0 | version of Gateway API resources to install |
49+
| K8S_VERSION | latest | version of k8s that the tests are run on |
50+
51+
## Step 1 - Create a Kubernetes cluster
52+
53+
This can be done in a cloud provider of choice, or locally using `kind`:
54+
55+
```makefile
56+
make create-kind-cluster
57+
```
58+
59+
> Note: The default kind cluster deployed is the latest available version. You can specify a different version by
60+
> defining the kind image to use through the KIND_IMAGE variable, e.g.
61+
62+
```makefile
63+
make create-kind-cluster KIND_IMAGE=kindest/node:v1.27.3
64+
```
65+
66+
## Step 2 - Build and Load Images
67+
68+
Loading the images only applies to a `kind` cluster. If using a cloud provider, you will need to tag and push
69+
your images to a registry that is accessible from that cloud provider.
70+
71+
```makefile
72+
make build-images load-images TAG=$(whoami)
73+
```
74+
75+
## Step 3 - Run the tests
76+
77+
```makefile
78+
make test TAG=$(whoami)
79+
```
80+
81+
To run a specific test, you can "focus" it by adding the `F` prefix to the name. For example:
82+
83+
```go
84+
It("runs some test", func(){
85+
...
86+
})
87+
```
88+
89+
becomes:
90+
91+
```go
92+
FIt("runs some test", func(){
93+
...
94+
})
95+
```
96+
97+
This can also be done at higher levels like `Context`.
98+
99+
To disable a specific test, add the `X` prefix to it, similar to the previous example:
100+
101+
```go
102+
It("runs some test", func(){
103+
...
104+
})
105+
```
106+
107+
becomes:
108+
109+
```go
110+
XIt("runs some test", func(){
111+
...
112+
})
113+
```
114+
115+
## Step 4 - Delete kind cluster
116+
117+
```makefile
118+
make delete-kind-cluster
119+
```

Diff for: tests/framework/portforward.go

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package framework
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
"net/url"
10+
"path"
11+
"time"
12+
13+
core "k8s.io/api/core/v1"
14+
"k8s.io/client-go/rest"
15+
"k8s.io/client-go/tools/portforward"
16+
"k8s.io/client-go/transport/spdy"
17+
"sigs.k8s.io/controller-runtime/pkg/client"
18+
)
19+
20+
// GetNGFPodName returns the name of the NGF Pod.
21+
func GetNGFPodName(
22+
k8sClient client.Client,
23+
namespace,
24+
releaseName string,
25+
timeout time.Duration,
26+
) (string, error) {
27+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
28+
defer cancel()
29+
30+
var podList core.PodList
31+
if err := k8sClient.List(
32+
ctx,
33+
&podList,
34+
client.InNamespace(namespace),
35+
client.MatchingLabels{
36+
"app.kubernetes.io/instance": releaseName,
37+
},
38+
); err != nil {
39+
return "", fmt.Errorf("error getting list of Pods: %w", err)
40+
}
41+
42+
if len(podList.Items) > 0 {
43+
return podList.Items[0].Name, nil
44+
}
45+
46+
return "", errors.New("unable to find NGF Pod")
47+
}
48+
49+
// PortForward starts a port-forward to the specified Pod and returns the local port being forwarded.
50+
func PortForward(config *rest.Config, namespace, podName string, stopCh chan struct{}) (int, error) {
51+
roundTripper, upgrader, err := spdy.RoundTripperFor(config)
52+
if err != nil {
53+
return 0, fmt.Errorf("error creating roundtripper: %w", err)
54+
}
55+
56+
serverURL, err := url.Parse(config.Host)
57+
if err != nil {
58+
return 0, fmt.Errorf("error parsing rest config host: %w", err)
59+
}
60+
61+
serverURL.Path = path.Join(
62+
"api", "v1",
63+
"namespaces", namespace,
64+
"pods", podName,
65+
"portforward",
66+
)
67+
68+
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: roundTripper}, http.MethodPost, serverURL)
69+
70+
readyCh := make(chan struct{}, 1)
71+
out, errOut := new(bytes.Buffer), new(bytes.Buffer)
72+
73+
forwarder, err := portforward.New(dialer, []string{":80"}, stopCh, readyCh, out, errOut)
74+
if err != nil {
75+
return 0, fmt.Errorf("error creating port forwarder: %w", err)
76+
}
77+
78+
go func() {
79+
if err := forwarder.ForwardPorts(); err != nil {
80+
panic(err)
81+
}
82+
}()
83+
84+
<-readyCh
85+
ports, err := forwarder.GetPorts()
86+
if err != nil {
87+
return 0, fmt.Errorf("error getting ports being forwarded: %w", err)
88+
}
89+
90+
return int(ports[0].Local), nil
91+
}

Diff for: tests/framework/request.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package framework
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"net"
8+
"net/http"
9+
"strings"
10+
"time"
11+
)
12+
13+
// Get sends a GET request to the specified url.
14+
// It resolves to localhost (where the NGF port-forward is running) instead of using DNS.
15+
// The status and body of the response is returned, or an error.
16+
func Get(url string, timeout time.Duration) (int, string, error) {
17+
dialer := &net.Dialer{}
18+
19+
http.DefaultTransport.(*http.Transport).DialContext = func(
20+
ctx context.Context,
21+
network,
22+
addr string,
23+
) (net.Conn, error) {
24+
split := strings.Split(addr, ":")
25+
port := split[len(split)-1]
26+
return dialer.DialContext(ctx, network, fmt.Sprintf("127.0.0.1:%s", port))
27+
}
28+
29+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
30+
defer cancel()
31+
32+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
33+
if err != nil {
34+
return 0, "", err
35+
}
36+
37+
resp, err := http.DefaultClient.Do(req)
38+
if err != nil {
39+
return 0, "", err
40+
}
41+
defer resp.Body.Close()
42+
43+
body := new(bytes.Buffer)
44+
_, err = body.ReadFrom(resp.Body)
45+
if err != nil {
46+
return resp.StatusCode, "", err
47+
}
48+
49+
return resp.StatusCode, body.String(), nil
50+
}

0 commit comments

Comments
 (0)