Files
ngrok-operator/pkg/managerdriver/driver_test.go
T
Jonathan Stacks 981805a7aa [Breaking Change!] Remove Deprecated CRDs (#664)
* chore!: Remove TCPEdge CRD

* chore!: Remove TLSEdge CRD

* chore!: Remove HTTPSEdge, Tunnel, and NgrokModuleSet CRDs
2025-07-08 16:12:32 +00:00

1252 lines
38 KiB
Go

package managerdriver
import (
"encoding/json"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"k8s.io/apimachinery/pkg/util/intstr"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
common "github.com/ngrok/ngrok-operator/api/common/v1alpha1"
ingressv1alpha1 "github.com/ngrok/ngrok-operator/api/ingress/v1alpha1"
ngrokv1alpha1 "github.com/ngrok/ngrok-operator/api/ngrok/v1alpha1"
"github.com/ngrok/ngrok-operator/internal/controller"
"github.com/ngrok/ngrok-operator/internal/errors"
"github.com/ngrok/ngrok-operator/internal/testutils"
"github.com/ngrok/ngrok-operator/internal/trafficpolicy"
"github.com/ngrok/ngrok-operator/internal/util"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
)
const defaultManagerName = "ngrok-ingress-controller"
var _ = Describe("Driver", func() {
var driver *Driver
var scheme = runtime.NewScheme()
cname := "cnametarget.com"
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(ingressv1alpha1.AddToScheme(scheme))
utilruntime.Must(gatewayv1.Install(scheme))
utilruntime.Must(gatewayv1alpha2.Install(scheme))
utilruntime.Must(ngrokv1alpha1.AddToScheme(scheme))
BeforeEach(func() {
driver = NewDriver(
GinkgoLogr,
scheme,
testutils.DefaultControllerName,
types.NamespacedName{Name: defaultManagerName},
WithGatewayEnabled(false),
WithSyncAllowConcurrent(true),
)
})
Describe("Seed", func() {
It("Should not error", func() {
err := driver.Seed(GinkgoT().Context(), fake.NewClientBuilder().WithScheme(scheme).Build())
Expect(err).ToNot(HaveOccurred())
})
It("Should add all the found items to the store", func() {
i1 := testutils.NewTestIngressV1("test-ingress", "test-namespace")
i2 := testutils.NewTestIngressV1("test-ingress-2", "test-namespace")
ic1 := testutils.NewTestIngressClass("test-ingress-class", true, true)
ic2 := testutils.NewTestIngressClass("test-ingress-class-2", true, true)
d1 := testutils.NewDomainV1("test-domain.com", "test-namespace")
d2 := testutils.NewDomainV1("test-domain-2.com", "test-namespace")
c1 := testutils.NewCloudEndpoint()
c2 := testutils.NewCloudEndpoint()
obs := []runtime.Object{ic1, ic2, i1, i2, d1, d2, c1, c2}
c := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(obs...).Build()
err := driver.Seed(GinkgoT().Context(), c)
Expect(err).ToNot(HaveOccurred())
for _, obj := range obs {
foundObj, found, err := driver.store.Get(obj)
Expect(err).ToNot(HaveOccurred())
Expect(found).To(BeTrue())
Expect(foundObj).ToNot(BeNil())
Expect(foundObj).To(Equal(obj))
}
})
})
Describe("DeleteIngress", func() {
It("Should remove the ingress from the store", func() {
i1 := testutils.NewTestIngressV1("test-ingress", "test-namespace")
c := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(i1).Build()
err := driver.Seed(GinkgoT().Context(), c)
Expect(err).ToNot(HaveOccurred())
err = driver.DeleteNamedIngress(types.NamespacedName{
Namespace: "test-namespace",
Name: "test-ingress",
})
Expect(err).ToNot(HaveOccurred())
foundObj, found, err := driver.store.Get(i1)
Expect(err).ToNot(HaveOccurred())
Expect(found).To(BeFalse())
Expect(foundObj).To(BeNil())
})
})
Describe("Sync", func() {
Context("When there are no ingresses in the store", func() {
It("Should not create anything or error", func() {
c := fake.NewClientBuilder().WithScheme(scheme).Build()
err := driver.Sync(GinkgoT().Context(), c)
Expect(err).ToNot(HaveOccurred())
domains := &ingressv1alpha1.DomainList{}
err = c.List(GinkgoT().Context(), &ingressv1alpha1.DomainList{})
Expect(err).ToNot(HaveOccurred())
Expect(domains.Items).To(HaveLen(0))
agentendpoints := &ngrokv1alpha1.AgentEndpointList{}
err = c.List(GinkgoT().Context(), &ngrokv1alpha1.AgentEndpointList{})
Expect(err).ToNot(HaveOccurred())
Expect(agentendpoints.Items).To(HaveLen(0))
cloudendpoints := &ngrokv1alpha1.CloudEndpointList{}
err = c.List(GinkgoT().Context(), &ngrokv1alpha1.CloudEndpointList{})
Expect(err).ToNot(HaveOccurred())
Expect(cloudendpoints.Items).To(HaveLen(0))
})
})
Context("When the old edges mapping-strategy is used, it defaults to endpoint", func() {
It("Should create AgentEndpoints", func() {
i1 := testutils.NewTestIngressV1("test-ingress", "test-namespace")
if i1.Annotations == nil {
i1.Annotations = map[string]string{}
}
i1.Annotations["k8s.ngrok.com/mapping-strategy"] = "edges"
i2 := testutils.NewTestIngressV1("test-ingress-2", "test-namespace")
if i2.Annotations == nil {
i2.Annotations = map[string]string{}
}
i2.Annotations["k8s.ngrok.com/mapping-strategy"] = "edges"
ic1 := testutils.NewTestIngressClass("test-ingress-class", true, true)
ic2 := testutils.NewTestIngressClass("test-ingress-class-2", true, true)
s := testutils.NewTestServiceV1("example", "test-namespace")
obs := []runtime.Object{ic1, ic2, i1, i2, s}
c := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(obs...).Build()
for _, obj := range obs {
err := driver.store.Update(obj)
Expect(err).ToNot(HaveOccurred())
}
err := driver.Seed(GinkgoT().Context(), c)
Expect(err).ToNot(HaveOccurred())
err = driver.Sync(GinkgoT().Context(), c)
Expect(err).ToNot(HaveOccurred())
foundDomain := &ingressv1alpha1.Domain{}
err = c.Get(GinkgoT().Context(), types.NamespacedName{
Namespace: "test-namespace",
Name: "example-com",
}, foundDomain)
Expect(err).ToNot(HaveOccurred())
Expect(foundDomain.Spec.Domain).To(Equal(i1.Spec.Rules[0].Host))
agentEndpoints := &ngrokv1alpha1.AgentEndpointList{}
err = c.List(GinkgoT().Context(), agentEndpoints, client.InNamespace("test-namespace"))
Expect(err).ToNot(HaveOccurred())
Expect(len(agentEndpoints.Items)).To(Equal(1))
agentEndpoint := agentEndpoints.Items[0]
Expect(agentEndpoint.Spec.URL).To(Equal("https://" + i1.Spec.Rules[0].Host))
})
})
When("A service specifies an appProtocol", func() {
var (
httpService *v1.Service
httpsService *v1.Service
ingress *netv1.Ingress
c client.WithWatch
namespace = "app-proto-namespace"
agentEndpoints *ngrokv1alpha1.AgentEndpointList
cloudEndpoints *ngrokv1alpha1.CloudEndpointList
ic = testutils.NewTestIngressClass("app-proto-ingress-class", true, true)
setIngressTargetService = func(i *netv1.Ingress, s *v1.Service) {
// Modify the ingress to include the service
i.Spec.Rules = []netv1.IngressRule{
{
Host: "foo.ngrok.io",
IngressRuleValue: netv1.IngressRuleValue{
HTTP: &netv1.HTTPIngressRuleValue{
Paths: []netv1.HTTPIngressPath{
{
Path: "/",
PathType: ptr.To(netv1.PathTypePrefix),
Backend: netv1.IngressBackend{
Service: &netv1.IngressServiceBackend{
Name: s.Name,
Port: netv1.ServiceBackendPort{
Name: s.Spec.Ports[0].Name,
},
},
},
},
},
},
},
},
}
}
)
BeforeEach(func() {
httpService = &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "http-service",
Namespace: namespace,
},
Spec: v1.ServiceSpec{
Ports: []v1.ServicePort{
{
Port: 80,
Name: "http",
TargetPort: intstr.FromInt(80),
},
},
},
}
httpsService = &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "https-service",
Namespace: namespace,
Annotations: map[string]string{
"k8s.ngrok.com/app-protocols": `{"https": "https"}`,
},
},
Spec: v1.ServiceSpec{
Ports: []v1.ServicePort{
{
Port: 443,
Name: "https",
TargetPort: intstr.FromInt(443),
},
},
},
}
ingress = &netv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ingress",
Namespace: namespace,
Annotations: map[string]string{"k8s.ngrok.com/mapping-strategy": "edges"},
},
Spec: netv1.IngressSpec{
IngressClassName: &ic.Name,
Rules: []netv1.IngressRule{},
},
}
})
JustBeforeEach(func() {
// Add the services and ingress to the fake client and the store
objs := []runtime.Object{ic, httpService, httpsService, ingress}
c = fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objs...).Build()
for _, obj := range objs {
Expect(driver.store.Update(obj)).To(BeNil())
}
// Seed & Sync
Expect(driver.Seed(GinkgoT().Context(), c)).To(BeNil())
Expect(driver.Sync(GinkgoT().Context(), c)).To(BeNil())
// Find the agent endpoints in this namespace
agentEndpoints = &ngrokv1alpha1.AgentEndpointList{}
err := c.List(GinkgoT().Context(), agentEndpoints, client.InNamespace(namespace))
Expect(err).ToNot(HaveOccurred())
// Find the cloud endpoints in this namespace
cloudEndpoints = &ngrokv1alpha1.CloudEndpointList{}
err = c.List(GinkgoT().Context(), cloudEndpoints, client.InNamespace(namespace))
Expect(err).ToNot(HaveOccurred())
})
When("The appProtocol is unknown", func() {
BeforeEach(func() {
// Set an unknown appProtocol on the httpService
httpService.Spec.Ports[0].AppProtocol = ptr.To("unknown")
// Modify the ingress to include the httpService
setIngressTargetService(ingress, httpService)
})
It("Should ignore the unknown appProtocol", func() {
// We expect one agent endpoint to be created
Expect(len(agentEndpoints.Items)).To(Equal(1))
By("Creating an agent endpoint with no appProtocol and the correct upstream")
foundAgentEndpoint := agentEndpoints.Items[0]
Expect(foundAgentEndpoint.Spec.Upstream.ProxyProtocolVersion).To(BeNil())
Expect(foundAgentEndpoint.Spec.Upstream.URL).To(Equal("http://http-service.app-proto-namespace:80"))
Expect(foundAgentEndpoint.Spec.Upstream.Protocol).To(BeNil())
})
})
When("The appProtocol is http", func() {
BeforeEach(func() {
// Set the appProtocol on the httpService
httpService.Spec.Ports[0].AppProtocol = ptr.To("http")
// Modify the ingress to include the httpService
setIngressTargetService(ingress, httpService)
})
It("Should create an AgentEndpoint with appProtocol http1", func() {
// We expect one AgentEndpoint to be created
Expect(len(agentEndpoints.Items)).To(Equal(1))
By("Creating an AgentEndpoint with appProtocol http1")
foundAgentEndpoint := agentEndpoints.Items[0]
Expect(foundAgentEndpoint.Spec.Upstream.Protocol).To(Equal(ptr.To(common.ApplicationProtocol_HTTP1)))
Expect(foundAgentEndpoint.Spec.Upstream.URL).To(Equal("http://http-service.app-proto-namespace:80"))
})
})
When("The appProtocol is k8s.ngrok.com/http2", func() {
BeforeEach(func() {
// Set the appProtocol on the httpService
httpsService.Spec.Ports[0].AppProtocol = ptr.To("k8s.ngrok.com/http2")
// Modify the ingress to include the httpsService
setIngressTargetService(ingress, httpsService)
})
It("Should create an AgentEndpoint with an upstream protocol of http2", func() {
// We expect one AgentEndpoint to be created
Expect(len(agentEndpoints.Items)).To(Equal(1))
By("Creating an AgentEndpoint with appProtocol http2")
foundAgentEndpoint := agentEndpoints.Items[0]
Expect(foundAgentEndpoint.Spec.Upstream.Protocol).To(Equal(ptr.To(common.ApplicationProtocol_HTTP2)))
Expect(foundAgentEndpoint.Spec.Upstream.URL).To(Equal("https://https-service.app-proto-namespace:443"))
})
})
When("The appProtocol is kubernetes.io/h2c", func() {
BeforeEach(func() {
// Set the appProtocol on the httpService
httpsService.Spec.Ports[0].AppProtocol = ptr.To("kubernetes.io/h2c")
// Modify the ingress to include the httpsService
setIngressTargetService(ingress, httpsService)
})
It("Should create an AgentEndpoint with appProtocol http2", func() {
// We expect one AgentEndpoint to be created
Expect(len(agentEndpoints.Items)).To(Equal(1))
By("Creating an AgentEndpoint with appProtocol http2")
foundAgentEndpoint := agentEndpoints.Items[0]
Expect(foundAgentEndpoint.Spec.Upstream.Protocol).To(Equal(ptr.To(common.ApplicationProtocol_HTTP2)))
Expect(foundAgentEndpoint.Spec.Upstream.URL).To(Equal("https://https-service.app-proto-namespace:443"))
})
})
})
When("An ingress specifies a traffic policy", func() {
var (
c client.WithWatch
namespace = "edge-tp-test-namespace"
httpService *v1.Service
ingress *netv1.Ingress
trafficPolicy *ngrokv1alpha1.NgrokTrafficPolicy
foundAgentEndpoints *ngrokv1alpha1.AgentEndpointList
foundCloudEndpoints *ngrokv1alpha1.CloudEndpointList
ic = testutils.NewTestIngressClass("edge-tp-ingress-class", true, true)
)
BeforeEach(func() {
pol := trafficpolicy.NewTrafficPolicy()
pol.AddRuleOnHTTPRequest(trafficpolicy.Rule{
Name: "test-name",
Actions: []trafficpolicy.Action{
trafficpolicy.NewCompressResponseAction(nil),
},
})
rawPolicy, err := json.Marshal(pol)
Expect(err).ToNot(HaveOccurred())
trafficPolicy = &ngrokv1alpha1.NgrokTrafficPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "test-policy",
Namespace: namespace,
},
Spec: ngrokv1alpha1.NgrokTrafficPolicySpec{
Policy: rawPolicy,
},
}
httpService = &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "http-service",
Namespace: namespace,
},
Spec: v1.ServiceSpec{
Ports: []v1.ServicePort{
{
Port: 80,
Name: "http",
TargetPort: intstr.FromInt(80),
},
},
},
}
ingress = &netv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ingress",
Namespace: namespace,
},
Spec: netv1.IngressSpec{
IngressClassName: &ic.Name,
Rules: []netv1.IngressRule{
{
Host: "foo.ngrok.io",
IngressRuleValue: netv1.IngressRuleValue{
HTTP: &netv1.HTTPIngressRuleValue{
Paths: []netv1.HTTPIngressPath{
{
Path: "/",
PathType: ptr.To(netv1.PathTypePrefix),
Backend: netv1.IngressBackend{
Service: &netv1.IngressServiceBackend{
Name: httpService.Name,
Port: netv1.ServiceBackendPort{
Name: httpService.Spec.Ports[0].Name,
},
},
},
},
},
},
},
},
},
},
}
})
JustBeforeEach(func() {
// Add the services and ingress to the fake client and the store
objs := []runtime.Object{ic, trafficPolicy, httpService, ingress}
c = fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objs...).Build()
for _, obj := range objs {
Expect(driver.store.Update(obj)).To(BeNil())
}
// Seed & Sync
Expect(driver.Seed(GinkgoT().Context(), c)).To(Succeed())
Expect(driver.Sync(GinkgoT().Context(), c)).To(Succeed())
// Find the AgentEndpoints in this namespace
foundAgentEndpoints = &ngrokv1alpha1.AgentEndpointList{}
Expect(c.List(GinkgoT().Context(), foundAgentEndpoints, client.InNamespace(namespace))).To(Succeed())
// Find the CloudEndpoints in this namespace
foundCloudEndpoints = &ngrokv1alpha1.CloudEndpointList{}
Expect(c.List(GinkgoT().Context(), foundCloudEndpoints, client.InNamespace(namespace))).To(Succeed())
})
When("The the ingress is using the old edges mapping strategy", func() {
BeforeEach(func() {
controller.AddAnnotations(ingress, map[string]string{
"k8s.ngrok.com/mapping-strategy": "edges",
})
})
It("Should create an AgentEndpoint", func() {
Expect(len(foundAgentEndpoints.Items)).To(Equal(1))
Expect(len(foundCloudEndpoints.Items)).To(Equal(0))
})
When("The traffic policy exists", func() {
BeforeEach(func() {
controller.AddAnnotations(ingress, map[string]string{
"k8s.ngrok.com/traffic-policy": trafficPolicy.Name,
})
})
It("Should use the traffic policy", func() {
foundAgentEndpoint := foundAgentEndpoints.Items[0]
By("Having the traffic policy on the AgentEndpoint")
Expect(foundAgentEndpoint.Spec.TrafficPolicy.Inline).ToNot(BeNil())
pol, err := trafficpolicy.NewTrafficPolicyFromJSON(foundAgentEndpoint.Spec.TrafficPolicy.Inline)
Expect(err).ToNot(HaveOccurred())
Expect(pol.OnHTTPRequest).To(ContainElement(
trafficpolicy.Rule{
Name: "test-name",
Actions: []trafficpolicy.Action{
{
Type: "compress-response",
Config: map[string]interface{}{},
},
},
},
))
})
})
})
When("The ingress is using the default mapping strategy", func() {
It("Should only create an AgentEndpoint", func() {
Expect(len(foundAgentEndpoints.Items)).To(Equal(1))
Expect(len(foundCloudEndpoints.Items)).To(Equal(0))
})
When("The traffic policy exists", func() {
BeforeEach(func() {
controller.AddAnnotations(ingress, map[string]string{
"k8s.ngrok.com/traffic-policy": trafficPolicy.Name,
})
})
It("Should include the traffic policy", func() {
agentEndpoint := foundAgentEndpoints.Items[0]
pol, err := trafficpolicy.NewTrafficPolicyFromJSON(agentEndpoint.Spec.TrafficPolicy.Inline)
Expect(err).ToNot(HaveOccurred())
Expect(pol.OnHTTPRequest).To(ContainElement(
trafficpolicy.Rule{
Name: "test-name",
Actions: []trafficpolicy.Action{
{
Type: "compress-response",
Config: map[string]interface{}{},
},
},
},
))
})
})
})
})
When("The defaultDomainReclaimPolicy is set", func() {
var (
defaultDomainReclaimPolicy ingressv1alpha1.DomainReclaimPolicy
objs []runtime.Object
c client.WithWatch
)
BeforeEach(func() {
objs = []runtime.Object{
testutils.NewTestIngressClass("test-ingress-class", true, true),
testutils.NewTestIngressV1("test-ingress", "test-namespace"),
testutils.NewTestServiceV1("example", "test-namespace"),
}
})
JustBeforeEach(func(ctx SpecContext) {
driver = NewDriver(
GinkgoLogr,
scheme,
testutils.DefaultControllerName,
types.NamespacedName{Name: defaultManagerName},
WithGatewayEnabled(false),
WithSyncAllowConcurrent(true),
WithDefaultDomainReclaimPolicy(defaultDomainReclaimPolicy),
)
c = fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objs...).Build()
Expect(driver.Seed(ctx, c)).To(Succeed())
Expect(driver.Sync(ctx, c)).To(Succeed())
})
When("policy is Delete", func() {
BeforeEach(func() {
defaultDomainReclaimPolicy = ingressv1alpha1.DomainReclaimPolicyDelete
})
When("no domains exist", func() {
It("should create new domains with ReclaimPolicy set to Delete", func(ctx SpecContext) {
domains := &ingressv1alpha1.DomainList{}
Expect(c.List(ctx, domains)).To(Succeed())
Expect(domains.Items).To(HaveLen(1))
Expect(domains.Items[0].Spec.ReclaimPolicy).To(Equal(ingressv1alpha1.DomainReclaimPolicyDelete))
})
})
When("a domain already exists", func() {
BeforeEach(func() {
d := testutils.NewDomainV1("example.com", "test-namespace")
d.ObjectMeta.SetCreationTimestamp(metav1.Now())
d.Spec.ReclaimPolicy = ingressv1alpha1.DomainReclaimPolicyRetain
objs = append(objs, d)
})
It("should not modify the reclaim policy of existing domains", func(ctx SpecContext) {
domains := &ingressv1alpha1.DomainList{}
Expect(c.List(ctx, domains)).To(Succeed())
Expect(domains.Items).To(HaveLen(1))
Expect(domains.Items[0].Spec.ReclaimPolicy).To(Equal(ingressv1alpha1.DomainReclaimPolicyRetain))
})
})
})
When("policy is Retain", func() {
BeforeEach(func() {
defaultDomainReclaimPolicy = ingressv1alpha1.DomainReclaimPolicyRetain
})
When("no domains exist", func() {
It("should create new domains with ReclaimPolicy set to Retain", func(ctx SpecContext) {
domains := &ingressv1alpha1.DomainList{}
Expect(c.List(ctx, domains)).To(Succeed())
Expect(domains.Items).To(HaveLen(1))
Expect(domains.Items[0].Spec.ReclaimPolicy).To(Equal(ingressv1alpha1.DomainReclaimPolicyRetain))
})
})
When("a domain already exists", func() {
BeforeEach(func() {
d := testutils.NewDomainV1("example.com", "test-namespace")
d.ObjectMeta.SetCreationTimestamp(metav1.Now())
d.Spec.ReclaimPolicy = ingressv1alpha1.DomainReclaimPolicyDelete
objs = append(objs, d)
})
It("should not modify the reclaim policy of existing domains", func(ctx SpecContext) {
domains := &ingressv1alpha1.DomainList{}
Expect(c.List(ctx, domains)).To(Succeed())
Expect(domains.Items).To(HaveLen(1))
Expect(domains.Items[0].Spec.ReclaimPolicy).To(Equal(ingressv1alpha1.DomainReclaimPolicyDelete))
})
})
})
})
})
Describe("calculateIngressLoadBalancerIPStatus", func() {
var domains []ingressv1alpha1.Domain
var ingress *netv1.Ingress
var c client.WithWatch
var status []netv1.IngressLoadBalancerIngress
JustBeforeEach(func() {
c = fake.NewClientBuilder().
WithLists(
&ingressv1alpha1.DomainList{
Items: domains,
},
).
WithScheme(scheme).
Build()
domainsByDomain, err := getDomainsByDomain(GinkgoT().Context(), c)
Expect(err).ToNot(HaveOccurred())
status = calculateIngressLoadBalancerIPStatus(ingress, domainsByDomain)
})
addIngressHostname := func(i *netv1.Ingress, hostname string) {
if i.Spec.Rules == nil {
i.Spec.Rules = []netv1.IngressRule{}
}
i.Spec.Rules = append(i.Spec.Rules, netv1.IngressRule{
Host: hostname,
})
}
newTestDomain := func(name, domain string, cnameTarget *string) ingressv1alpha1.Domain {
return ingressv1alpha1.Domain{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: ingressv1alpha1.DomainSpec{
Domain: domain,
},
Status: ingressv1alpha1.DomainStatus{
Domain: domain,
CNAMETarget: cnameTarget,
},
}
}
newTestDomainList := func(domains ...ingressv1alpha1.Domain) []ingressv1alpha1.Domain {
return domains
}
When("the CNAME is present", func() {
BeforeEach(func() {
ingress = testutils.NewTestIngressV1("test-ingress", "test-namespace")
domains = newTestDomainList(
newTestDomain(
"example-com",
"example.com",
&cname,
),
)
})
It("should return the CNAME as the status", func() {
Expect(len(status)).To(Equal(1))
Expect(status[0].Hostname).To(Equal(cname))
})
})
When("no matching domain is found", func() {
BeforeEach(func() {
ingress = testutils.NewTestIngressV1("test-ingress", "test-namespace")
domains = newTestDomainList(
newTestDomain(
"another-domain-com",
"another-domain.com",
&cname,
),
)
})
It("should return an empty status", func() {
Expect(len(status)).To(Equal(0))
})
})
When("the CNAME target is nil and the domain.status.domain is empty", func() {
BeforeEach(func() {
ingress = testutils.NewTestIngressV1("test-ingress", "test-namespace")
domains = newTestDomainList(
ingressv1alpha1.Domain{
ObjectMeta: metav1.ObjectMeta{
Name: "example-com",
},
Spec: ingressv1alpha1.DomainSpec{
Domain: "example.com",
},
Status: ingressv1alpha1.DomainStatus{},
},
)
})
It("should return an empty status", func() {
Expect(len(status)).To(Equal(0))
})
})
When("the domain is a non-wildcard ngrok managed domain", func() {
BeforeEach(func() {
ingress = testutils.NewTestIngressV1("test-ingress", "test-namespace")
ingress.Spec = netv1.IngressSpec{
Rules: []netv1.IngressRule{
{
Host: "example.ngrok.io",
},
},
}
domains = newTestDomainList(
newTestDomain(
"example-ngrok-io",
"example.ngrok.io",
nil,
),
)
})
It("should have a status hostname matching the domain", func() {
Expect(len(status)).To(Equal(1))
Expect(status[0].Hostname).To(Equal("example.ngrok.io"))
})
})
When("the domain is a wildcard ngrok managed domain", func() {
BeforeEach(func() {
ingress = testutils.NewTestIngressV1("test-ingress", "test-namespace")
ingress.Spec = netv1.IngressSpec{
Rules: []netv1.IngressRule{
{
Host: "*.example.ngrok.io",
},
},
}
domains = newTestDomainList(
newTestDomain(
"wildcard-example-ngrok-io",
"*.example.ngrok.io",
nil,
),
)
})
It("should have a .Status[].Hostname equal to the domain without the wildcard", func() {
Expect(len(status)).To(Equal(1))
Expect(status[0].Hostname).To(Equal("example.ngrok.io"))
})
})
When("There are multiple domains", func() {
cname1 := "cnametarget1.com"
cname2 := "cnametarget2.com"
BeforeEach(func() {
ingress = testutils.NewTestIngressV1("test-ingress", "test-namespace")
addIngressHostname(ingress, "test-domain1.com")
addIngressHostname(ingress, "test-domain2.com")
domains = newTestDomainList(
newTestDomain(
"test-domain1-com",
"test-domain1.com",
&cname1,
),
newTestDomain(
"test-domain2-com",
"test-domain2.com",
&cname2,
),
)
})
It("should return multiple statuses with those domains", func() {
Expect(status).Should(ConsistOf(
HaveField("Hostname", cname1),
HaveField("Hostname", cname2),
))
})
})
When("The ingress has multiple duplicate hostnames", func() {
cname1 := "cnametarget1.com"
cname2 := "cnametarget2.com"
BeforeEach(func() {
ingress = testutils.NewTestIngressV1("test-ingress", "test-namespace")
addIngressHostname(ingress, "test-domain1.com")
addIngressHostname(ingress, "test-domain1.com")
domains = newTestDomainList(
newTestDomain(
"test-domain1-com",
"test-domain1.com",
&cname1,
),
newTestDomain(
"test-domain2-com",
"test-domain2.com",
&cname2,
),
)
})
It("should only have a single status the unique domain", func() {
Expect(status).Should(ConsistOf(
HaveField("Hostname", cname1),
))
})
})
})
Describe("createEndpointPolicyForGateway", func() {
var rule *gatewayv1.HTTPRouteRule
var namespace string
var policyCrd *ngrokv1alpha1.NgrokTrafficPolicy
var legacyPolicyCrd *ngrokv1alpha1.NgrokTrafficPolicy
BeforeEach(func() {
rule = &gatewayv1.HTTPRouteRule{}
namespace = "test"
policyCrd = &ngrokv1alpha1.NgrokTrafficPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "test-policy",
Namespace: namespace,
},
Spec: ngrokv1alpha1.NgrokTrafficPolicySpec{
Policy: []byte(`{"on_http_request": [{"name":"t","actions":[{"type":"deny"}]}]}`),
},
}
Expect(driver.store.Add(policyCrd)).To(BeNil())
legacyPolicyCrd = &ngrokv1alpha1.NgrokTrafficPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "legacy-test-policy",
Namespace: namespace,
},
Spec: ngrokv1alpha1.NgrokTrafficPolicySpec{
Policy: []byte(`{"inbound": [{"name":"t","actions":[{"type":"deny"}]}], "outbound": []}`),
},
}
Expect(driver.store.Add(legacyPolicyCrd)).To(BeNil())
})
It("Should return an empty policy if the rule has nothing in it", func() {
policy, err := driver.createEndpointPolicyForGateway(rule, namespace)
Expect(err).To(BeNil())
Expect(policy).ToNot(BeNil())
})
It("Should return a merged policy if there rules with extensionRef", func() {
hostname := gatewayv1.PreciseHostname("test-hostname.com")
replacePrefixMatch := "/paprika"
rule.Filters = []gatewayv1.HTTPRouteFilter{
{
Type: "RequestHeaderModifier",
RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{
Add: []gatewayv1.HTTPHeader{
{
Name: "test-header",
Value: "test-value",
},
},
},
},
{
Type: "ExtensionRef",
ExtensionRef: &gatewayv1.LocalObjectReference{
Name: "test-policy",
Kind: "NgrokTrafficPolicy",
Group: "ngrok.k8s.ngrok.com",
},
},
{
Type: "URLRewrite",
URLRewrite: &gatewayv1.HTTPURLRewriteFilter{
Hostname: &hostname,
Path: &gatewayv1.HTTPPathModifier{
Type: "ReplacePrefixMatch",
ReplacePrefixMatch: &replacePrefixMatch,
},
},
},
}
expectedPolicy := `{"on_http_request":[{"name":"Inbound HTTPRouteRule 1","actions":[{"type":"add-headers","config":{"headers":{"test-header":"test-value"}}}]},{"actions":[{"type":"deny"}],"name":"t"},{"name":"Inbound HTTPRouteRule 2","actions":[{"type":"add-headers","config":{"headers":{"Host":"test-hostname.com"}}}]}]}`
policy, err := driver.createEndpointPolicyForGateway(rule, namespace)
Expect(err).To(BeNil())
Expect(policy).ToNot(BeNil())
jsonString, err := json.Marshal(policy)
Expect(err).To(BeNil())
Expect(string(jsonString)).To(Equal(expectedPolicy))
})
It("Should return a merged policy if there rules with extensionRef, legacy policy is remapped", func() {
hostname := gatewayv1.PreciseHostname("test-hostname.com")
replacePrefixMatch := "/paprika"
rule.Filters = []gatewayv1.HTTPRouteFilter{
{
Type: "RequestHeaderModifier",
RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{
Add: []gatewayv1.HTTPHeader{
{
Name: "test-header",
Value: "test-value",
},
},
},
},
{
Type: "ExtensionRef",
ExtensionRef: &gatewayv1.LocalObjectReference{
Name: "legacy-test-policy",
Kind: "NgrokTrafficPolicy",
Group: "ngrok.k8s.ngrok.com",
},
},
{
Type: "URLRewrite",
URLRewrite: &gatewayv1.HTTPURLRewriteFilter{
Hostname: &hostname,
Path: &gatewayv1.HTTPPathModifier{
Type: "ReplacePrefixMatch",
ReplacePrefixMatch: &replacePrefixMatch,
},
},
},
}
expectedPolicy := `{"on_http_request":[{"name":"Inbound HTTPRouteRule 1","actions":[{"type":"add-headers","config":{"headers":{"test-header":"test-value"}}}]},{"actions":[{"type":"deny"}],"name":"t"},{"name":"Inbound HTTPRouteRule 2","actions":[{"type":"add-headers","config":{"headers":{"Host":"test-hostname.com"}}}]}]}`
policy, err := driver.createEndpointPolicyForGateway(rule, namespace)
Expect(err).To(BeNil())
Expect(policy).ToNot(BeNil())
jsonString, err := json.Marshal(policy)
Expect(err).To(BeNil())
Expect(string(jsonString)).To(Equal(expectedPolicy))
})
})
Describe("When not running concurrently", func() {
It("starts one", func() {
proceed, wait := driver.syncStart(false)
Expect(proceed).To(BeTrue())
Expect(wait).To(BeNil())
driver.syncDone()
})
It("second waits, then returns error", func() {
firstProceed, _ := driver.syncStart(false)
Expect(firstProceed).To(BeTrue())
secondProceed, secondWait := driver.syncStart(false)
Expect(secondProceed).To(BeFalse())
Expect(secondWait).To(Not(BeNil()))
driver.syncDone()
err := secondWait(GinkgoT().Context())
Expect(err).To(Equal(errSyncDone))
})
It("third releases second, no error", func() {
firstProceed, _ := driver.syncStart(false)
Expect(firstProceed).To(BeTrue())
secondProceed, secondWait := driver.syncStart(false)
Expect(secondProceed).To(BeFalse())
Expect(secondWait).To(Not(BeNil()))
thirdProceed, thirdWait := driver.syncStart(false)
Expect(thirdProceed).To(BeFalse())
Expect(thirdWait).To(Not(BeNil()))
secondErr := secondWait(GinkgoT().Context())
Expect(secondErr).To(BeNil())
driver.syncDone()
err := thirdWait(GinkgoT().Context())
Expect(err).To(Equal(errSyncDone))
})
It("partial third does not wait, no error", func() {
firstProceed, _ := driver.syncStart(true)
Expect(firstProceed).To(BeTrue())
secondProceed, secondWait := driver.syncStart(false)
Expect(secondProceed).To(BeFalse())
Expect(secondWait).To(Not(BeNil()))
thirdProceed, thirdWait := driver.syncStart(true)
Expect(thirdProceed).To(BeFalse())
Expect(thirdWait).To(Not(BeNil()))
thirdErr := thirdWait(GinkgoT().Context())
Expect(thirdErr).To(BeNil())
driver.syncDone()
err := secondWait(GinkgoT().Context())
Expect(err).To(Equal(errSyncDone))
})
})
Describe("When ingresses are opted in to use edges", func() {
It("Should create edges and not endpoints", func() {
ic1 := testutils.NewTestIngressClass("test-ingress-class", true, true)
i1 := testutils.NewTestIngressV1WithClass("ingress-1", "test-namespace", ic1.Name)
if i1.Annotations == nil {
i1.Annotations = map[string]string{}
}
i1.Annotations["k8s.ngrok.com/mapping-strategy"] = "edges"
i1.Spec.Rules = []netv1.IngressRule{
{
Host: "a.customdomain.com",
IngressRuleValue: netv1.IngressRuleValue{
HTTP: &netv1.HTTPIngressRuleValue{
Paths: []netv1.HTTPIngressPath{
{
Path: "/",
Backend: netv1.IngressBackend{
Service: &netv1.IngressServiceBackend{
Name: "test-service",
Port: netv1.ServiceBackendPort{
Number: 80,
},
},
},
},
{
Path: "/api",
Backend: netv1.IngressBackend{
Service: &netv1.IngressServiceBackend{
Name: "api-service",
Port: netv1.ServiceBackendPort{
Number: 80,
},
},
},
},
},
},
},
},
{
Host: "b.customdomain.com",
IngressRuleValue: netv1.IngressRuleValue{
HTTP: &netv1.HTTPIngressRuleValue{
Paths: []netv1.HTTPIngressPath{
{
Path: "/b/",
Backend: netv1.IngressBackend{
Service: &netv1.IngressServiceBackend{
Name: "b-service",
Port: netv1.ServiceBackendPort{
Number: 80,
},
},
},
},
},
},
},
},
}
i2 := testutils.NewTestIngressV1WithClass("ingress-2", "other-namespace", ic1.Name)
if i2.Annotations == nil {
i2.Annotations = map[string]string{}
}
i2.Annotations["k8s.ngrok.com/mapping-strategy"] = "edges"
i2.Spec.Rules = []netv1.IngressRule{
{
Host: "c.customdomain.com",
IngressRuleValue: netv1.IngressRuleValue{
HTTP: &netv1.HTTPIngressRuleValue{
Paths: []netv1.HTTPIngressPath{
{
Path: "/",
Backend: netv1.IngressBackend{
Service: &netv1.IngressServiceBackend{
Name: "test-service",
Port: netv1.ServiceBackendPort{
Number: 80,
},
},
},
},
},
},
},
},
{
Host: "d.customdomain.com",
IngressRuleValue: netv1.IngressRuleValue{
HTTP: &netv1.HTTPIngressRuleValue{
Paths: []netv1.HTTPIngressPath{
{
Path: "/",
Backend: netv1.IngressBackend{
Service: &netv1.IngressServiceBackend{
Name: "test-service",
Port: netv1.ServiceBackendPort{
Number: 80,
},
},
},
},
},
},
},
},
}
obs := []runtime.Object{ic1, i1, i2}
c := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(obs...).Build()
Expect(driver.Seed(GinkgoT().Context(), c)).To(BeNil())
domainSet := driver.calculateDomainSet()
Expect(domainSet.endpointIngressDomains).To(HaveLen(4))
Expect(domainSet.endpointIngressDomains).To(HaveKey("a.customdomain.com"))
Expect(domainSet.endpointIngressDomains).To(HaveKey("b.customdomain.com"))
Expect(domainSet.endpointIngressDomains).To(HaveKey("c.customdomain.com"))
Expect(domainSet.endpointIngressDomains).To(HaveKey("d.customdomain.com"))
})
})
})
func TestExtractPolicy(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
msg json.RawMessage
expectedTrafficPolicy map[string][]util.RawRule
expectedErr error
}{
{
name: "legacy policy configuration",
msg: []byte(`{"inbound":[{"name":"test-inbound","actions":[{"type":"deny"}]}],"outbound":[{"name":"test-outbound","actions":[{"type":"some-action"}]}]}`),
expectedTrafficPolicy: map[string][]util.RawRule{
util.PhaseOnHttpRequest: {
[]byte(`{"actions":[{"type":"deny"}],"name":"test-inbound"}`),
},
util.PhaseOnHttpResponse: {
[]byte(`{"actions":[{"type":"some-action"}],"name":"test-outbound"}`),
},
},
},
{
name: "phase-based policy config",
msg: []byte(`{"on_http_request":[{"name":"test-inbound","actions":[{"type":"deny"}]}],"on_http_response":[{"name":"test-outbound","actions":[{"type":"some-action"}]}]}`),
expectedTrafficPolicy: map[string][]util.RawRule{
util.PhaseOnHttpRequest: {
[]byte(`{"actions":[{"type":"deny"}],"name":"test-inbound"}`),
},
util.PhaseOnHttpResponse: {
[]byte(`{"actions":[{"type":"some-action"}],"name":"test-outbound"}`),
},
},
},
{
name: "invalid json message",
msg: []byte(`ngrok operates a global network where it accepts traffic to your upstream services from clients.`),
expectedErr: errors.New("invalid character 'g' in literal null (expecting 'u')"),
},
{
name: "empty json message",
msg: []byte(""),
expectedErr: errors.New("unexpected end of JSON input"),
},
{
name: "nil json message",
expectedErr: errors.New("unexpected end of JSON input"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
trafficPolicy, err := extractPolicy(tc.msg)
if tc.expectedTrafficPolicy == nil {
assert.Nil(t, trafficPolicy)
} else {
assert.Equal(t, tc.expectedTrafficPolicy, trafficPolicy.Deconstruct())
}
if tc.expectedErr == nil {
assert.NoError(t, err)
} else {
require.Error(t, err)
// Can't compare the exact error as we don't have access to json SyntaxError underlying `msg` field`
assert.Equal(t, tc.expectedErr.Error(), err.Error())
}
})
}
}