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

Static IPs for Gateways #97

Merged
merged 2 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions controllers/gateway_controller_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,26 @@ func (r *GatewayReconciler) createServiceForGateway(ctx context.Context, gw *gat
},
},
}

if len(gw.Spec.Addresses) > 0 {
addr := gw.Spec.Addresses[0]

if *addr.Type != gatewayv1beta1.IPAddressType {
// TODO: update status https://github.com/Kong/blixt/issues/96
return fmt.Errorf("status addresses of type %s are not supported, only IP addresses are supported", *addr.Type)
}

svc.Spec.LoadBalancerIP = addr.Value
}

if len(gw.Spec.Addresses) > 1 {
// TODO: update status https://github.com/Kong/blixt/issues/96
r.Log.Error(
fmt.Errorf("assigning multiple static IPs for a Gateway is not currently supported"),
fmt.Sprintf("%d addresses were requested, only %s will be allocated", len(gw.Spec.Addresses), svc.Spec.LoadBalancerIP),
)
}

_, err := r.ensureServiceConfiguration(ctx, &svc, gw)
if err != nil {
return err
Expand All @@ -64,6 +84,15 @@ func setOwnerReference(svc *corev1.Service, gw client.Object) {
}

func (r *GatewayReconciler) ensureServiceConfiguration(_ context.Context, svc *corev1.Service, gw *gatewayv1beta1.Gateway) (bool, error) {
// TODO: handle removal and changes of addresses https://github.com/Kong/blixt/issues/96

// check whether there's been a problem allocating an IP address
for _, cond := range svc.Status.Conditions {
if cond.Type == corev1.EventTypeWarning && cond.Reason == "AllocationFailed" { // TODO: only handles metallb right now https://github.com/Kong/blixt/issues/96
return false, fmt.Errorf(cond.Message)
}
}

ports := make([]corev1.ServicePort, 0, len(gw.Spec.Listeners))
for _, listener := range gw.Spec.Listeners {
switch proto := listener.Protocol; proto {
Expand Down
217 changes: 217 additions & 0 deletions test/integration/gateway_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
//go:build integration_tests
// +build integration_tests

package integration

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

"github.com/google/uuid"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"

testutils "github.com/kong/blixt/internal/test/utils"
"github.com/kong/blixt/pkg/vars"
)

func TestGatewayBasics(t *testing.T) {
gatewayBasicsCleanupKey := "gatewaybasics"
defer func() {
testutils.DumpDiagnosticsIfFailed(ctx, t, env.Cluster())
runCleanup(gatewayBasicsCleanupKey)
}()

t.Log("deploying GatewayClass")
gwc := &gatewayv1beta1.GatewayClass{
ObjectMeta: metav1.ObjectMeta{
Name: uuid.NewString(),
},
Spec: gatewayv1beta1.GatewayClassSpec{
ControllerName: vars.GatewayClassControllerName,
},
}
gwc, err := gwclient.GatewayV1beta1().GatewayClasses().Create(ctx, gwc, metav1.CreateOptions{})
require.NoError(t, err)
addCleanup(gatewayBasicsCleanupKey, func(ctx context.Context) error {
cleanupLog("cleaning up gatewayclass")
return gwclient.GatewayV1beta1().GatewayClasses().Delete(ctx, gwc.Name, metav1.DeleteOptions{})
})

t.Log("waiting for GatewayClass to be accepted")
require.Eventually(t, func() bool {
var err error
gwc, err = gwclient.GatewayV1beta1().GatewayClasses().Get(ctx, gwc.Name, metav1.GetOptions{})
require.NoError(t, err)
for _, cond := range gwc.Status.Conditions {
if cond.Type == string(gatewayv1beta1.GatewayClassConditionStatusAccepted) &&
cond.Reason == string(gatewayv1beta1.GatewayClassReasonAccepted) &&
cond.Status == metav1.ConditionTrue {
return true
}
}
return false
}, time.Minute, time.Second)

t.Log("determining an available IP address for Gateway")
// TODO: dynamically https://github.com/Kong/blixt/issues/96
ipAddrType := gatewayv1beta1.IPAddressType
gwaddr := gatewayv1beta1.GatewayAddress{
Type: &ipAddrType,
Value: "172.18.0.242",
}

t.Log("creating a Gateway with a static IP")
listenerPort := gatewayv1beta1.PortNumber(8080)
gw := &gatewayv1beta1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: uuid.NewString(),
},
Spec: gatewayv1beta1.GatewaySpec{
GatewayClassName: gatewayv1beta1.ObjectName(gwc.Name),
Addresses: []gatewayv1beta1.GatewayAddress{gwaddr},
Listeners: []gatewayv1beta1.Listener{{
Name: "tcp",
Protocol: gatewayv1beta1.TCPProtocolType,
Port: listenerPort,
}},
},
}
gw, err = gwclient.GatewayV1beta1().Gateways(corev1.NamespaceDefault).Create(ctx, gw, metav1.CreateOptions{})
require.NoError(t, err)
addCleanup(gatewayBasicsCleanupKey, func(ctx context.Context) error {
cleanupLog("cleaning up gateway")
return gwclient.GatewayV1beta1().Gateways(corev1.NamespaceDefault).Delete(ctx, gw.Name, metav1.DeleteOptions{})
})

t.Logf("verifying that the static IP %s is allocated properly", gwaddr.Value)
require.Eventually(t, func() bool {
var err error
gw, err = gwclient.GatewayV1beta1().Gateways(corev1.NamespaceDefault).Get(ctx, gw.Name, metav1.GetOptions{})
require.NoError(t, err)
return len(gw.Status.Addresses) > 0
}, time.Minute, time.Second)
require.NotNil(t, gw.Status.Addresses[0].Type)
require.Equal(t, gatewayv1beta1.IPAddressType, *gw.Status.Addresses[0].Type)
require.Equal(t, gwaddr.Value, gw.Status.Addresses[0].Value)

t.Log("creating a Deployment for an HTTP server to test traffic with")
deploymentName := uuid.NewString()
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: deploymentName,
Labels: map[string]string{
"app": deploymentName,
},
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": deploymentName,
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": deploymentName,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "server",
Image: "ghcr.io/shaneutt/malutki",
ImagePullPolicy: corev1.PullIfNotPresent,
Env: []corev1.EnvVar{{
Name: "LISTEN_PORT",
Value: "8080",
}},
Ports: []corev1.ContainerPort{{
ContainerPort: 8080,
Protocol: corev1.ProtocolTCP,
}},
}},
},
},
},
}
deployment, err = env.Cluster().Client().AppsV1().Deployments(corev1.NamespaceDefault).Create(ctx, deployment, metav1.CreateOptions{})
require.NoError(t, err)
addCleanup(gatewayBasicsCleanupKey, func(ctx context.Context) error {
cleanupLog("cleaning up deployment")
return env.Cluster().Client().AppsV1().Deployments(corev1.NamespaceDefault).Delete(ctx, deployment.Name, metav1.DeleteOptions{})
})

t.Log("exposing the HTTP server via a ClusterIP type Service")
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "integration-tests-gateway-service",
},
Spec: corev1.ServiceSpec{
Selector: map[string]string{
"app": deploymentName,
},
Ports: []corev1.ServicePort{{
Name: "tcp",
Port: 8080,
Protocol: corev1.ProtocolTCP,
TargetPort: intstr.FromInt(8080),
}},
},
}
svc, err = env.Cluster().Client().CoreV1().Services(corev1.NamespaceDefault).Create(ctx, svc, metav1.CreateOptions{})
require.NoError(t, err)
addCleanup(gatewayBasicsCleanupKey, func(ctx context.Context) error {
cleanupLog("cleaning up service")
return env.Cluster().Client().CoreV1().Services(corev1.NamespaceDefault).Delete(ctx, svc.Name, metav1.DeleteOptions{})
})

t.Log("creating a TCPRoute to the server")
tcproute := &gatewayv1alpha2.TCPRoute{
ObjectMeta: metav1.ObjectMeta{
Name: uuid.NewString(),
},
Spec: gatewayv1alpha2.TCPRouteSpec{
CommonRouteSpec: gatewayv1beta1.CommonRouteSpec{
ParentRefs: []gatewayv1alpha2.ParentReference{{
Name: gatewayv1beta1.ObjectName(gw.Name),
Port: &listenerPort,
}},
},
Rules: []gatewayv1alpha2.TCPRouteRule{{
BackendRefs: []gatewayv1alpha2.BackendRef{{
BackendObjectReference: gatewayv1beta1.BackendObjectReference{
Name: gatewayv1beta1.ObjectName(svc.Name),
Port: &listenerPort,
},
}},
}},
},
}
tcproute, err = gwclient.GatewayV1alpha2().TCPRoutes(corev1.NamespaceDefault).Create(ctx, tcproute, metav1.CreateOptions{})
require.NoError(t, err)
addCleanup(gatewayBasicsCleanupKey, func(ctx context.Context) error {
cleanupLog("cleaning up tcproute")
return gwclient.GatewayV1alpha2().TCPRoutes(corev1.NamespaceDefault).Delete(ctx, tcproute.Name, metav1.DeleteOptions{})
})

t.Log("verifying HTTP connectivity to the server")
httpc := http.Client{Timeout: time.Second * 10}
require.Eventually(t, func() bool {
resp, err := httpc.Get(fmt.Sprintf("http://%s:%d/status/%d", gwaddr.Value, listenerPort, http.StatusTeapot))
if err != nil {
t.Logf("received error checking HTTP server: [%s], retrying...", err)
return false
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusTeapot
}, time.Minute, time.Second)

}
Loading