Skip to content

Commit

Permalink
test: added integration test on HTTPS
Browse files Browse the repository at this point in the history
Signed-off-by: Mattia Lavacca <lavacca.mattia@gmail.com>
  • Loading branch information
mlavacca committed Apr 18, 2024
1 parent e8a7e61 commit cbdd513
Show file tree
Hide file tree
Showing 15 changed files with 459 additions and 180 deletions.
4 changes: 2 additions & 2 deletions pkg/utils/gateway/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import (
// Gateway Utils - Status Updates
// -----------------------------------------------------------------------------

// IsScheduled indicates whether or not the provided Gateway object was
// IsAccepted indicates whether or not the provided Gateway object was
// marked as scheduled by the controller.
func IsScheduled(gateway *gwtypes.Gateway) bool {
func IsAccepted(gateway *gwtypes.Gateway) bool {
for _, cond := range gateway.Status.Conditions {
if cond.Type == string(gatewayv1.GatewayConditionAccepted) &&
cond.Reason == string(gatewayv1.GatewayClassReasonAccepted) &&
Expand Down
13 changes: 5 additions & 8 deletions pkg/utils/test/predicates.go
Original file line number Diff line number Diff line change
Expand Up @@ -628,10 +628,10 @@ func GatewayNotExist(t *testing.T, ctx context.Context, gatewayNSN types.Namespa
}
}

// GatewayIsScheduled returns a function that checks if a Gateway is scheduled.
func GatewayIsScheduled(t *testing.T, ctx context.Context, gatewayNSN types.NamespacedName, clients K8sClients) func() bool {
// GatewayIsAccepted returns a function that checks if a Gateway is scheduled.
func GatewayIsAccepted(t *testing.T, ctx context.Context, gatewayNSN types.NamespacedName, clients K8sClients) func() bool {
return func() bool {
return gatewayutils.IsScheduled(MustGetGateway(t, ctx, gatewayNSN, clients))
return gatewayutils.IsAccepted(MustGetGateway(t, ctx, gatewayNSN, clients))
}
}

Expand Down Expand Up @@ -768,12 +768,9 @@ func GatewayIPAddressExist(t *testing.T, ctx context.Context, gatewayNSN types.N
}

// GetResponseBodyContains issues an HTTP request and checks if a response body contains a string.
func GetResponseBodyContains(t *testing.T, ctx context.Context, clients K8sClients, httpc http.Client, url string, method string, responseContains string) func() bool {
func GetResponseBodyContains(t *testing.T, clients K8sClients, httpc *http.Client, request *http.Request, responseContains string) func() bool {
return func() bool {
req, err := http.NewRequestWithContext(ctx, method, url, nil)
require.NoError(t, err)

resp, err := httpc.Do(req)
resp, err := httpc.Do(request)
if err != nil {
return false
}
Expand Down
5 changes: 3 additions & 2 deletions test/e2e/test_operator_logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"

testutils "github.com/kong/gateway-operator/pkg/utils/test"
"github.com/kong/gateway-operator/test/helpers"
)

func init() {
Expand Down Expand Up @@ -143,7 +144,7 @@ func TestOperatorLogs(t *testing.T) {
}()

t.Log("deploying a GatewayClass resource")
gatewayClass := testutils.GenerateGatewayClass()
gatewayClass := helpers.GenerateGatewayClass(nil)
gatewayClass, err = clients.GatewayClient.GatewayV1().GatewayClasses().Create(ctx, gatewayClass, metav1.CreateOptions{})
require.NoError(t, err)
cleaner.Add(gatewayClass)
Expand All @@ -154,7 +155,7 @@ func TestOperatorLogs(t *testing.T) {
Name: uuid.NewString(),
Namespace: testNamespace.Name,
}
gateway := testutils.GenerateGateway(gatewayNN, gatewayClass)
gateway := helpers.GenerateGateway(gatewayNN, gatewayClass)
gateway, err = clients.GatewayClient.GatewayV1().Gateways(testNamespace.Name).Create(ctx, gateway, metav1.CreateOptions{})
require.NoError(t, err)
cleaner.Add(gateway)
Expand Down
68 changes: 67 additions & 1 deletion test/helpers/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io"
"math/big"
"net"
"testing"
"time"

"github.com/stretchr/testify/require"
)

// -----------------------------------------------------------------------------
// Certificate test helper functions and types
// CP-DP mTLS Certificate test helper functions and types
// -----------------------------------------------------------------------------

// Cert represents a TLS certificate that can be used for testing purposes.
Expand Down Expand Up @@ -148,3 +152,65 @@ func TLSSecretData(t *testing.T, ca Cert, c Cert) map[string][]byte {
"tls.key": c.KeyPEM.Bytes(),
}
}

// -----------------------------------------------------------------------------
// TLS Certificate test helper functions and types
// -----------------------------------------------------------------------------

const (
rsaBits = 2048
validFor = 365 * 24 * time.Hour
)

// generateRSACert generates a basic self signed certificate valid for a year
func generateRSACert(hosts []string, keyOut, certOut io.Writer) error {
priv, err := rsa.GenerateKey(rand.Reader, rsaBits)
if err != nil {
return fmt.Errorf("failed to generate key: %w", err)
}
notBefore := time.Now()
notAfter := notBefore.Add(validFor)

serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return fmt.Errorf("failed to generate serial number: %w", err)
}

template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: "default",
Organization: []string{"Acme Co"},
},
NotBefore: notBefore,
NotAfter: notAfter,

KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}

for _, h := range hosts {
if ip := net.ParseIP(h); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, h)
}
}

derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return fmt.Errorf("failed to create certificate: %w", err)
}

if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
return fmt.Errorf("failed creating cert: %w", err)
}

if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
return fmt.Errorf("failed creating key: %w", err)
}

return nil
}
96 changes: 88 additions & 8 deletions pkg/utils/test/generators.go → test/helpers/generators.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package test
package helpers

import (
"bytes"
"testing"

"github.com/google/uuid"
"github.com/samber/lo"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
Expand All @@ -12,17 +17,17 @@ import (
gwtypes "github.com/kong/gateway-operator/internal/types"
"github.com/kong/gateway-operator/pkg/consts"
"github.com/kong/gateway-operator/pkg/vars"
"github.com/kong/gateway-operator/test/helpers"
)

// GenerateGatewayClass generates the default GatewayClass to be used in tests
func GenerateGatewayClass() *gatewayv1.GatewayClass {
func GenerateGatewayClass(parametersRef *gatewayv1.ParametersReference) *gatewayv1.GatewayClass {
gatewayClass := &gatewayv1.GatewayClass{
ObjectMeta: metav1.ObjectMeta{
Name: uuid.NewString(),
},
Spec: gatewayv1.GatewayClassSpec{
ControllerName: gatewayv1.GatewayController(vars.ControllerName()),
ParametersRef: parametersRef,
},
}
return gatewayClass
Expand Down Expand Up @@ -53,11 +58,11 @@ func GenerateGateway(gatewayNSN types.NamespacedName, gatewayClass *gatewayv1.Ga
}

// GenerateGatewayConfiguration generates a GatewayConfiguration to be used in tests
func GenerateGatewayConfiguration(gatewayConfigurationNSN types.NamespacedName) *operatorv1beta1.GatewayConfiguration {
func GenerateGatewayConfiguration(namespace string) *operatorv1beta1.GatewayConfiguration {
return &operatorv1beta1.GatewayConfiguration{
ObjectMeta: metav1.ObjectMeta{
Namespace: gatewayConfigurationNSN.Namespace,
Name: gatewayConfigurationNSN.Name,
Namespace: namespace,
Name: uuid.NewString(),
},
Spec: operatorv1beta1.GatewayConfigurationSpec{
ControlPlaneOptions: &operatorv1beta1.ControlPlaneOptions{
Expand All @@ -82,6 +87,12 @@ func GenerateGatewayConfiguration(gatewayConfigurationNSN types.NamespacedName)
},
},
},
Env: []corev1.EnvVar{
{
Name: "CONTROLLER_LOG_LEVEL",
Value: "trace",
},
},
},
},
},
Expand All @@ -96,7 +107,7 @@ func GenerateGatewayConfiguration(gatewayConfigurationNSN types.NamespacedName)
Containers: []corev1.Container{
{
Name: consts.DataPlaneProxyContainerName,
Image: helpers.GetDefaultDataPlaneImage(),
Image: GetDefaultDataPlaneImage(),
ReadinessProbe: &corev1.Probe{
FailureThreshold: 3,
InitialDelaySeconds: 0,
Expand All @@ -105,7 +116,7 @@ func GenerateGatewayConfiguration(gatewayConfigurationNSN types.NamespacedName)
TimeoutSeconds: 1,
ProbeHandler: corev1.ProbeHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/status",
Path: "/status/ready",
Port: intstr.FromInt(consts.DataPlaneMetricsPort),
Scheme: corev1.URISchemeHTTP,
},
Expand All @@ -121,3 +132,72 @@ func GenerateGatewayConfiguration(gatewayConfigurationNSN types.NamespacedName)
},
}
}

// GenerateHTTPRoute generates an HTTPRoute to be used in tests
func GenerateHTTPRoute(namespace string, gatewayName, serviceName string, opts ...func(*gatewayv1.HTTPRoute)) *gatewayv1.HTTPRoute {
httpRoute := &gatewayv1.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: uuid.NewString(),
Annotations: map[string]string{
"konghq.com/strip-path": "true",
},
},
Spec: gatewayv1.HTTPRouteSpec{
CommonRouteSpec: gatewayv1.CommonRouteSpec{
ParentRefs: []gatewayv1.ParentReference{{
Name: gatewayv1.ObjectName(gatewayName),
}},
},
Rules: []gatewayv1.HTTPRouteRule{
{
Matches: []gatewayv1.HTTPRouteMatch{
{
Path: &gatewayv1.HTTPPathMatch{
Type: lo.ToPtr(gatewayv1.PathMatchPathPrefix),
Value: lo.ToPtr("/test"),
},
},
},
BackendRefs: []gatewayv1.HTTPBackendRef{
{
BackendRef: gatewayv1.BackendRef{
BackendObjectReference: gatewayv1.BackendObjectReference{
Name: gatewayv1.ObjectName(serviceName),
Port: lo.ToPtr(gatewayv1.PortNumber(80)),
Kind: lo.ToPtr(gatewayv1.Kind("Service")),
},
},
},
},
},
},
},
}

for _, opt := range opts {
opt(httpRoute)
}

return httpRoute
}

// MustGenerateTLSSecret generates a TLS secret to be used in tests
func MustGenerateTLSSecret(t *testing.T, namespace, secretName string, hosts []string) *corev1.Secret {
var serverKey, serverCert bytes.Buffer
require.NoError(t, generateRSACert(hosts, &serverKey, &serverCert), "failed to generate RSA certificate")

data := map[string][]byte{
corev1.TLSCertKey: serverCert.Bytes(),
corev1.TLSPrivateKeyKey: serverKey.Bytes(),
}

return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: secretName,
},
Type: corev1.SecretTypeTLS,
Data: data,
}
}
86 changes: 86 additions & 0 deletions test/helpers/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package helpers

import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net/http"
"testing"
"time"

"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
)

// MustCreateHTTPClient creates an HTTP client with the given TLS secret and host
func MustCreateHTTPClient(t *testing.T, tlsSecret *corev1.Secret, host string) *http.Client {
httpClient, err := createHTTPClient(tlsSecret, host)
assert.NoError(t, err)
return httpClient
}

// CreateHTTPClient creates an HTTP client with the given TLS secret and host and returns an error if it fails
func CreateHTTPClient(tlsSecret *corev1.Secret, host string) (*http.Client, error) {
return createHTTPClient(tlsSecret, host)
}

func createHTTPClient(tlsSecret *corev1.Secret, host string) (*http.Client, error) {
var tlsClientConfig *tls.Config
var err error
if tlsSecret != nil {
tlsClientConfig, err = createTLSClientConfig(tlsSecret, host)
if err != nil {
return nil, err
}
}
return &http.Client{
Timeout: time.Second * 10,
Transport: &http.Transport{
TLSClientConfig: tlsClientConfig,
},
}, nil
}

func createTLSClientConfig(tlsSecret *corev1.Secret, server string) (*tls.Config, error) {
certPem, ok := tlsSecret.Data["tls.crt"]
if !ok {
return nil, errors.New("tls.crt not found in secret")
}
keyPem, ok := tlsSecret.Data["tls.key"]
if !ok {
return nil, errors.New("tls.key not found in secret")
}
if server == "" {
return nil, errors.New("server name required for TLS")
}

cert, err := tls.X509KeyPair(certPem, keyPem)
if err != nil {
return nil, fmt.Errorf("unexpected error creating X509KeyPair: %w", err)
}

certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(certPem) {
return nil, errors.New("unexpected error adding trusted CA")
}

// Disable G402: TLS MinVersion too low. (gosec)
// #nosec G402
return &tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: server,
RootCAs: certPool,
}, nil
}

// MustBuildRequest creates an HTTP request with the given method, URL, and host
func MustBuildRequest(t *testing.T, ctx context.Context, method, url, host string) *http.Request {
req, err := http.NewRequestWithContext(ctx, method, url, nil)
assert.NoError(t, err)
if host != "" {
req.Host = host
}
return req
}
Loading

0 comments on commit cbdd513

Please sign in to comment.