blob: d9d3063fbe4161a11a2e94add64e874a1f40a8a3 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ssl
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"testing"
"time"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
v1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1"
apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
"github.com/apache/apisix-ingress-controller/internal/controller/config"
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
internaltypes "github.com/apache/apisix-ingress-controller/internal/types"
)
const (
testNamespace = "default"
testIngressClass = "example-class"
)
func TestConflictDetectorDetectsGatewayConflict(t *testing.T) {
scheme := buildScheme(t)
secretA := newTLSSecret(t, "cert-a", []string{"example.com"})
secretB := newTLSSecret(t, "cert-b", []string{"example.com"})
gatewayProxy := &v1alpha1.GatewayProxy{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-gp",
Namespace: testNamespace,
UID: "gatewayproxy-uid",
},
}
modeTerminate := gatewayv1.TLSModeTerminate
hostname := gatewayv1.Hostname("example.com")
gateway := &gatewayv1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-gateway",
Namespace: testNamespace,
UID: "gateway-uid",
},
Spec: gatewayv1.GatewaySpec{
GatewayClassName: gatewayv1.ObjectName("demo-gc"),
Listeners: []gatewayv1.Listener{
{
Name: "tls",
Protocol: gatewayv1.HTTPSProtocolType,
Port: 443,
Hostname: &hostname,
TLS: &gatewayv1.GatewayTLSConfig{
Mode: &modeTerminate,
CertificateRefs: []gatewayv1.SecretObjectReference{
{Name: gatewayv1.ObjectName(secretA.Name)},
},
},
},
},
},
}
gateway.Spec.Infrastructure = &gatewayv1.GatewayInfrastructure{
ParametersRef: &gatewayv1.LocalParametersReference{
Group: gatewayv1.Group(v1alpha1.GroupVersion.Group),
Kind: gatewayv1.Kind(internaltypes.KindGatewayProxy),
Name: gatewayProxy.Name,
},
}
ingressClass := &networkingv1.IngressClass{
ObjectMeta: metav1.ObjectMeta{
Name: testIngressClass,
},
Spec: networkingv1.IngressClassSpec{
Controller: config.ControllerConfig.ControllerName,
Parameters: &networkingv1.IngressClassParametersReference{
APIGroup: ptr.To(v1alpha1.GroupVersion.Group),
Kind: internaltypes.KindGatewayProxy,
Name: gatewayProxy.Name,
Namespace: ptr.To(testNamespace),
},
},
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithIndex(&gatewayv1.Gateway{}, indexer.ParametersRef, indexer.GatewayParametersRefIndexFunc).
WithIndex(&gatewayv1.Gateway{}, indexer.TLSHostIndexRef, indexer.GatewayTLSHostIndexFunc).
WithIndex(&networkingv1.IngressClass{}, indexer.IngressClassParametersRef, indexer.IngressClassParametersRefIndexFunc).
WithIndex(&networkingv1.Ingress{}, indexer.IngressClassRef, indexer.IngressClassRefIndexFunc).
WithIndex(&networkingv1.Ingress{}, indexer.TLSHostIndexRef, indexer.IngressTLSHostIndexFunc).
WithIndex(&apiv2.ApisixTls{}, indexer.IngressClassRef, indexer.ApisixTlsIngressClassIndexFunc).
WithIndex(&apiv2.ApisixTls{}, indexer.TLSHostIndexRef, indexer.ApisixTlsHostIndexFunc).
WithObjects(secretA, secretB, gatewayProxy, gateway, ingressClass).
Build()
detector := NewConflictDetector(fakeClient)
ctx := context.Background()
newTls := &apiv2.ApisixTls{
ObjectMeta: metav1.ObjectMeta{
Name: "incoming",
Namespace: testNamespace,
UID: "apisixtls-uid",
},
Spec: apiv2.ApisixTlsSpec{
IngressClassName: testIngressClass,
Hosts: []apiv2.HostType{"example.com"},
Secret: apiv2.ApisixSecret{
Name: secretB.Name,
Namespace: secretB.Namespace,
},
},
}
conflicts := detector.DetectConflicts(ctx, newTls)
if len(conflicts) != 1 {
t.Fatalf("expected 1 conflict, got %d", len(conflicts))
}
conflict := conflicts[0]
if conflict.Host != "example.com" {
t.Fatalf("unexpected host: %s", conflict.Host)
}
expectedRef := fmt.Sprintf("Gateway/%s/%s", gateway.Namespace, gateway.Name)
if conflict.ConflictingResource != expectedRef {
t.Fatalf("unexpected conflicting resource: %s", conflict.ConflictingResource)
}
}
func TestConflictDetectorAllowedWhenCertificateMatches(t *testing.T) {
scheme := buildScheme(t)
secret := newTLSSecret(t, "shared-cert", []string{"shared.example.com"})
gatewayProxy := &v1alpha1.GatewayProxy{
ObjectMeta: metav1.ObjectMeta{
Name: "gp",
Namespace: testNamespace,
UID: "gatewayproxy-uid-2",
},
}
modeTerminate := gatewayv1.TLSModeTerminate
listenerHostname := gatewayv1.Hostname("shared.example.com")
gateway := &gatewayv1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: "gw",
Namespace: testNamespace,
UID: "gateway-uid-2",
},
Spec: gatewayv1.GatewaySpec{
GatewayClassName: gatewayv1.ObjectName("gc"),
Listeners: []gatewayv1.Listener{
{
Name: "tls",
Protocol: gatewayv1.HTTPSProtocolType,
Port: 443,
Hostname: &listenerHostname,
TLS: &gatewayv1.GatewayTLSConfig{
Mode: &modeTerminate,
CertificateRefs: []gatewayv1.SecretObjectReference{{Name: gatewayv1.ObjectName(secret.Name)}},
},
},
},
},
}
gateway.Spec.Infrastructure = &gatewayv1.GatewayInfrastructure{
ParametersRef: &gatewayv1.LocalParametersReference{
Group: gatewayv1.Group(v1alpha1.GroupVersion.Group),
Kind: gatewayv1.Kind(internaltypes.KindGatewayProxy),
Name: gatewayProxy.Name,
},
}
ingressClass := &networkingv1.IngressClass{
ObjectMeta: metav1.ObjectMeta{Name: testIngressClass},
Spec: networkingv1.IngressClassSpec{
Controller: config.ControllerConfig.ControllerName,
Parameters: &networkingv1.IngressClassParametersReference{
APIGroup: ptr.To(v1alpha1.GroupVersion.Group),
Kind: internaltypes.KindGatewayProxy,
Name: gatewayProxy.Name,
Namespace: ptr.To(testNamespace),
},
},
}
client := fake.NewClientBuilder().
WithScheme(scheme).
WithIndex(&gatewayv1.Gateway{}, indexer.ParametersRef, indexer.GatewayParametersRefIndexFunc).
WithIndex(&gatewayv1.Gateway{}, indexer.TLSHostIndexRef, indexer.GatewayTLSHostIndexFunc).
WithIndex(&networkingv1.IngressClass{}, indexer.IngressClassParametersRef, indexer.IngressClassParametersRefIndexFunc).
WithIndex(&networkingv1.Ingress{}, indexer.IngressClassRef, indexer.IngressClassRefIndexFunc).
WithIndex(&networkingv1.Ingress{}, indexer.TLSHostIndexRef, indexer.IngressTLSHostIndexFunc).
WithIndex(&apiv2.ApisixTls{}, indexer.IngressClassRef, indexer.ApisixTlsIngressClassIndexFunc).
WithIndex(&apiv2.ApisixTls{}, indexer.TLSHostIndexRef, indexer.ApisixTlsHostIndexFunc).
WithObjects(secret, gatewayProxy, gateway, ingressClass).
Build()
detector := NewConflictDetector(client)
ctx := context.Background()
newTls := &apiv2.ApisixTls{
ObjectMeta: metav1.ObjectMeta{
Name: "allowed",
Namespace: testNamespace,
UID: "apisixtls-uid-2",
},
Spec: apiv2.ApisixTlsSpec{
IngressClassName: testIngressClass,
Hosts: []apiv2.HostType{"shared.example.com"},
Secret: apiv2.ApisixSecret{Name: secret.Name, Namespace: secret.Namespace},
},
}
conflicts := detector.DetectConflicts(ctx, newTls)
if len(conflicts) != 0 {
t.Fatalf("expected no conflicts, got %v", conflicts)
}
}
func buildScheme(t *testing.T) *runtime.Scheme {
scheme := runtime.NewScheme()
for _, add := range []func(*runtime.Scheme) error{
corev1.AddToScheme,
networkingv1.AddToScheme,
gatewayv1.Install,
apiv2.AddToScheme,
v1alpha1.AddToScheme,
} {
if err := add(scheme); err != nil {
t.Fatalf("failed to add to scheme: %v", err)
}
}
return scheme
}
func newTLSSecret(t *testing.T, name string, hosts []string) *corev1.Secret {
cert, key := generateCertificate(t, hosts)
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: testNamespace,
},
Type: corev1.SecretTypeTLS,
Data: map[string][]byte{
corev1.TLSCertKey: cert,
corev1.TLSPrivateKeyKey: key,
},
}
}
func TestConflictDetectorDetectsSelfConflict(t *testing.T) {
scheme := buildScheme(t)
secretA := newTLSSecret(t, "cert-a", []string{"example.com"})
secretB := newTLSSecret(t, "cert-b", []string{"example.com"})
gatewayProxy := &v1alpha1.GatewayProxy{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-gp",
Namespace: testNamespace,
UID: "gatewayproxy-uid-3",
},
}
modeTerminate := gatewayv1.TLSModeTerminate
hostname := gatewayv1.Hostname("example.com")
// Create a Gateway with TWO listeners using DIFFERENT certificates for the SAME host
gateway := &gatewayv1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-gateway",
Namespace: testNamespace,
UID: "gateway-uid-3",
},
Spec: gatewayv1.GatewaySpec{
GatewayClassName: gatewayv1.ObjectName("demo-gc"),
Listeners: []gatewayv1.Listener{
{
Name: "tls-1",
Protocol: gatewayv1.HTTPSProtocolType,
Port: 443,
Hostname: &hostname,
TLS: &gatewayv1.GatewayTLSConfig{
Mode: &modeTerminate,
CertificateRefs: []gatewayv1.SecretObjectReference{
{Name: gatewayv1.ObjectName(secretA.Name)},
},
},
},
{
Name: "tls-2",
Protocol: gatewayv1.HTTPSProtocolType,
Port: 8443,
Hostname: &hostname,
TLS: &gatewayv1.GatewayTLSConfig{
Mode: &modeTerminate,
CertificateRefs: []gatewayv1.SecretObjectReference{
{Name: gatewayv1.ObjectName(secretB.Name)},
},
},
},
},
},
}
gateway.Spec.Infrastructure = &gatewayv1.GatewayInfrastructure{
ParametersRef: &gatewayv1.LocalParametersReference{
Group: gatewayv1.Group(v1alpha1.GroupVersion.Group),
Kind: gatewayv1.Kind(internaltypes.KindGatewayProxy),
Name: gatewayProxy.Name,
},
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithIndex(&gatewayv1.Gateway{}, indexer.ParametersRef, indexer.GatewayParametersRefIndexFunc).
WithIndex(&gatewayv1.Gateway{}, indexer.TLSHostIndexRef, indexer.GatewayTLSHostIndexFunc).
WithIndex(&networkingv1.IngressClass{}, indexer.IngressClassParametersRef, indexer.IngressClassParametersRefIndexFunc).
WithIndex(&networkingv1.Ingress{}, indexer.IngressClassRef, indexer.IngressClassRefIndexFunc).
WithIndex(&networkingv1.Ingress{}, indexer.TLSHostIndexRef, indexer.IngressTLSHostIndexFunc).
WithIndex(&apiv2.ApisixTls{}, indexer.IngressClassRef, indexer.ApisixTlsIngressClassIndexFunc).
WithIndex(&apiv2.ApisixTls{}, indexer.TLSHostIndexRef, indexer.ApisixTlsHostIndexFunc).
WithObjects(secretA, secretB, gatewayProxy, gateway).
Build()
detector := NewConflictDetector(fakeClient)
ctx := context.Background()
// Build mappings for this Gateway - should have 2 mappings for same host with different certs
mappings := detector.BuildGatewayMappings(ctx, gateway)
if len(mappings) != 2 {
t.Fatalf("expected 2 mappings, got %d", len(mappings))
}
// Both mappings should be for the same host
if mappings[0].Host != mappings[1].Host {
t.Fatalf("expected same host, got %s and %s", mappings[0].Host, mappings[1].Host)
}
// But with different certificate hashes
if mappings[0].CertificateHash == mappings[1].CertificateHash {
t.Fatalf("expected different certificate hashes, but they are the same: %s", mappings[0].CertificateHash)
}
// DetectConflicts should detect this self-conflict
conflicts := detector.DetectConflicts(ctx, gateway)
// Should detect 1 conflict (the resource conflicts with itself)
if len(conflicts) != 1 {
t.Fatalf("expected 1 self-conflict, got %d", len(conflicts))
}
conflict := conflicts[0]
if conflict.Host != "example.com" {
t.Fatalf("unexpected host: %s", conflict.Host)
}
// The conflicting resource should point to itself
expectedRef := fmt.Sprintf("Gateway/%s/%s", gateway.Namespace, gateway.Name)
if conflict.ConflictingResource != expectedRef {
t.Fatalf("unexpected conflicting resource: %s, expected %s", conflict.ConflictingResource, expectedRef)
}
}
func generateCertificate(t *testing.T, hosts []string) ([]byte, []byte) {
t.Helper()
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("failed to generate private key: %v", err)
}
serial, err := rand.Int(rand.Reader, big.NewInt(1<<62))
if err != nil {
t.Fatalf("failed to generate serial: %v", err)
}
template := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
CommonName: hosts[0],
},
DNSNames: hosts,
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
if err != nil {
t.Fatalf("failed to create certificate: %v", err)
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
return certPEM, keyPEM
}