Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
4 changes: 2 additions & 2 deletions api/v1/ingress_gateway_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ type IngressGatewaySpec struct {

// GatewayClassName selects the GatewayClass for the Gateway resource.
// If not set and the GatewayAPI CR has exactly one class, that class is
// used. If not set and multiple classes exist, the controller sets a
// warning requiring the user to specify one.
// used. If not set and multiple classes exist, the component degrades
// until the user specifies one.
// +optional
GatewayClassName *string `json:"gatewayClassName,omitempty"`
}
Expand Down
7 changes: 7 additions & 0 deletions api/v1/whisker_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ type WhiskerSpec struct {
// Allowed values are Enabled or Disabled. Defaults to Enabled.
// +optional
Notifications *NotificationMode `json:"notifications,omitempty"`

// IngressGateway configures Calico Ingress Gateway access to the Whisker UI.
// When set, the operator renders Gateway API resources (Gateway, HTTPRoute,
// Backend, ReferenceGrant, TLS Secret) to expose Whisker via CIG.
// Requires a GatewayAPI CR to be present.
// +optional
IngressGateway *IngressGatewaySpec `json:"ingressGateway,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
5 changes: 5 additions & 0 deletions api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pkg/apis/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
aggregator "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
gateway "sigs.k8s.io/gateway-api/apis/v1"
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
csisecret "sigs.k8s.io/secrets-store-csi-driver/apis/v1"
)

Expand All @@ -69,6 +70,7 @@ func init() {
AddToSchemes = append(AddToSchemes, policyv1.SchemeBuilder.AddToScheme)
AddToSchemes = append(AddToSchemes, policyv1beta1.SchemeBuilder.AddToScheme)
AddToSchemes = append(AddToSchemes, gateway.Install)
AddToSchemes = append(AddToSchemes, gatewayv1beta1.Install)
AddToSchemes = append(AddToSchemes, envoy.AddToScheme)
// EnvoyFilter is a hand-rolled shim type defined in pkg/render/istio (used
// for waypoint L7 logging); register it centrally like the other types.
Expand Down
10 changes: 8 additions & 2 deletions pkg/controller/gatewayapi/gatewayapi_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -690,8 +690,9 @@ func (r *ReconcileGatewayAPI) maintainFinalizer(ctx context.Context, gatewayAPI

// reconcileGatewayNamespaceResources writes the per-namespace resources owned by the namespace's
// Gateways, so the GC removes them once the last Gateway is gone (and the GatewayAPI CR's deletion
// doesn't strand them). Reserved namespaces are skipped; the trust bundle is written for every
// variant, and the variant's extension adds whatever else the namespace needs.
// doesn't strand them). Reserved namespaces are skipped; the trust bundle and operator-secrets
// RoleBinding are written for every variant, and the variant's extension adds whatever else the
// namespace needs.
// Each object is written once per owning Gateway, because the component handler takes a single
// owner. MultipleOwnersLabel makes it merge that owner reference into the references already on the
// object instead of replacing them, which is what keeps the namespace's other Gateways — and any
Expand Down Expand Up @@ -726,6 +727,11 @@ func gatewayNamespaceObjects(namespace string, bundle certificatemanagement.Trus
if bundle != nil {
objs = append(objs, bundle.ConfigMap(namespace))
}
// The operator needs secret CRUD in every gateway namespace on both
// variants: it places the UI gateway TLS secret there when
// spec.ingressGateway names a custom namespace (Manager on Enterprise,
// Whisker on Calico).
objs = append(objs, render.CreateOperatorSecretsRoleBinding(namespace))
objs = append(objs, extra...)
for _, obj := range objs {
labels := common.MapExistsOrInitialize(obj.GetLabels())
Expand Down
19 changes: 19 additions & 0 deletions pkg/controller/gatewayapi/gatewayapi_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
ctrlrfake "github.com/tigera/operator/pkg/ctrlruntime/client/fake"
"github.com/tigera/operator/pkg/dns"
egatewayapi "github.com/tigera/operator/pkg/enterprise/gatewayapi"
"github.com/tigera/operator/pkg/extensions"
"github.com/tigera/operator/pkg/render"
"github.com/tigera/operator/pkg/render/gatewayapi"
"github.com/tigera/operator/pkg/tls/certificatemanagement"
Expand Down Expand Up @@ -747,6 +748,24 @@ var _ = Describe("Gateway API controller tests", func() {
Expect(c.Get(ctx, client.ObjectKey{Name: gatewayapi.GatewayClassName}, &gapi.GatewayClass{})).NotTo(HaveOccurred())
})

It("writes the bundle and operator-secrets RoleBinding, but not WAF resources, on Calico", func() {
r.newComponentHandler = utils.NewComponentHandler
r.ext = extensions.Extensions{}.GatewayAPI()
bundle, err := certificatemanagement.CreateTrustedBundleWithSystemRootCertificates(nil)
Expect(err).NotTo(HaveOccurred())
gateways := []gapi.Gateway{
{ObjectMeta: metav1.ObjectMeta{Namespace: "app-ns", Name: "gw1", UID: "u1"}, Spec: gapi.GatewaySpec{GatewayClassName: gatewayapi.GatewayClassName}},
}
Expect(r.reconcileGatewayNamespaceResources(ctx, bundle, nil, gateways, map[string]bool{gatewayapi.GatewayClassName: true})).NotTo(HaveOccurred())

// The operator needs secret CRUD for the UI gateway TLS secret on
// both variants; the WAF SA/RoleBinding are Enterprise-only.
Expect(c.Get(ctx, client.ObjectKey{Namespace: "app-ns", Name: certificatemanagement.TrustedCertConfigMapName}, &corev1.ConfigMap{})).NotTo(HaveOccurred())
Expect(c.Get(ctx, client.ObjectKey{Namespace: "app-ns", Name: "tigera-operator-secrets"}, &rbacv1.RoleBinding{})).NotTo(HaveOccurred())
Expect(apierrors.IsNotFound(c.Get(ctx, client.ObjectKey{Namespace: "app-ns", Name: "waf-http-filter"}, &corev1.ServiceAccount{}))).To(BeTrue())
Expect(apierrors.IsNotFound(c.Get(ctx, client.ObjectKey{Namespace: "app-ns", Name: "waf-http-filter-gateway-resources"}, &rbacv1.RoleBinding{}))).To(BeTrue())
})

It("writes per-namespace Gateway resources owned by the namespace's Gateways (Enterprise)", func() {
// These go through the real component handler, which is what merges each Gateway's
// owner reference in rather than replacing what is already there.
Expand Down
108 changes: 0 additions & 108 deletions pkg/controller/manager/gateway_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,118 +15,10 @@
package manager

import (
"context"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
gapi "sigs.k8s.io/gateway-api/apis/v1"

"github.com/tigera/operator/pkg/apis"
ctrlrfake "github.com/tigera/operator/pkg/ctrlruntime/client/fake"
)

var _ = Describe("gatewayUnhealthyReason", func() {
const ns = "calico-system"

var (
ctx context.Context
r *ReconcileManager
)

cond := func(t string, status metav1.ConditionStatus, msg string) metav1.Condition {
return metav1.Condition{Type: t, Status: status, Message: msg, Reason: "Test", LastTransitionTime: metav1.Now()}
}

newGateway := func(conds ...metav1.Condition) *gapi.Gateway {
return &gapi.Gateway{
ObjectMeta: metav1.ObjectMeta{Name: ManagerGatewayResourcePrefix + "-gateway", Namespace: ns},
Status: gapi.GatewayStatus{Conditions: conds},
}
}

newRoute := func(conds ...metav1.Condition) *gapi.HTTPRoute {
route := &gapi.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{Name: ManagerGatewayResourcePrefix + "-route", Namespace: ns},
}
if len(conds) > 0 {
route.Status.Parents = []gapi.RouteParentStatus{{Conditions: conds}}
}
return route
}

healthyGateway := func() *gapi.Gateway {
return newGateway(
cond(string(gapi.GatewayConditionAccepted), metav1.ConditionTrue, ""),
cond(string(gapi.GatewayConditionProgrammed), metav1.ConditionTrue, ""),
)
}

build := func(objs ...client.Object) {
scheme := runtime.NewScheme()
Expect(apis.AddToScheme(scheme, false)).NotTo(HaveOccurred())
cli := ctrlrfake.DefaultFakeClientBuilder(scheme).WithObjects(objs...).Build()
r = &ReconcileManager{client: cli}
}

BeforeEach(func() {
ctx = context.Background()
})

It("reports a missing Gateway", func() {
build()
Expect(r.gatewayUnhealthyReason(ctx, ns)).To(ContainSubstring("not found yet"))
})

It("reports Gateway not accepted", func() {
build(newGateway(cond(string(gapi.GatewayConditionAccepted), metav1.ConditionFalse, "invalid listener")))
Expect(r.gatewayUnhealthyReason(ctx, ns)).To(Equal("Gateway not accepted: invalid listener"))
})

It("reports Gateway not programmed", func() {
build(newGateway(
cond(string(gapi.GatewayConditionAccepted), metav1.ConditionTrue, ""),
cond(string(gapi.GatewayConditionProgrammed), metav1.ConditionFalse, "no addresses assigned"),
))
Expect(r.gatewayUnhealthyReason(ctx, ns)).To(Equal("Gateway not programmed: no addresses assigned"))
})

It("treats missing Gateway conditions as healthy and moves on to the HTTPRoute", func() {
build(newGateway(), newRoute())
Expect(r.gatewayUnhealthyReason(ctx, ns)).To(BeEmpty())
})

It("reports a missing HTTPRoute once the Gateway is healthy", func() {
build(healthyGateway())
Expect(r.gatewayUnhealthyReason(ctx, ns)).To(ContainSubstring("HTTPRoute"))
Expect(r.gatewayUnhealthyReason(ctx, ns)).To(ContainSubstring("not found yet"))
})

It("reports HTTPRoute not accepted", func() {
build(healthyGateway(), newRoute(cond(string(gapi.RouteConditionAccepted), metav1.ConditionFalse, "no matching parent")))
Expect(r.gatewayUnhealthyReason(ctx, ns)).To(Equal("HTTPRoute not accepted: no matching parent"))
})

It("reports HTTPRoute refs not resolved", func() {
build(healthyGateway(), newRoute(
cond(string(gapi.RouteConditionAccepted), metav1.ConditionTrue, ""),
cond(string(gapi.RouteConditionResolvedRefs), metav1.ConditionFalse, "backend not permitted"),
))
Expect(r.gatewayUnhealthyReason(ctx, ns)).To(Equal("HTTPRoute refs not resolved: backend not permitted"))
})

It("returns empty when the Gateway and HTTPRoute are healthy", func() {
build(healthyGateway(), newRoute(
cond(string(gapi.RouteConditionAccepted), metav1.ConditionTrue, ""),
cond(string(gapi.RouteConditionResolvedRefs), metav1.ConditionTrue, ""),
))
Expect(r.gatewayUnhealthyReason(ctx, ns)).To(BeEmpty())
})
})

var _ = DescribeTable("managerDomainHost",
func(input, expected string) {
Expect(managerDomainHost(input)).To(Equal(expected))
Expand Down
Loading