Skip to content

Commit

Permalink
chore(tests): gRPC with Ingress over HTTP and HTTPS (#5239)
Browse files Browse the repository at this point in the history
  • Loading branch information
programmer04 committed Nov 29, 2023
1 parent fe08f67 commit 73ab4d3
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 47 deletions.
3 changes: 3 additions & 0 deletions test/integration/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ const (

// IngressWait is the default amount of time to wait for any particular ingress resource to be provisioned.
IngressWait = 3 * time.Minute

// StatusWait is the default amount of time to wait for object statuses to fulfill a provided predicate.
StatusWait = 3 * time.Minute
)
2 changes: 1 addition & 1 deletion test/integration/consts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ const (

// statusWait is a const duration used in test assertions like .Eventually to
// wait for object statuses to fulfill a provided predicate.
statusWait = time.Minute * 3
statusWait = consts.StatusWait
)
46 changes: 0 additions & 46 deletions test/integration/ingress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,52 +193,6 @@ func TestIngressDefaultBackend(t *testing.T) {
)
}

func TestGRPCIngressEssentials(t *testing.T) {
t.Parallel()

ctx := context.Background()
ns, cleaner := helpers.Setup(ctx, t, env)

t.Log("deploying a minimal HTTP container deployment to test Ingress routes")
container := generators.NewContainer("grpcbin", test.GRPCBinImage, test.GRPCBinPort)
deployment := generators.NewDeploymentForContainer(container)
deployment, err := env.Cluster().Client().AppsV1().Deployments(ns.Name).Create(ctx, deployment, metav1.CreateOptions{})
require.NoError(t, err)
cleaner.Add(deployment)

t.Logf("exposing deployment %s via service", deployment.Name)
service := generators.NewServiceForDeployment(deployment, corev1.ServiceTypeLoadBalancer)
// as of KTF 0.9,0 NewServiceForDeployment doesn't initialize annotations itself, need to do it outside
service.ObjectMeta.Annotations = map[string]string{annotations.AnnotationPrefix + annotations.ProtocolKey: "grpc"}
service, err = env.Cluster().Client().CoreV1().Services(ns.Name).Create(ctx, service, metav1.CreateOptions{})
require.NoError(t, err)
cleaner.Add(service)

t.Logf("creating an ingress for service %s with ingress.class %s", service.Name, consts.IngressClass)
ingress := generators.NewIngressForService("/", map[string]string{
annotations.AnnotationPrefix + annotations.ProtocolsKey: "grpc,grpcs",
annotations.AnnotationPrefix + annotations.StripPathKey: "false",
}, service)
ingress.Spec.IngressClassName = kong.String(consts.IngressClass)
require.NoError(t, clusters.DeployIngress(ctx, env.Cluster(), ns.Name, ingress))
cleaner.Add(ingress)

t.Log("waiting for updated ingress status to include IP")
require.Eventually(t, func() bool {
lbstatus, err := clusters.GetIngressLoadbalancerStatus(ctx, env.Cluster(), ns.Name, ingress)
if err != nil {
return false
}
return len(lbstatus.Ingress) > 0
}, statusWait, waitTick)

// So far this only tests that the ingress is created and receives status information, to confirm the fix for
// https://github.com/Kong/kubernetes-ingress-controller/issues/1991
// It does not test routing, though the status implementation implies it (we only add status after we confirm
// configuration is present in the proxy). This test could be expanded to better confirm routing with a suitable
// gRPC test client.
}

func TestIngressClassNameSpec(t *testing.T) {
t.Parallel()
t.Log("locking IngressClass management")
Expand Down
11 changes: 11 additions & 0 deletions test/integration/isolated/ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,14 @@ func GetAdminURLFromCtx(ctx context.Context) *url.URL {
}
return u.(*url.URL)
}

type _ingressClass struct{}

// GetIngressClassFromCtx gets the Ingress Class from the context.
func GetIngressClassFromCtx(ctx context.Context) string {
r := ctx.Value(_ingressClass{})
if r == nil {
return ""
}
return r.(string)
}
198 changes: 198 additions & 0 deletions test/integration/isolated/ingress_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
//go:build integration_tests

package isolated

import (
"context"
"fmt"
"testing"

"github.com/google/uuid"
"github.com/kong/kubernetes-testing-framework/pkg/clusters"
ktfkong "github.com/kong/kubernetes-testing-framework/pkg/clusters/addons/kong"
"github.com/kong/kubernetes-testing-framework/pkg/utils/kubernetes/generators"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/e2e-framework/pkg/envconf"
"sigs.k8s.io/e2e-framework/pkg/features"

"github.com/kong/kubernetes-ingress-controller/v3/internal/annotations"
"github.com/kong/kubernetes-ingress-controller/v3/internal/util/builder"
"github.com/kong/kubernetes-ingress-controller/v3/test"
"github.com/kong/kubernetes-ingress-controller/v3/test/helpers/certificate"
"github.com/kong/kubernetes-ingress-controller/v3/test/integration/consts"
"github.com/kong/kubernetes-ingress-controller/v3/test/internal/testlabels"
)

func TestIngressGRPC(t *testing.T) {
const testHostname = "grpcs-over-ingress.example"

f := features.
New("essentials").
WithLabel(testlabels.NetworkingFamily, testlabels.NetworkingFamilyIngress).
WithLabel(testlabels.Kind, testlabels.KindIngress).
WithSetup("deploy kong addon into cluster", featureSetup(
withKongProxyEnvVars(map[string]string{
"PROXY_LISTEN": `0.0.0.0:8000 http2\, 0.0.0.0:8443 http2 ssl`,
}),
)).
WithSetup("deploying gRPC service exposed via Ingress", func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context {
cleaner := GetFromCtxForT[*clusters.Cleaner](ctx, t)
cluster := GetClusterFromCtx(ctx)
namespace := GetNamespaceForT(ctx, t)

t.Log("configuring secret")
tlsRouteExampleTLSCert, tlsRouteExampleTLSKey := certificate.MustGenerateSelfSignedCertPEMFormat(certificate.WithCommonName(testHostname))
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "secret-test",
},
Data: map[string][]byte{
"tls.crt": tlsRouteExampleTLSCert,
"tls.key": tlsRouteExampleTLSKey,
},
}

t.Log("deploying secret")
secret, err := cluster.Client().CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{})
assert.NoError(t, err)
cleaner.Add(secret)

type kongProtocolAnnotation string
const (
gRPC kongProtocolAnnotation = "grpc"
gRPCS kongProtocolAnnotation = "grpcs"
)
const (
gRPCBinPort int32 = 9000
gRPCSBinPort int32 = 9001
)
t.Log("deploying a minimal gRPC container deployment to test Ingress routes")
container := generators.NewContainer("grpcbin", test.GRPCBinImage, 0)
// Overwrite ports to specify gRPC over HTTP (9000) and gRPC over HTTPS (9001).
container.Ports = []corev1.ContainerPort{{ContainerPort: gRPCBinPort, Name: string(gRPC)}, {ContainerPort: gRPCSBinPort, Name: string(gRPCS)}}
deployment := generators.NewDeploymentForContainer(container)
deployment, err = cluster.Client().AppsV1().Deployments(namespace).Create(ctx, deployment, metav1.CreateOptions{})
assert.NoError(t, err)
cleaner.Add(deployment)

exposeWithService := func(p kongProtocolAnnotation) *corev1.Service {
grpcBinPort := gRPCBinPort
if p == gRPCS {
grpcBinPort = gRPCSBinPort
}
kongProtocol := string(p)
t.Logf("exposing deployment gRPC (%s) port %s via service", kongProtocol, deployment.Name)
svc := generators.NewServiceForDeploymentWithMappedPorts(deployment, corev1.ServiceTypeLoadBalancer, map[int32]int32{grpcBinPort: grpcBinPort})
svc.Name += kongProtocol
svc.Annotations = map[string]string{
annotations.AnnotationPrefix + annotations.ProtocolKey: kongProtocol,
}
_, err = cluster.Client().CoreV1().Services(namespace).Create(ctx, svc, metav1.CreateOptions{})
assert.NoError(t, err)
cleaner.Add(svc)
return svc
}

// Deploy two services, one for gRPC and one for gRPCS. Two protocols in one service annotation (konghq.com/protocol) are not supported.
serviceGRPC := exposeWithService(gRPC)
serviceGRPCS := exposeWithService(gRPCS)

ingressClass := GetIngressClassFromCtx(ctx)
t.Logf("creating an ingress for services: %s, %s with ingress.class %s", serviceGRPC.Name, serviceGRPCS.Name, ingressClass)
ingress := builder.NewIngress(uuid.NewString(), ingressClass).WithRules(
netv1.IngressRule{
Host: testHostname,
IngressRuleValue: netv1.IngressRuleValue{
HTTP: &netv1.HTTPIngressRuleValue{
Paths: []netv1.HTTPIngressPath{
{
Path: "/",
PathType: lo.ToPtr(netv1.PathTypePrefix),
Backend: netv1.IngressBackend{
Service: &netv1.IngressServiceBackend{
Name: serviceGRPCS.Name,
Port: netv1.ServiceBackendPort{
Number: gRPCSBinPort,
},
},
},
},
},
},
},
},
netv1.IngressRule{
IngressRuleValue: netv1.IngressRuleValue{
HTTP: &netv1.HTTPIngressRuleValue{
Paths: []netv1.HTTPIngressPath{
{
Path: "/",
PathType: lo.ToPtr(netv1.PathTypePrefix),
Backend: netv1.IngressBackend{
Service: &netv1.IngressServiceBackend{
Name: serviceGRPC.Name,
Port: netv1.ServiceBackendPort{
Number: gRPCBinPort,
},
},
},
},
},
},
},
},
).Build()
ingress.Annotations[annotations.AnnotationPrefix+annotations.ProtocolsKey] = fmt.Sprintf("%s,%s", gRPC, gRPCS)
assert.NoError(t, clusters.DeployIngress(ctx, cluster, namespace, ingress))
cleaner.Add(ingress)
ctx = SetInCtxForT(ctx, t, ingress)

return ctx
}).
Assess("checking whether Ingress status is updated and gRPC traffic over HTTPS is properly routed", func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context {
t.Log("waiting for updated ingress status to include IP")
assert.Eventually(t, func() bool {
cluster := GetClusterFromCtx(ctx)
namespace := GetNamespaceForT(ctx, t)
ingress := GetFromCtxForT[*netv1.Ingress](ctx, t)

lbstatus, err := clusters.GetIngressLoadbalancerStatus(ctx, cluster, namespace, ingress)
if err != nil {
return false
}
return len(lbstatus.Ingress) > 0
}, consts.StatusWait, consts.WaitTick)

verifyEchoResponds := func(hostname string) {
// Kong Gateway uses different ports for HTTP and HTTPS traffic.
proxyPort := ktfkong.DefaultProxyTLSServicePort
tlsEnabled := true
if hostname == "" {
proxyPort = ktfkong.DefaultProxyHTTPPort
tlsEnabled = false
}
assert.Eventually(t, func() bool {
if err := grpcEchoResponds(
ctx, fmt.Sprintf("%s:%d", GetProxyURLFromCtx(ctx).Hostname(), proxyPort), hostname, "echo Kong", tlsEnabled,
); err != nil {
t.Log(err)
return false
}
return true
}, consts.IngressWait, consts.WaitTick)
}
t.Log("verifying service connectivity via HTTPS (gRPCS)")
verifyEchoResponds(testHostname)
t.Log("verifying service connectivity via HTTP (gRPC)")
verifyEchoResponds("")

return ctx
}).
Teardown(featureTeardown())

tenv.Test(t, f.Feature())
}
1 change: 1 addition & 0 deletions test/integration/isolated/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ func featureSetup(opts ...featureSetupOpt) func(ctx context.Context, t *testing.
// the cleaner always gets a 404 on it for unknown reasons
_ = cluster.Client().NetworkingV1().IngressClasses().Delete(ctx, ingressClass, metav1.DeleteOptions{})
}()
ctx = setInCtx(ctx, _ingressClass{}, ingressClass)

clusterVersion, err := cluster.Version()
if !assert.NoError(t, err, "failed getting cluster version") {
Expand Down
1 change: 1 addition & 0 deletions test/internal/testlabels/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const (
Kind = "kind"
KindUDPRoute = "UDPRoute"
KindGRPCRoute = "GRPCRoute"
KindIngress = "Ingress"
)

const (
Expand Down

0 comments on commit 73ab4d3

Please sign in to comment.