Skip to content
31 changes: 0 additions & 31 deletions api/v1/manager_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,37 +26,6 @@ type ManagerSpec struct {
// ManagerDeployment configures the Manager Deployment.
// +optional
ManagerDeployment *ManagerDeployment `json:"managerDeployment,omitempty"`

// RBACUI configures the RBAC management UI feature.
// +optional
RBACUI *RBACUI `json:"rbacUI,omitempty"`
}

// +kubebuilder:validation:Enum=Enabled;Disabled
type RBACUIStatusType string

const (
RBACUIDisabled RBACUIStatusType = "Disabled"
RBACUIEnabled RBACUIStatusType = "Enabled"
)

// RBACUI configures the RBAC management UI. This is a separate control plane
// for Calico Enterprise RBAC that lives alongside, and does not replace, the
// user's ability to configure RBAC themselves.
type RBACUI struct {
// State turns the RBAC management UI on or off. Defaults to Disabled.
// +optional
State *RBACUIStatusType `json:"state,omitempty"`
}

// RBACManagementEnabled returns true when the Manager CR enables the RBAC
// management UI. Safe to call on a nil receiver; returns false for a nil
// Manager, an unset RBACUI, or any state other than Enabled.
func (m *Manager) RBACManagementEnabled() bool {
if m == nil || m.Spec.RBACUI == nil || m.Spec.RBACUI.State == nil {
return false
}
return *m.Spec.RBACUI.State == RBACUIEnabled
}

// ManagerDeployment is the configuration for the Manager Deployment.
Expand Down
51 changes: 0 additions & 51 deletions api/v1/manager_types_test.go

This file was deleted.

25 changes: 0 additions & 25 deletions api/v1/zz_generated.deepcopy.go

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

33 changes: 18 additions & 15 deletions pkg/controller/apiserver/apiserver_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ func Add(mgr manager.Manager, opts options.ControllerOptions) error {
}

if opts.EnterpriseCRDExists {
// Watched so a toggle re-renders the rules gated on it.
if err = utils.AddConfigMapWatch(c, render.RBACManagementConfigMapName, common.CalicoNamespace, &handler.EnqueueRequestForObject{}); err != nil {
return fmt.Errorf("apiserver-controller failed to watch ConfigMap %s: %w", render.RBACManagementConfigMapName, err)
}

// Watch for changes to ApplicationLayer
err = c.WatchObject(&operatorv1.ApplicationLayer{ObjectMeta: metav1.ObjectMeta{Name: utils.DefaultEnterpriseInstanceKey.Name}}, &handler.EnqueueRequestForObject{})
if err != nil {
Expand All @@ -122,13 +127,6 @@ func Add(mgr manager.Manager, opts options.ControllerOptions) error {
return fmt.Errorf("apiserver-controller failed to watch primary resource: %v", err)
}

// Watch the Manager CR so toggling spec.rbac re-runs the apiserver
// reconcile (the tigera-network-admin RBAC is gated on it).
err = c.WatchObject(&operatorv1.Manager{}, &handler.EnqueueRequestForObject{})
if err != nil {
return fmt.Errorf("apiserver-controller failed to watch Manager: %v", err)
}

for _, namespace := range []string{common.OperatorNamespace(), render.APIServerNamespace} {
for _, secretName := range []string{render.VoltronTunnelSecretName, render.ManagerTLSSecretName} {
if err = utils.AddSecretsWatch(c, secretName, namespace); err != nil {
Expand Down Expand Up @@ -361,7 +359,7 @@ func (r *ReconcileAPIServer) Reconcile(ctx context.Context, request reconcile.Re
var managementCluster *operatorv1.ManagementCluster
var managementClusterConnection *operatorv1.ManagementClusterConnection
var keyValidatorConfig authentication.KeyValidatorConfig
var managerCR *operatorv1.Manager
var rbacManagementEnabled bool
includeV3NetworkPolicy := false

if installationSpec.Variant.IsEnterprise() {
Expand All @@ -372,6 +370,17 @@ func (r *ReconcileAPIServer) Reconcile(ctx context.Context, request reconcile.Re
return reconcile.Result{}, err
}

// The admin owns this ConfigMap; the operator only reads it, and an absent one
// reads as disabled.
gate, err := utils.GetIfExists[corev1.ConfigMap](ctx, client.ObjectKey{
Name: render.RBACManagementConfigMapName, Namespace: common.CalicoNamespace,
}, r.client)
if err != nil {
r.status.SetDegraded(operatorv1.ResourceReadError, "Error reading the RBAC management UI ConfigMap", err, reqLogger)
return reconcile.Result{}, err
}
rbacManagementEnabled = render.RBACManagementEnabled(gate)

applicationLayer, err = utils.GetApplicationLayer(ctx, r.client)
if err != nil {
r.status.SetDegraded(operatorv1.ResourceReadError, "Error reading ApplicationLayer", err, reqLogger)
Expand All @@ -390,12 +399,6 @@ func (r *ReconcileAPIServer) Reconcile(ctx context.Context, request reconcile.Re
return reconcile.Result{}, err
}

managerCR, err = utils.GetManager(ctx, r.client, false, "")
if err != nil {
r.status.SetDegraded(operatorv1.ResourceReadError, "Error reading Manager", err, reqLogger)
return reconcile.Result{}, err
}

if managementClusterConnection != nil && managementCluster != nil {
err = fmt.Errorf("having both a ManagementCluster and a ManagementClusterConnection is not supported")
r.status.SetDegraded(operatorv1.ResourceValidationError, "", err, reqLogger)
Expand Down Expand Up @@ -538,7 +541,7 @@ func (r *ReconcileAPIServer) Reconcile(ctx context.Context, request reconcile.Re
ClusterDomain: r.opts.ClusterDomain,
Cloud: r.opts.Cloud,
RequiresAggregationServer: !r.opts.UseV3CRDs,
RBACManagementEnabled: managerCR.RBACManagementEnabled(),
RBACManagementEnabled: rbacManagementEnabled,
QueryServerTLSKeyPairCertificateManagementOnly: queryServerTLSSecretCertificateManagementOnly,
}

Expand Down
121 changes: 121 additions & 0 deletions pkg/controller/apiserver/apiserver_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

v3 "github.com/tigera/api/pkg/apis/projectcalico/v3"
Expand Down Expand Up @@ -491,6 +492,126 @@ var _ = Describe("apiserver controller tests", func() {
})
})

// These cover the controller's half: reading the ConfigMap and handing the value to
// the renderer.
Context("RBAC management UI feature gate", func() {
// gatedRule is where the gate's value is observable in the rendered output.
gatedRule := rbacv1.PolicyRule{
APIGroups: []string{"rbac.authorization.k8s.io"},
Resources: []string{"clusterrolebindings", "rolebindings"},
Verbs: []string{"get", "list", "watch", "create", "update", "delete"},
}

writeGate := func(value string) {
Expect(cli.Create(ctx, &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: render.RBACManagementConfigMapName,
Namespace: common.CalicoNamespace,
},
Data: map[string]string{render.RBACManagementConfigMapKey: value},
})).NotTo(HaveOccurred())
}

// networkAdminRules reconciles and returns the rendered rules.
networkAdminRules := func() []rbacv1.PolicyRule {
r := ReconcileAPIServer{
client: cli,
scheme: scheme,
status: mockStatus,
tierWatchReady: ready,
migrationWatchReady: &utils.ReadyFlag{},
opts: options.ControllerOptions{
EnterpriseCRDExists: true,
DetectedProvider: operatorv1.ProviderNone,
},
}
_, err := r.Reconcile(ctx, reconcile.Request{})
Expect(err).ShouldNot(HaveOccurred())

cr := rbacv1.ClusterRole{}
Expect(cli.Get(ctx, client.ObjectKey{Name: "tigera-network-admin"}, &cr)).NotTo(HaveOccurred())
return cr.Rules
}

BeforeEach(func() {
Expect(cli.Create(ctx, installation)).NotTo(HaveOccurred())
})

It("withholds the rules when the admin has not created the ConfigMap", func() {
Expect(networkAdminRules()).NotTo(ContainElement(gatedRule))
})

It("adds the rules once the admin enables the feature", func() {
writeGate("true")
Expect(networkAdminRules()).To(ContainElement(gatedRule))
})

It("withholds the rules when the admin sets the value to false", func() {
writeGate("false")
Expect(networkAdminRules()).NotTo(ContainElement(gatedRule))
})

// A managed cluster carries tigera-network-admin too, so the read must not be
// skipped there.
It("reads the gate on a managed cluster", func() {
Expect(cli.Create(ctx, &operatorv1.ManagementClusterConnection{
ObjectMeta: metav1.ObjectMeta{Name: utils.DefaultEnterpriseInstanceKey.Name},
})).NotTo(HaveOccurred())
writeGate("true")

Expect(networkAdminRules()).To(ContainElement(gatedRule))
})

// 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")
failing := ctrlrfake.DefaultFakeClientBuilder(scheme).
WithInterceptorFuncs(interceptor.Funcs{
Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
if _, ok := obj.(*corev1.ConfigMap); ok && key.Name == render.RBACManagementConfigMapName {
return readErr
}
return c.Get(ctx, key, obj, opts...)
},
}).Build()

// Re-plant what the reconcile needs on the new client.
certificateManager, err := certificatemanager.Create(failing, nil, "cluster.local", common.OperatorNamespace(), certificatemanager.AllowCACreation())
Expect(err).NotTo(HaveOccurred())
Expect(failing.Create(ctx, certificateManager.KeyPair().Secret(common.OperatorNamespace()))).NotTo(HaveOccurred())
Expect(failing.Create(ctx, &operatorv1.APIServer{ObjectMeta: metav1.ObjectMeta{Name: "tigera-secure"}})).NotTo(HaveOccurred())
Expect(failing.Create(ctx, &v3.Tier{ObjectMeta: metav1.ObjectMeta{Name: "calico-system"}})).NotTo(HaveOccurred())
// The shared installation carries a resourceVersion that Create would reject.
freshInstallation := installation.DeepCopy()
freshInstallation.ResourceVersion = ""
Expect(failing.Create(ctx, freshInstallation)).NotTo(HaveOccurred())

degraded := &status.MockStatus{}
degraded.On("OnCRFound").Return()
degraded.On("SetMetaData", mock.Anything).Return()
degraded.On("AddCertificateSigningRequests", mock.Anything).Return().Maybe()
degraded.On("RemoveCertificateSigningRequests", mock.Anything).Return().Maybe()
degraded.On("SetDegraded", operatorv1.ResourceReadError,
"Error reading the RBAC management UI ConfigMap", readErr.Error(), mock.Anything).Return().Once()

r := ReconcileAPIServer{
client: failing,
scheme: scheme,
status: degraded,
tierWatchReady: ready,
migrationWatchReady: &utils.ReadyFlag{},
opts: options.ControllerOptions{
EnterpriseCRDExists: true,
DetectedProvider: operatorv1.ProviderNone,
},
}
_, err = r.Reconcile(ctx, reconcile.Request{})
Expect(err).To(MatchError(readErr))
degraded.AssertExpectations(GinkgoT())
})
})

Context("Reconcile for Condition status", func() {
generation := int64(2)
BeforeEach(func() {
Expand Down
Loading
Loading