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

Bump go 1.22.2->1.22.5 and add E2E tests #639

Merged
merged 33 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
13ad396
E2E tests
carreter Jul 8, 2024
9fea054
Fix e2e tests
carreter Jul 8, 2024
dbe8e76
Bump go version and add e2e test runner
carreter Jul 3, 2024
29c2503
Vendor
carreter Jul 8, 2024
1cafd93
go mod tidy to vendor more dependencies
carreter Jul 3, 2024
2c7858a
Improve e2e tests
carreter Jul 8, 2024
af61fa0
Add e2e test for multiple servers and multiple agents
carreter Jul 8, 2024
28ebff1
Review comments
carreter Jul 17, 2024
6615f6a
Add kind-e2e workflow
carreter Jul 17, 2024
810059a
Exclude e2e tests from fast-test and test runners
carreter Jul 18, 2024
de25d5b
More stringent matching of e2e tests
carreter Jul 19, 2024
4e00652
Properly escape $ in makefile
carreter Jul 19, 2024
a7ca28e
Fix typo
carreter Jul 19, 2024
801565d
Fix empty REGISTRY variable
carreter Jul 19, 2024
bfea27c
Make kind-e2e run on different k8s versions
carreter Jul 19, 2024
2796ac4
Make kind-e2e workflow only build images once
carreter Jul 19, 2024
51313fd
Load prebuilt konnectivity images
carreter Jul 19, 2024
9b4df3c
Will this work? who knows
carreter Jul 19, 2024
36781ff
Here we go again!
carreter Jul 19, 2024
1152f81
Add e2e testing for http-connect mode
carreter Jul 22, 2024
a2c28db
Don't rebuild docker images on every kind-e2e run
carreter Jul 23, 2024
4204eff
Typo
carreter Jul 23, 2024
bca2467
Bump go from 1.22.3 to 1.22.5
carreter Jul 24, 2024
3d97cbe
Review nit
carreter Jul 24, 2024
1923987
Bump go toolchain to 1.22.5
carreter Jul 24, 2024
8aae4aa
Revert go toolchain to 1.22.4
carreter Jul 24, 2024
84c06a0
e2e refactor and add lease testing
carreter Jul 26, 2024
0e365ee
Switch out StatefulSet for Deployment
carreter Jul 29, 2024
bc020f4
Add README with info on tests
carreter Jul 29, 2024
55f34f6
Fix kind capitalization
carreter Jul 29, 2024
2ffb6d5
Fix konnectivity-client version
carreter Jul 29, 2024
ba1fba3
Restore toolchain directive
carreter Jul 29, 2024
8f39a84
Review comments
carreter Aug 5, 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
47 changes: 47 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,53 @@ jobs:
with:
name: konnectivity-agent
path: _output/konnectivity-agent.tar
kind-e2e:
name: kind-e2e
runs-on: ubuntu-20.04
timeout-minutes: 100
needs:
- build
env:
REGISTRY: gcr.io/k8s-staging-kas-network-proxy
KIND_IMAGE: kindest/node${{ matrix.k8s }}
TAG: master
CONNECTION_MODE: ${{ matrix.connection-mode }}
strategy:
fail-fast: false
matrix:
k8s: [ v1.27.11, v1.28.7, v1.29.2 ]
connection-mode: [ grpc, http-connect ]
steps:
- name: Install kind
run: |
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.23.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
- name: Check out code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
id: go
- name: Download prebuilt konnectivity-server image
uses: actions/download-artifact@v4
with:
name: konnectivity-server
- name: Download prebuilt konnectivity-agent image
uses: actions/download-artifact@v4
with:
name: konnectivity-agent
- name: Load prebuilt konnectivity images
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please tell me what this does ?

Does it not build a new konnectivity server/agent image per PR which is then used by the e2e tests ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to make it so that the image gets built once per workflow run, instead of being built each kind-e2e step. I'm not sure if that's working, though...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aojea Could I get you to take a look at this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to fix this by making a new entry in the Makefile that doesn't have a dependency on docker-build.

run: |
docker load --input konnectivity-server.tar
docker load --input konnectivity-agent.tar
- name: Fix konnectivity docker image tags
run: |
docker tag gcr.io/k8s-staging-kas-network-proxy/proxy-server:master gcr.io/k8s-staging-kas-network-proxy/proxy-server-amd64:master
docker tag gcr.io/k8s-staging-kas-network-proxy/proxy-agent:master gcr.io/k8s-staging-kas-network-proxy/proxy-agent-amd64:master
- name: Run e2e tests
run: make test-e2e-ci
e2e:
name: e2e
runs-on: ubuntu-20.04
Expand Down
18 changes: 15 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ ALL_ARCH ?= amd64 arm arm64 ppc64le s390x
# The output type could either be docker (local), or registry.
OUTPUT_TYPE ?= docker
GO_TOOLCHAIN ?= golang
GO_VERSION ?= 1.22.2
GO_VERSION ?= 1.22.5
BASEIMAGE ?= gcr.io/distroless/static-debian11:nonroot

ifeq ($(GOPATH),)
Expand Down Expand Up @@ -54,6 +54,9 @@ TAG ?= $(shell git rev-parse HEAD)
DOCKER_CMD ?= docker
DOCKER_CLI_EXPERIMENTAL ?= enabled
PROXY_SERVER_IP ?= 127.0.0.1

KIND_IMAGE ?= kindest/node
CONNECTION_MODE ?= grpc
## --------------------------------------
## Testing
## --------------------------------------
Expand All @@ -67,19 +70,28 @@ mock_gen:
# Unit tests with faster execution (nicer for development).
.PHONY: fast-test
fast-test:
go test -mod=vendor -race ./...
go test -mod=vendor -race $(shell go list ./... | grep -v -e "/e2e$$" -e "/e2e/.*")
cd konnectivity-client && go test -race ./...

# Unit tests with fuller coverage, invoked by CI system.
.PHONY: test
test:
go test -mod=vendor -race -covermode=atomic -coverprofile=konnectivity.out ./... && go tool cover -html=konnectivity.out -o=konnectivity.html
go test -mod=vendor -race -covermode=atomic -coverprofile=konnectivity.out $(shell go list ./... | grep -v -e "/e2e$$" -e "/e2e/.*") && go tool cover -html=konnectivity.out -o=konnectivity.html
cd konnectivity-client && go test -race -covermode=atomic -coverprofile=client.out ./... && go tool cover -html=client.out -o=client.html

.PHONY: test-integration
test-integration: build
go test -mod=vendor -race ./tests -agent-path $(PWD)/bin/proxy-agent

.PHONY: test-e2e
test-e2e: docker-build
go test -mod=vendor ./e2e -race -agent-image ${AGENT_FULL_IMAGE}-$(TARGETARCH):${TAG} -server-image ${SERVER_FULL_IMAGE}-$(TARGETARCH):${TAG} -kind-image ${KIND_IMAGE} -mode ${CONNECTION_MODE}

# e2e test runner for continuous integration that does not build a new image.
.PHONY: test-e2e-ci
test-e2e-ci:
go test -mod=vendor ./e2e -race -agent-image ${AGENT_FULL_IMAGE}-$(TARGETARCH):${TAG} -server-image ${SERVER_FULL_IMAGE}-$(TARGETARCH):${TAG} -kind-image ${KIND_IMAGE} -mode ${CONNECTION_MODE}

## --------------------------------------
## Binaries
## --------------------------------------
Expand Down
23 changes: 23 additions & 0 deletions e2e/README.md
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I don't see 'KinD' capitalization used on the linked docs, keep it 'kind' for consistency

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# End-to-end tests for konnectivity-network-proxy running in a kind cluster

These e2e tests deploy the KNP agent and server to a local [kind](https://kind.sigs.k8s.io/)
cluster to verify their functionality.

These can be run automatically using `make e2e-test`.

## Setup in `main_test.go`

Before any of the actual tests are run, the `TestMain()` function
in `main_test.go` performs the following set up steps:

- Spin up a new kind cluster with the node image provided by the `-kind-image` flag.
- Sideload the KNP agent and server images provided with `-agent-image` and `-server-image` into the cluster.
- Deploy the necessary RBAC and service templates for both the KNP agent and server (see `renderAndApplyManifests`).

## The tests

### `static_count_test.go`

These tests deploy the KNP servers and agents to the previously created kind cluster.
After the deployments are up, the tests check that both the agent and server report
the correct number of connections on their metrics endpoints.
178 changes: 178 additions & 0 deletions e2e/main_test.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a short README.md to the e2e directory that goes over what maint_test.go sets up (RBAC and service) and what each specific test is expected to set up?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On it!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, could you take a look?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package e2e

import (
"bytes"
"context"
"flag"
"fmt"
"log"
"os"
"path"
"testing"
"text/template"
"time"

"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/e2e-framework/klient/wait"
"sigs.k8s.io/e2e-framework/klient/wait/conditions"
"sigs.k8s.io/e2e-framework/pkg/env"
"sigs.k8s.io/e2e-framework/pkg/envconf"
"sigs.k8s.io/e2e-framework/pkg/envfuncs"
"sigs.k8s.io/e2e-framework/support/kind"
)

var (
testenv env.Environment
agentImage = flag.String("agent-image", "", "The proxy agent's docker image.")
serverImage = flag.String("server-image", "", "The proxy server's docker image.")
kindImage = flag.String("kind-image", "kindest/node", "Image to use for kind nodes.")
connectionMode = flag.String("mode", "grpc", "Connection mode to use during e2e tests.")
)

func TestMain(m *testing.M) {
flag.Parse()
if *agentImage == "" {
log.Fatalf("must provide agent image with -agent-image")
}
if *serverImage == "" {
log.Fatalf("must provide server image with -server-image")
}

scheme.AddToScheme(scheme.Scheme)

testenv = env.New()
kindClusterName := "kind-test"
kindCluster := kind.NewCluster(kindClusterName).WithOpts(kind.WithImage(*kindImage))

testenv.Setup(
envfuncs.CreateCluster(kindCluster, kindClusterName),
envfuncs.LoadImageToCluster(kindClusterName, *agentImage),
envfuncs.LoadImageToCluster(kindClusterName, *serverImage),
renderAndApplyManifests,
)

testenv.Finish(envfuncs.DestroyCluster(kindClusterName))

os.Exit(testenv.Run(m))
}

// renderTemplate renders a template from e2e/templates into a kubernetes object.
// Template paths are relative to e2e/templates.
func renderTemplate(file string, params any) (client.Object, *schema.GroupVersionKind, error) {
b := &bytes.Buffer{}

tmp, err := template.ParseFiles(path.Join("templates/", file))
if err != nil {
return nil, nil, fmt.Errorf("could not parse template %v: %w", file, err)
}

err = tmp.Execute(b, params)
if err != nil {
return nil, nil, fmt.Errorf("could not execute template %v: %w", file, err)
}

decoder := scheme.Codecs.UniversalDeserializer()

obj, gvk, err := decoder.Decode(b.Bytes(), nil, nil)
if err != nil {
return nil, nil, fmt.Errorf("could not decode rendered yaml into kubernetes object: %w", err)
}

return obj.(client.Object), gvk, nil
}

type KeyValue struct {
Key string
Value string
}

type DeploymentConfig struct {
Replicas int
Image string
Args []KeyValue
}

func renderAndApplyManifests(ctx context.Context, cfg *envconf.Config) (context.Context, error) {
client := cfg.Client()

// Render agent RBAC and Service templates.
agentServiceAccount, _, err := renderTemplate("agent/serviceaccount.yaml", struct{}{})
if err != nil {
return nil, err
}
agentClusterRole, _, err := renderTemplate("agent/clusterrole.yaml", struct{}{})
if err != nil {
return nil, err
}
agentClusterRoleBinding, _, err := renderTemplate("agent/clusterrolebinding.yaml", struct{}{})
if err != nil {
return ctx, err
}
agentService, _, err := renderTemplate("agent/service.yaml", struct{}{})
if err != nil {
return ctx, err
}

// Submit agent RBAC templates to k8s.
err = client.Resources().Create(ctx, agentServiceAccount)
if err != nil {
return ctx, err
}
err = client.Resources().Create(ctx, agentClusterRole)
if err != nil {
return ctx, err
}
err = client.Resources().Create(ctx, agentClusterRoleBinding)
if err != nil {
return ctx, err
}
err = client.Resources().Create(ctx, agentService)
if err != nil {
return ctx, err
}

// Render server RBAC and Service templates.
serverClusterRoleBinding, _, err := renderTemplate("server/clusterrolebinding.yaml", struct{}{})
if err != nil {
return ctx, err
}
serverService, _, err := renderTemplate("server/service.yaml", struct{}{})
if err != nil {
return ctx, err
}

// Submit server templates to k8s.
err = client.Resources().Create(ctx, serverClusterRoleBinding)
if err != nil {
return ctx, err
}
err = client.Resources().Create(ctx, serverService)
if err != nil {
return ctx, err
}

return ctx, nil
}

func deployAndWaitForDeployment(deployment client.Object) func(context.Context, *testing.T, *envconf.Config) context.Context {
return func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
client := cfg.Client()
err := client.Resources().Create(ctx, deployment)
if err != nil {
t.Fatalf("could not create Deployment: %v", err)
}

err = wait.For(
conditions.New(client.Resources()).DeploymentAvailable(deployment.GetName(), deployment.GetNamespace()),
wait.WithTimeout(1*time.Minute),
wait.WithInterval(10*time.Second),
)
if err != nil {
t.Fatalf("waiting for Deployment failed: %v", err)
}

return ctx
}
}
85 changes: 85 additions & 0 deletions e2e/metrics_assertions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package e2e

import (
"context"
"fmt"
"net/http"
"testing"

corev1 "k8s.io/api/core/v1"

"github.com/prometheus/common/expfmt"
"sigs.k8s.io/e2e-framework/klient/k8s/resources"
"sigs.k8s.io/e2e-framework/pkg/envconf"
)

func getMetricsGaugeValue(url string, name string) (int, error) {
resp, err := http.Get(url)
if err != nil {
return 0, fmt.Errorf("could not get metrics: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debuggability is easier if you include the url in with the error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

metricsParser := &expfmt.TextParser{}
metricsFamilies, err := metricsParser.TextToMetricFamilies(resp.Body)
defer resp.Body.Close()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not critical but I think the defer should really be right after the http.Get error check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if err != nil {
return 0, fmt.Errorf("could not parse metrics: %w", err)
}

metricFamily, exists := metricsFamilies[name]
if !exists {
return 0, fmt.Errorf("metric %v does not exist", name)
}
value := int(metricFamily.GetMetric()[0].GetGauge().GetValue())
return value, nil
}

func assertAgentsAreConnected(expectedConnections int, adminPort int) func(context.Context, *testing.T, *envconf.Config) context.Context {
return func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
client := cfg.Client()

var agentPods *corev1.PodList
err := client.Resources().List(ctx, agentPods, resources.WithLabelSelector("k8s-app=konnectivity-agent"))
if err != nil {
t.Fatalf("couldn't get agent pods: %v", err)
}

for _, agentPod := range agentPods.Items {
numConnections, err := getMetricsGaugeValue(fmt.Sprintf("%v:%v/metrics", agentPod.Status.PodIP, adminPort), "konnectivity_network_proxy_agent_open_server_connections")
if err != nil {
t.Fatalf("couldn't get agent metric 'konnectivity_network_proxy_agent_open_server_connections' for pod %v", agentPod.Name)
}

if numConnections != expectedConnections {
t.Errorf("incorrect number of connected servers (want: %v, got: %v)", expectedConnections, numConnections)
}
}

return ctx
}
}

func assertServersAreConnected(expectedConnections int, adminPort int) func(context.Context, *testing.T, *envconf.Config) context.Context {
return func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
client := cfg.Client()

var serverPods *corev1.PodList
err := client.Resources().List(ctx, serverPods, resources.WithLabelSelector("k8s-app=konnectivity-server"))
if err != nil {
t.Fatalf("couldn't get server pods: %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again debuggability goes up if you have a reference to relevant request (server pods) with the error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a reference to the label selector being used to discover the pods.

}

for _, serverPod := range serverPods.Items {
numConnections, err := getMetricsGaugeValue(fmt.Sprintf("%v:%v/metrics", serverPod.Status.PodIP, adminPort), "konnectivity_network_proxy_server_ready_backend_connections")
if err != nil {
t.Fatalf("couldn't get agent metric 'konnectivity_network_proxy_server_ready_backend_connections' for pod %v", serverPod.Name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nout found is the obvious error but there could be other reasons we got an error here and we did not check the error type. I would suggest logging it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

if numConnections != expectedConnections {
t.Errorf("incorrect number of connected agents (want: %v, got: %v)", expectedConnections, numConnections)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

%v can mask some interesting errors in a case like this. They may be unequal because they are of different types but their toString method generates the same string. If you believe they should both be ints then it is better to use %d.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
}

return ctx
}
}
Loading