Skip to content
Open
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
13 changes: 13 additions & 0 deletions pkg/controller/manager/manager_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import (
relasticsearch "github.com/tigera/operator/pkg/render/common/elasticsearch"
"github.com/tigera/operator/pkg/render/common/networkpolicy"
"github.com/tigera/operator/pkg/render/common/rbacmanagement"
"github.com/tigera/operator/pkg/render/common/wafmanagement"
rgateway "github.com/tigera/operator/pkg/render/gateway"
"github.com/tigera/operator/pkg/render/logstorage/eck"
rmanager "github.com/tigera/operator/pkg/render/manager"
Expand Down Expand Up @@ -232,6 +233,11 @@ func Add(mgr manager.Manager, opts options.ControllerOptions) error {
return fmt.Errorf("manager-controller failed to watch ConfigMap resource %s: %w", rbacmanagement.ConfigMapName, err)
}

// Watched so that toggling the WAF management UI rolls ui-apis with the new WAF_UI_ENABLED.
if err = utils.AddConfigMapWatch(c, wafmanagement.ConfigMapName, common.CalicoNamespace, eventHandler); err != nil {
return fmt.Errorf("manager-controller failed to watch ConfigMap resource %s: %w", wafmanagement.ConfigMapName, err)
}

if err = utils.AddConfigMapWatch(c, relasticsearch.ClusterConfigConfigMapName, common.OperatorNamespace(), eventHandler); err != nil {
return fmt.Errorf("manager-controller failed to watch the ConfigMap resource: %w", err)
}
Expand Down Expand Up @@ -738,6 +744,12 @@ func (r *ReconcileManager) Reconcile(ctx context.Context, request reconcile.Requ
return reconcile.Result{}, err
}

wafManagementEnabled, err := utils.WAFManagementEnabled(ctx, r.client, installationSpec.Variant, tenant.MultiTenant())
if err != nil {
r.status.SetDegraded(operatorv1.ResourceReadError, "Error reading the WAF management UI ConfigMap", err, logc)
return reconcile.Result{}, err
}

managerCfg := &render.ManagerConfiguration{
VoltronRouteConfig: routeConfig,
KeyValidatorConfig: keyValidatorConfig,
Expand Down Expand Up @@ -765,6 +777,7 @@ func (r *ReconcileManager) Reconcile(ctx context.Context, request reconcile.Requ
Authentication: authenticationCR,
KibanaEnabled: kibanaEnabled,
RBACManagementEnabled: rbacManagementEnabled,
WAFManagementEnabled: wafManagementEnabled,
CACertCommonName: certificateManager.CACertCommonName(),
Cloud: r.opts.Cloud,
CloudResources: mcr,
Expand Down
79 changes: 74 additions & 5 deletions pkg/controller/manager/manager_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import (
relasticsearch "github.com/tigera/operator/pkg/render/common/elasticsearch"
"github.com/tigera/operator/pkg/render/common/rbacmanagement"
rsecret "github.com/tigera/operator/pkg/render/common/secret"
"github.com/tigera/operator/pkg/render/common/wafmanagement"
"github.com/tigera/operator/pkg/render/logstorage/eck"
"github.com/tigera/operator/pkg/render/monitor"
tigeratls "github.com/tigera/operator/pkg/tls"
Expand Down Expand Up @@ -610,7 +611,7 @@ var _ = Describe("Manager controller tests", func() {
// rather than rendering as disabled.
It("degrades and requeues when the ConfigMap cannot be read", func() {
readErr := fmt.Errorf("the API server is having a bad day")
r.client = failingGateReadClient{Client: c, err: readErr}
r.client = failingGateReadClient{Client: c, name: rbacmanagement.ConfigMapName, err: readErr}
// The shared mockStatus expects a full reconcile, which this returns
// early from, so assert the one call.
mockStatus.On("SetDegraded", operatorv1.ResourceReadError,
Expand All @@ -623,6 +624,73 @@ var _ = Describe("Manager controller tests", func() {
})
})

// ui-apis reads WAF_UI_ENABLED at startup, so the controller's half is
// getting the admin's value onto the container.
Context("WAF management UI feature gate", func() {
writeGate := func(value string) {
Expect(c.Create(ctx, &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: wafmanagement.ConfigMapName,
Namespace: common.CalicoNamespace,
},
Data: map[string]string{wafmanagement.ConfigMapKey: value},
})).NotTo(HaveOccurred())
}

// wafUIEnv reconciles and reports the value handed to ui-apis.
wafUIEnv := func() string {
mockStatus.On("RemoveCertificateSigningRequests", mock.Anything).Return()
_, err := r.Reconcile(ctx, reconcile.Request{})
Expect(err).NotTo(HaveOccurred())

d := appsv1.Deployment{
TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{
Name: render.ManagerName,
Namespace: render.ManagerNamespace,
},
}
Expect(test.GetResource(c, &d)).To(BeNil())
uiAPIs := test.GetContainer(d.Spec.Template.Spec.Containers, render.UIAPIsName)
Expect(uiAPIs).NotTo(BeNil())
for _, e := range uiAPIs.Env {
if e.Name == "WAF_UI_ENABLED" {
return e.Value
}
}
Fail("WAF_UI_ENABLED is not set on the ui-apis container")
return ""
}

It("leaves the UI off when the admin has not created the ConfigMap", func() {
Expect(wafUIEnv()).To(Equal("false"))
})

It("turns the UI on once the admin enables the feature", func() {
writeGate("true")
Expect(wafUIEnv()).To(Equal("true"))
})

It("leaves the UI off when the admin sets the value to false", func() {
writeGate("false")
Expect(wafUIEnv()).To(Equal("false"))
})

// An unreadable ConfigMap is unknown state, not absent, so it degrades
// rather than rendering as disabled.
It("degrades and requeues when the ConfigMap cannot be read", func() {
readErr := fmt.Errorf("the API server is having a bad day")
r.client = failingGateReadClient{Client: c, name: wafmanagement.ConfigMapName, err: readErr}
mockStatus.On("SetDegraded", operatorv1.ResourceReadError,
"Error reading the WAF management UI ConfigMap", readErr.Error(), mock.Anything).Return().Once()

_, err := r.Reconcile(ctx, reconcile.Request{})
Expect(err).To(MatchError(readErr))
mockStatus.AssertCalled(GinkgoT(), "SetDegraded", operatorv1.ResourceReadError,
"Error reading the WAF management UI ConfigMap", readErr.Error(), mock.Anything)
})
})

It("should reconcile legacy manager namespace", func() {
result, err := r.Reconcile(ctx, reconcile.Request{})
Expect(err).NotTo(HaveOccurred())
Expand Down Expand Up @@ -1544,15 +1612,16 @@ var _ = Describe("Manager controller tests", func() {
})
})

// failingGateReadClient fails the read of the gate ConfigMap and passes everything else
// through, to distinguish an unreadable ConfigMap from an absent one.
// failingGateReadClient fails the read of the named gate ConfigMap and passes everything
// else through, to distinguish an unreadable ConfigMap from an absent one.
type failingGateReadClient struct {
client.Client
err error
name string
err error
}

func (f failingGateReadClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
if _, ok := obj.(*corev1.ConfigMap); ok && key.Name == rbacmanagement.ConfigMapName {
if _, ok := obj.(*corev1.ConfigMap); ok && key.Name == f.name {
return f.err
}
return f.Client.Get(ctx, key, obj, opts...)
Expand Down
18 changes: 18 additions & 0 deletions pkg/controller/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import (
"github.com/tigera/operator/pkg/ctrlruntime"
"github.com/tigera/operator/pkg/render"
"github.com/tigera/operator/pkg/render/common/rbacmanagement"
"github.com/tigera/operator/pkg/render/common/wafmanagement"
"github.com/tigera/operator/pkg/render/logstorage/eck"
)

Expand Down Expand Up @@ -577,6 +578,23 @@ func RBACManagementEnabled(ctx context.Context, c client.Client, variant operato
return rbacmanagement.Enabled(gate), nil
}

// WAFManagementEnabled reports whether the WAF management UI should be rendered.
// The feature is Enterprise-only and is not offered on multi-tenant management
// clusters. Otherwise the admin's switch decides; an absent ConfigMap reads as
// disabled.
func WAFManagementEnabled(ctx context.Context, c client.Client, variant operatorv1.ProductVariant, multiTenant bool) (bool, error) {
if !variant.IsEnterprise() || multiTenant {
return false, nil
}
gate, err := GetIfExists[corev1.ConfigMap](ctx, client.ObjectKey{
Name: wafmanagement.ConfigMapName, Namespace: common.CalicoNamespace,
}, c)
if err != nil {
return false, err
}
return wafmanagement.Enabled(gate), nil
}

// GetNonClusterHost finds the NonClusterHost CR in your cluster.
func GetNonClusterHost(ctx context.Context, cli client.Client) (*operatorv1.NonClusterHost, error) {
nonclusterhost := &operatorv1.NonClusterHost{}
Expand Down
5 changes: 3 additions & 2 deletions pkg/render/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
"github.com/tigera/operator/pkg/render/common/secret"
"github.com/tigera/operator/pkg/render/common/securitycontext"
"github.com/tigera/operator/pkg/render/common/securitycontextconstraints"
"github.com/tigera/operator/pkg/render/common/wafmanagement"
"github.com/tigera/operator/pkg/tls/certificatemanagement"
)

Expand Down Expand Up @@ -2222,7 +2223,7 @@ func (c *apiServerComponent) tigeraNetworkAdminClusterRole() *rbacv1.ClusterRole
},
}...)

// Write access to the switch, so a network admin can enable the feature without
// Write access to the switches, so a network admin can enable the features without
// cluster-admin. Not gated: a rule rendered only while the feature is on could never
// be used to turn it on. create cannot be restricted by resource name, so it admits
// creating any ConfigMap in the namespace.
Expand All @@ -2235,7 +2236,7 @@ func (c *apiServerComponent) tigeraNetworkAdminClusterRole() *rbacv1.ClusterRole
rbacv1.PolicyRule{
APIGroups: []string{""},
Resources: []string{"configmaps"},
ResourceNames: []string{rbacmanagement.ConfigMapName},
ResourceNames: []string{rbacmanagement.ConfigMapName, wafmanagement.ConfigMapName},
Verbs: []string{"get", "list", "watch", "update", "patch", "delete"},
},
)
Expand Down
7 changes: 4 additions & 3 deletions pkg/render/apiserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
"github.com/tigera/operator/pkg/render/common/podaffinity"
"github.com/tigera/operator/pkg/render/common/rbacmanagement"
rtest "github.com/tigera/operator/pkg/render/common/test"
"github.com/tigera/operator/pkg/render/common/wafmanagement"
"github.com/tigera/operator/pkg/render/testutils"
"github.com/tigera/operator/pkg/tls/certificatemanagement"
"github.com/tigera/operator/test"
Expand Down Expand Up @@ -417,7 +418,7 @@ var _ = Describe("API server rendering tests (Calico Enterprise)", func() {
{
APIGroups: []string{""},
Resources: []string{"configmaps"},
ResourceNames: []string{rbacmanagement.ConfigMapName},
ResourceNames: []string{rbacmanagement.ConfigMapName, wafmanagement.ConfigMapName},
Verbs: []string{"get", "list", "watch", "update", "patch", "delete"},
},
}
Expand Down Expand Up @@ -2026,7 +2027,7 @@ var (
ResourceNames: []string{"webhooks-secret"},
Verbs: []string{"patch"},
},
// Write access to the switch, ungated so it can be used to turn the feature on.
// Write access to the switches, ungated so they can be used to turn the features on.
{
APIGroups: []string{""},
Resources: []string{"configmaps"},
Expand All @@ -2035,7 +2036,7 @@ var (
{
APIGroups: []string{""},
Resources: []string{"configmaps"},
ResourceNames: []string{rbacmanagement.ConfigMapName},
ResourceNames: []string{rbacmanagement.ConfigMapName, wafmanagement.ConfigMapName},
Verbs: []string{"get", "list", "watch", "update", "patch", "delete"},
},
}
Expand Down
41 changes: 41 additions & 0 deletions pkg/render/common/wafmanagement/gate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2026 Tigera, Inc. All rights reserved.

// Licensed 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 wafmanagement reads the admin-owned gate that switches the WAF
// management UI on for a cluster.
package wafmanagement

import (
"strconv"

corev1 "k8s.io/api/core/v1"
)

const (
// ConfigMapName is the admin-owned switch for the WAF management UI. Unlike the
// RBAC gate, only the operator reads it: it is projected onto the ui-apis
// container as WAF_UI_ENABLED, so a toggle rolls the manager Deployment.
ConfigMapName = "waf-ui-config"
ConfigMapKey = "waf-ui-enabled"
)

// Enabled reports whether the WAF management UI is switched on for this cluster.
// A missing ConfigMap, missing key or unparsable value reads as disabled.
func Enabled(cm *corev1.ConfigMap) bool {
if cm == nil {
return false
}
enabled, err := strconv.ParseBool(cm.Data[ConfigMapKey])
return err == nil && enabled
}
41 changes: 41 additions & 0 deletions pkg/render/common/wafmanagement/gate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2026 Tigera, Inc. All rights reserved.

// Licensed 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 wafmanagement_test

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

corev1 "k8s.io/api/core/v1"

"github.com/tigera/operator/pkg/render/common/wafmanagement"
)

// The gate is hand-edited by an admin, so the parser has to be forgiving about
// spelling and strict about everything else: anything it cannot read as an explicit
// true leaves the WAF management UI switched off.
var _ = DescribeTable("Enabled",
func(cm *corev1.ConfigMap, expected bool) {
Expect(wafmanagement.Enabled(cm)).To(Equal(expected))
},
Entry("nil ConfigMap (never created, or deleted)", nil, false),
Entry("missing key", &corev1.ConfigMap{Data: map[string]string{}}, false),
Entry("explicitly disabled", &corev1.ConfigMap{Data: map[string]string{wafmanagement.ConfigMapKey: "false"}}, false),
Entry("enabled", &corev1.ConfigMap{Data: map[string]string{wafmanagement.ConfigMapKey: "true"}}, true),
Entry("enabled, capitalised", &corev1.ConfigMap{Data: map[string]string{wafmanagement.ConfigMapKey: "True"}}, true),
Entry("enabled as 1", &corev1.ConfigMap{Data: map[string]string{wafmanagement.ConfigMapKey: "1"}}, true),
Entry("unparsable value stays off", &corev1.ConfigMap{Data: map[string]string{wafmanagement.ConfigMapKey: "yes please"}}, false),
Entry("empty value stays off", &corev1.ConfigMap{Data: map[string]string{wafmanagement.ConfigMapKey: ""}}, false),
)
29 changes: 29 additions & 0 deletions pkg/render/common/wafmanagement/wafmanagement_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) 2026 Tigera, Inc. All rights reserved.

// Licensed 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 wafmanagement_test

import (
"testing"

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

func TestWAFManagement(t *testing.T) {
gomega.RegisterFailHandler(ginkgo.Fail)
suiteConfig, reporterConfig := ginkgo.GinkgoConfiguration()
reporterConfig.JUnitReport = "../../../../report/ut/wafmanagement_suite.xml"
ginkgo.RunSpecs(t, "pkg/render/common/wafmanagement Suite", suiteConfig, reporterConfig)
}
5 changes: 5 additions & 0 deletions pkg/render/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ type ManagerConfiguration struct {
// The controller has already applied the variant, the admin's gate and tenancy.
RBACManagementEnabled bool

// WAFManagementEnabled reports whether to serve the WAF management UI surface.
// The controller has already applied the variant, the admin's gate and tenancy.
WAFManagementEnabled bool

// CACertCommonName is the CommonName from the CA certificate used for operator-managed certificates.
// Passed to Voltron so it can identify the correct CA issuer public key.
CACertCommonName string
Expand Down Expand Up @@ -770,6 +774,7 @@ func (c *managerComponent) managerUIAPIsContainer() corev1.Container {
{Name: "LINSEED_CLIENT_KEY", Value: keyPath},
{Name: "ELASTIC_KIBANA_DISABLED", Value: strconv.FormatBool(c.cfg.Tenant.MultiTenant())},
{Name: "VOLTRON_URL", Value: ManagerService(c.cfg.Tenant)},
{Name: "WAF_UI_ENABLED", Value: strconv.FormatBool(c.cfg.WAFManagementEnabled)},
}

// Determine the Linseed location. Use code default unless in multi-tenant mode,
Expand Down
Loading
Loading