Skip to content
Draft
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
9 changes: 7 additions & 2 deletions config/pomerium/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ rules:
- ""
resources:
- services
- endpoints
- secrets
verbs:
- get
Expand All @@ -19,9 +18,15 @@ rules:
resources:
- services/status
- secrets/status
- endpoints/status
verbs:
- get
- apiGroups:
- discovery.k8s.io
resources:
- endpointslices
verbs:
- list
- watch
- apiGroups:
- networking.k8s.io
resources:
Expand Down
17 changes: 9 additions & 8 deletions controllers/ingress/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"time"

corev1 "k8s.io/api/core/v1"
discoveryv1 "k8s.io/api/discovery/v1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
Expand Down Expand Up @@ -56,12 +57,12 @@ type ingressController struct {
globalSettings *types.NamespacedName

// object Kinds are frequently used, do not change and are cached
endpointsKind string
ingressKind string
ingressClassKind string
secretKind string
serviceKind string
settingsKind string
endpointSliceKind string
ingressKind string
ingressClassKind string
secretKind string
serviceKind string
settingsKind string

initComplete *once
}
Expand Down Expand Up @@ -129,7 +130,7 @@ func (r *ingressController) SetupWithManager(mgr ctrl.Manager) error {
r.ingressKind = generic.GVKForType[*networkingv1.Ingress](r.Scheme).Kind
r.serviceKind = generic.GVKForType[*corev1.Service](r.Scheme).Kind
r.settingsKind = generic.GVKForType[*icsv1.Pomerium](r.Scheme).Kind
r.endpointsKind = generic.GVKForType[*corev1.Endpoints](r.Scheme).Kind
r.endpointSliceKind = generic.GVKForType[*discoveryv1.EndpointSlice](r.Scheme).Kind
r.ingressClassKind = generic.GVKForType[*networkingv1.IngressClass](r.Scheme).Kind

err := ctrl.NewControllerManagedBy(mgr).
Expand All @@ -144,7 +145,7 @@ func (r *ingressController) SetupWithManager(mgr ctrl.Manager) error {
).
Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(r.getDependantIngressFn(r.secretKind))).
Watches(&corev1.Service{}, handler.EnqueueRequestsFromMapFunc(r.getDependantIngressFn(r.serviceKind))).
Watches(&corev1.Endpoints{}, handler.EnqueueRequestsFromMapFunc(r.getDependantIngressFn(r.endpointsKind))).
Watches(&discoveryv1.EndpointSlice{}, handler.EnqueueRequestsFromMapFunc(r.getDependantIngressFn(r.endpointSliceKind))).
WithEventFilter(predicate.ResourceVersionChangedPredicate{}).
Complete(r)
if err != nil {
Expand Down
81 changes: 64 additions & 17 deletions controllers/ingress/controller_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ import (
"go.uber.org/zap/zaptest"
"google.golang.org/protobuf/proto"
corev1 "k8s.io/api/core/v1"
discoveryv1 "k8s.io/api/discovery/v1"
networkingv1 "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/apimachinery/pkg/util/intstr"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/config"
Expand Down Expand Up @@ -203,6 +205,12 @@ func (s *ControllerTestSuite) deleteAll() {
s.NoError(s.Client.Delete(ctx, &secrets.Items[i]))
}

endpointSlices := new(discoveryv1.EndpointSliceList)
s.NoError(s.Client.List(ctx, endpointSlices))
for i := range endpointSlices.Items {
s.NoError(s.Client.Delete(ctx, &endpointSlices.Items[i]))
}

settings := new(icsv1.PomeriumList)
s.NoError(s.Client.List(ctx, settings))
for i := range settings.Items {
Expand Down Expand Up @@ -252,7 +260,7 @@ func (s *ControllerTestSuite) createTestController(ctx context.Context, opts ...
type testObjs struct {
*networkingv1.IngressClass
*networkingv1.Ingress
*corev1.Endpoints
*discoveryv1.EndpointSlice
*corev1.Service
*corev1.Secret
}
Expand Down Expand Up @@ -296,13 +304,23 @@ func (s *ControllerTestSuite) initialTestObjects(namespace string) *testObjs {
}},
},
},
&corev1.Endpoints{
&discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
Name: "service",
Name: "service-slice",
Namespace: namespace,
Labels: map[string]string{
discoveryv1.LabelServiceName: "service",
},
},
Subsets: []corev1.EndpointSubset{{
Addresses: []corev1.EndpointAddress{{IP: "1.2.3.4"}},
AddressType: discoveryv1.AddressTypeIPv4,
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"1.2.3.4"},
Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)},
}},
Ports: []discoveryv1.EndpointPort{{
Name: ptr.To("http"),
Port: ptr.To(int32(80)),
Protocol: ptr.To(corev1.ProtocolTCP),
}},
},
&corev1.Service{
Expand Down Expand Up @@ -339,7 +357,7 @@ func (s *ControllerTestSuite) TestIngressClass() {
s.createTestController(ctx)

to := s.initialTestObjects("default")
ingressClass, ingress, endpoints, service := to.IngressClass, to.Ingress, to.Endpoints, to.Service
ingressClass, ingress, endpoints, service := to.IngressClass, to.Ingress, to.EndpointSlice, to.Service

s.Run("no ingress class", func() {
ingress.Spec.IngressClassName = nil
Expand Down Expand Up @@ -405,7 +423,7 @@ func (s *ControllerTestSuite) TestDependencies() {
s.createTestController(ctx)

to := s.initialTestObjects("default")
ingressClass, ingress, endpoints, service, secret := to.IngressClass, to.Ingress, to.Endpoints, to.Service, to.Secret
ingressClass, ingress, endpoints, service, secret := to.IngressClass, to.Ingress, to.EndpointSlice, to.Service, to.Secret
svcName := types.NamespacedName{Name: "service", Namespace: "default"}
secretName := types.NamespacedName{Name: "secret", Namespace: "default"}

Expand Down Expand Up @@ -437,14 +455,33 @@ func (s *ControllerTestSuite) TestDependencies() {
s.EventuallyUpsert(func(ic *model.IngressConfig) string {
return cmp.Diff(secret, ic.Secrets[secretName], cmpOpts...)
}, "updated secret")

// update endpoint slice - verify that changes to EndpointSlice trigger reconciliation
s.NoError(s.Client.Get(ctx, types.NamespacedName{Name: endpoints.Name, Namespace: endpoints.Namespace}, endpoints))
endpoints.Endpoints[0].Addresses = []string{"5.6.7.8"}
s.NoError(s.Client.Update(ctx, endpoints))
s.EventuallyUpsert(func(ic *model.IngressConfig) string {
ep := ic.Endpoints[svcName]
if ep == nil || len(ep.Subsets) == 0 {
return "no endpoints"
}
for _, subset := range ep.Subsets {
for _, addr := range subset.Addresses {
if addr.IP == "5.6.7.8" {
return ""
}
}
}
return "expected IP 5.6.7.8 not found in endpoints"
}, "updated endpoint slice")
}

func (s *ControllerTestSuite) TestAnnotationDependencies() {
ctx := context.Background()
s.createTestController(ctx)

to := s.initialTestObjects("default")
ingressClass, ingress, endpoints, service, secret := to.IngressClass, to.Ingress, to.Endpoints, to.Service, to.Secret
ingressClass, ingress, endpoints, service, secret := to.IngressClass, to.Ingress, to.EndpointSlice, to.Service, to.Secret
ingress.Annotations = map[string]string{
fmt.Sprintf("%s/%s", ingress_controller.DefaultAnnotationPrefix, model.TLSCustomCASecret): "custom-ca",
fmt.Sprintf("%s/%s", ingress_controller.DefaultAnnotationPrefix, model.TLSClientSecret): "client",
Expand Down Expand Up @@ -509,7 +546,7 @@ func (s *ControllerTestSuite) TestNamespaces() {

for ns, shouldCreate := range namespaces {
to := s.initialTestObjects(ns)
ingress, endpoints, service, secret := to.Ingress, to.Endpoints, to.Service, to.Secret
ingress, endpoints, service, secret := to.Ingress, to.EndpointSlice, to.Service, to.Secret
for _, obj := range []client.Object{
&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}},
ingress, endpoints, service, secret,
Expand Down Expand Up @@ -557,7 +594,7 @@ func (s *ControllerTestSuite) TestIngressStatus() {
},
}
to := s.initialTestObjects("default")
class, ingress, endpoints, service, secret := to.IngressClass, to.Ingress, to.Endpoints, to.Service, to.Secret
class, ingress, endpoints, service, secret := to.IngressClass, to.Ingress, to.EndpointSlice, to.Service, to.Secret
del := func(obj client.Object) { s.Client.Delete(ctx, obj) }
for _, obj := range []client.Object{
ns, proxySvc,
Expand Down Expand Up @@ -631,7 +668,7 @@ func (s *ControllerTestSuite) TestIngressAddressWithNodeport() {
},
}
to := s.initialTestObjects("default")
class, ingress, endpoints, service, secret := to.IngressClass, to.Ingress, to.Endpoints, to.Service, to.Secret
class, ingress, endpoints, service, secret := to.IngressClass, to.Ingress, to.EndpointSlice, to.Service, to.Secret
del := func(obj client.Object) { s.Client.Delete(ctx, obj) }
for _, obj := range []client.Object{
ns, proxySvc,
Expand Down Expand Up @@ -704,13 +741,23 @@ func (s *ControllerTestSuite) TestHttp01Solver() {
},
}

endpoints := &corev1.Endpoints{
endpointSlice := &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
Name: "service",
Name: "service-slice",
Namespace: "default",
Labels: map[string]string{
discoveryv1.LabelServiceName: "service",
},
},
Subsets: []corev1.EndpointSubset{{
Addresses: []corev1.EndpointAddress{{IP: "1.2.3.4"}},
AddressType: discoveryv1.AddressTypeIPv4,
Endpoints: []discoveryv1.Endpoint{{
Addresses: []string{"1.2.3.4"},
Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)},
}},
Ports: []discoveryv1.EndpointPort{{
Name: ptr.To("http"),
Port: ptr.To(int32(8089)),
Protocol: ptr.To(corev1.ProtocolTCP),
}},
}
service := &corev1.Service{
Expand All @@ -731,7 +778,7 @@ func (s *ControllerTestSuite) TestHttp01Solver() {
// ingress should not be picked up unless there's a certificate
s.NoError(s.Client.Create(ctx, ingressClass))
s.NoError(s.Client.Create(ctx, ingress))
s.NoError(s.Client.Create(ctx, endpoints))
s.NoError(s.Client.Create(ctx, endpointSlice))
s.NoError(s.Client.Create(ctx, service))

s.EventuallyUpsert(func(ic *model.IngressConfig) string {
Expand All @@ -743,7 +790,7 @@ func (s *ControllerTestSuite) TestCustomSecrets() {
ctx := context.Background()
s.createTestController(ctx)
to := s.initialTestObjects("default")
ingressClass, ingress, endpoints, service, secret := to.IngressClass, to.Ingress, to.Endpoints, to.Service, to.Secret
ingressClass, ingress, endpoints, service, secret := to.IngressClass, to.Ingress, to.EndpointSlice, to.Service, to.Secret
ingress.Annotations = map[string]string{
fmt.Sprintf("%s/%s", ingress_controller.DefaultAnnotationPrefix, model.SetRequestHeadersSecret): "request-headers",
fmt.Sprintf("%s/%s", ingress_controller.DefaultAnnotationPrefix, model.SetResponseHeadersSecret): "response-headers",
Expand Down
18 changes: 9 additions & 9 deletions controllers/ingress/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ func TestManagingIngressClass(t *testing.T) {
ctx := context.Background()
mc := controllers_mock.NewMockClient(gomock.NewController(t))
ctrl := ingressController{
controllerName: pomeriumControllerName,
annotationPrefix: DefaultAnnotationPrefix,
Client: mc,
endpointsKind: "Endpoints",
ingressKind: "Ingress",
ingressClassKind: "IngressClass",
secretKind: "Secret",
serviceKind: "Service",
initComplete: newOnce(func(_ context.Context) error { return nil }),
controllerName: pomeriumControllerName,
annotationPrefix: DefaultAnnotationPrefix,
Client: mc,
endpointSliceKind: "EndpointSlice",
ingressKind: "Ingress",
ingressClassKind: "IngressClass",
secretKind: "Secret",
serviceKind: "Service",
initComplete: newOnce(func(_ context.Context) error { return nil }),
}

testCases := []struct {
Expand Down
20 changes: 19 additions & 1 deletion controllers/ingress/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"reflect"

discoveryv1 "k8s.io/api/discovery/v1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -23,8 +24,25 @@ func (r *ingressController) getDependantIngressFn(kind string) handler.MapFunc {
return nil
}

lookupKind := kind
name := types.NamespacedName{Name: a.GetName(), Namespace: a.GetNamespace()}
deps := r.DepsOfKind(model.Key{Kind: kind, NamespacedName: name}, r.ingressKind)

// For EndpointSlice, look up dependencies by the associated Service name.
// EndpointSlices have a label "kubernetes.io/service-name" that identifies
// which Service they belong to. Dependencies are registered under the Service,
// not the EndpointSlice, so we need to translate the lookup.
if kind == r.endpointSliceKind {
serviceName := a.GetLabels()[discoveryv1.LabelServiceName]
if serviceName == "" {
log.FromContext(ctx).V(1).Info("EndpointSlice missing service-name label",
"endpointSlice", name)
return nil
}
name.Name = serviceName
lookupKind = r.serviceKind
}

deps := r.DepsOfKind(model.Key{Kind: lookupKind, NamespacedName: name}, r.ingressKind)
reqs := make([]reconcile.Request, 0, len(deps))
for _, k := range deps {
reqs = append(reqs, reconcile.Request{NamespacedName: k.NamespacedName})
Expand Down
Loading