Add scoped mode for the operator (#66762)

This work is not production ready yet. While this work makes the
operator support running with a scoped token, this is not enough for a
public release.

Several failure modes must be addressed, including scoped name
conflicts, and operators fighting for ownership of a resource.
This commit is contained in:
Hugo Shaka
2026-05-22 20:48:45 +00:00
committed by GitHub
parent 8b4ba68a8b
commit e2c22ebd54
8 changed files with 224 additions and 19 deletions
@@ -85,6 +85,18 @@ Setting this value is required when joining via the
`token` is the name of the token used by the operator to join the Teleport cluster.
## `scoped`
| Type | Default |
|------|---------|
| `bool` | `false` |
`scoped` makes the operator run in scoped mode.
In scoped mode, the operator can only reconcile scoped resource such as
`TeleportScopedTokenV1`, `TeleportScopedRoleV1`, or `TeleportScopedRoleAssignmentV1`.
Scoped mode requires the operator to join using a scoped token and the experimental scope
feature to be enabled on both Teleport Auth Service instances and Proxy Service instances.
## `teleportVersionOverride`
| Type | Default |
@@ -56,6 +56,9 @@ spec:
- '{{ .Values.joinMethod }}'
- -token
- '{{ .Values.token }}'
{{- if .Values.scoped }}
- -scoped
{{- end }}
{{- if .Values.caPins }}
- -ca-pin
- '{{ join "," .Values.caPins }}'
@@ -47,6 +47,13 @@ teleportClusterName: ""
# token(string) -- is the name of the token used by the operator to join the Teleport cluster.
token: ""
# scoped(bool) -- makes the operator run in scoped mode.
# In scoped mode, the operator can only reconcile scoped resource such as
# `TeleportScopedTokenV1`, `TeleportScopedRoleV1`, or `TeleportScopedRoleAssignmentV1`.
# Scoped mode requires the operator to join using a scoped token and the experimental scope
# feature to be enabled on both Teleport Auth Service instances and Proxy Service instances.
scoped: false
# teleportVersionOverride(string) -- controls the Teleport Kubernetes Operator
# image version deployed by the chart.
#
+2
View File
@@ -36,6 +36,7 @@ type operatorConfig struct {
syncPeriod time.Duration
namespace string
logLevel string
scoped bool
}
// BindFlags binds operatorConfig fields to CLI flags.
@@ -52,6 +53,7 @@ func (c *operatorConfig) BindFlags(fs *flag.FlagSet) {
fs.StringVar(&c.namespace, "namespace", "", "The namespace containing the Teleport CRs.")
fs.DurationVar(&c.syncPeriod, "sync-period", defaultSyncPeriod, "Operator sync period (format: https://pkg.go.dev/time#ParseDuration)")
fs.StringVar(&c.logLevel, "log-level", "INFO", "Log level (DEBUG, INFO, WARN, ERROR).")
fs.BoolVar(&c.scoped, "scoped", false, "Run operator in scoped mode. Only scoped resources will be reconciled.")
}
// CheckAndSetDefaults checks the operatorConfig and populates unspecified
@@ -39,7 +39,44 @@ type reconcilerFactory struct {
}
// SetupAllControllers sets up all controllers
func SetupAllControllers(log logr.Logger, mgr manager.Manager, teleportClient *client.Client, features *proto.Features) error {
func SetupAllControllers(log logr.Logger, mgr manager.Manager, teleportClient *client.Client, features *proto.Features, scoped bool) error {
kubeClient := mgr.GetClient()
for _, reconciler := range enabledReconcilers(log, features, scoped) {
r, err := reconciler.factory(kubeClient, teleportClient)
if err != nil {
return trace.Wrap(err, "failed to create controller for %s", reconciler.cr)
}
err = r.SetupWithManager(mgr)
if err != nil {
return trace.Wrap(err, "failed to setup controller for: %s", reconciler.cr)
}
}
return nil
}
func enabledReconcilers(log logr.Logger, features *proto.Features, scoped bool) []reconcilerFactory {
var reconcilers []reconcilerFactory
// We always run
reconcilers = append(reconcilers, scopedReconcilers(log, features)...)
if scoped {
log.Info("Running in scoped mode. Unscoped resources will not be reconciled.")
} else {
reconcilers = append(reconcilers, unscopedReconcilers(log, features)...)
}
return reconcilers
}
func scopedReconcilers(log logr.Logger, features *proto.Features) []reconcilerFactory {
return []reconcilerFactory{
{"TeleportScopedTokenV1", NewScopedTokenV1Reconciler},
{"TeleportScopedRoleV1", NewScopedRoleV1Reconciler},
{"TeleportScopedRoleAssignmentV1", NewScopedRoleAssignmentV1Reconciler},
}
}
func unscopedReconcilers(log logr.Logger, features *proto.Features) []reconcilerFactory {
reconcilers := []reconcilerFactory{
{"TeleportRole", NewRoleReconciler},
{"TeleportRoleV6", NewRoleV6Reconciler},
@@ -65,9 +102,6 @@ func SetupAllControllers(log logr.Logger, mgr manager.Manager, teleportClient *c
// saml_idp_service_provider objects using tctl for any build. We
// therefore enable it here unconditionally to mirror tctl behavior.
{"TeleportSAMLIdPServiceProviderV1", NewSAMLIdPServiceProviderV1Reconciler},
{"TeleportScopedTokenV1", NewScopedTokenV1Reconciler},
{"TeleportScopedRoleV1", NewScopedRoleV1Reconciler},
{"TeleportScopedRoleAssignmentV1", NewScopedRoleAssignmentV1Reconciler},
}
oidc := modules.GetProtoEntitlement(features, entitlements.OIDC)
@@ -110,17 +144,5 @@ func SetupAllControllers(log logr.Logger, mgr manager.Manager, teleportClient *c
log.Info("The cluster license does not contain advanced workflows. TeleportAccessList, TeleportOktaImportRule resources won't be reconciled")
}
kubeClient := mgr.GetClient()
for _, reconciler := range reconcilers {
r, err := reconciler.factory(kubeClient, teleportClient)
if err != nil {
return trace.Wrap(err, "failed to create controller for %s", reconciler.cr)
}
err = r.SetupWithManager(mgr)
if err != nil {
return trace.Wrap(err, "failed to setup controller for: %s", reconciler.cr)
}
}
return nil
return reconcilers
}
@@ -0,0 +1,156 @@
/*
Copyright 2026 Gravitational, Inc.
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 resources
import (
"testing"
"github.com/go-logr/logr"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/entitlements"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/modules/modulestest"
"github.com/gravitational/teleport/lib/utils/log/logtest"
)
func TestEnabledReconcilers(t *testing.T) {
// Test setup: create the fixtures
ossFeatures := modulestest.OSSModules().Features()
entFeatures := modulestest.EnterpriseModules().Features()
entFeatures.Entitlements[entitlements.OIDC] = modules.EntitlementInfo{Enabled: true}
entFeatures.Entitlements[entitlements.SAML] = modules.EntitlementInfo{Enabled: true}
entFeatures.Entitlements[entitlements.Policy] = modules.EntitlementInfo{Enabled: true}
entFeatures.AdvancedAccessWorkflows = true
tests := []struct {
name string
scoped bool
features modules.Features
expectedReconcilers map[string]struct{}
}{
{
name: "Unscoped, OSS",
scoped: false,
features: ossFeatures,
expectedReconcilers: map[string]struct{}{
// scoped
"TeleportScopedTokenV1": {},
"TeleportScopedRoleV1": {},
"TeleportScopedRoleAssignmentV1": {},
// unscoped oss
"TeleportRole": {},
"TeleportRoleV6": {},
"TeleportRoleV7": {},
"TeleportRoleV8": {},
"TeleportUser": {},
"TeleportGithubConnector": {},
"TeleportLockV2": {},
"TeleportProvisionToken": {},
"TeleportOpenSSHServerV2": {},
"TeleportOpenSSHEICEServerV2": {},
"TeleportTrustedClusterV2": {},
"TeleportBotV1": {},
"TeleportWorkloadIdentityV1": {},
"TeleportAutoupdateConfigV1": {},
"TeleportAutoupdateVersionV1": {},
"TeleportAppV3": {},
"TeleportDatabaseV3": {},
"TeleportAccessMonitoringRuleV1": {},
"TeleportSAMLIdPServiceProviderV1": {},
},
},
{
name: "Scoped, OSS",
scoped: true,
features: ossFeatures,
expectedReconcilers: map[string]struct{}{
"TeleportScopedTokenV1": {},
"TeleportScopedRoleV1": {},
"TeleportScopedRoleAssignmentV1": {},
},
},
{
name: "Unscoped, enterprise",
scoped: false,
features: entFeatures,
expectedReconcilers: map[string]struct{}{
// scoped
"TeleportScopedTokenV1": {},
"TeleportScopedRoleV1": {},
"TeleportScopedRoleAssignmentV1": {},
// unscoped oss
"TeleportRole": {},
"TeleportRoleV6": {},
"TeleportRoleV7": {},
"TeleportRoleV8": {},
"TeleportUser": {},
"TeleportGithubConnector": {},
"TeleportLockV2": {},
"TeleportProvisionToken": {},
"TeleportOpenSSHServerV2": {},
"TeleportOpenSSHEICEServerV2": {},
"TeleportTrustedClusterV2": {},
"TeleportBotV1": {},
"TeleportWorkloadIdentityV1": {},
"TeleportAutoupdateConfigV1": {},
"TeleportAutoupdateVersionV1": {},
"TeleportAppV3": {},
"TeleportDatabaseV3": {},
"TeleportAccessMonitoringRuleV1": {},
"TeleportSAMLIdPServiceProviderV1": {},
// unscoped enterprise
"TeleportOIDCConnector": {},
"TeleportSAMLConnector": {},
"TeleportInferenceModel": {},
"TeleportInferencePolicy": {},
"TeleportInferenceSecret": {},
"TeleportRetrievalModelV1": {},
"TeleportLoginRule": {},
"TeleportAccessList": {},
"TeleportOktaImportRule": {},
},
},
{
name: "Scoped, enterprise",
scoped: true,
features: entFeatures,
expectedReconcilers: map[string]struct{}{
"TeleportScopedTokenV1": {},
"TeleportScopedRoleV1": {},
"TeleportScopedRoleAssignmentV1": {},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
log := logr.FromSlogHandler(logtest.NewLogger().Handler())
features := tt.features.ToProto()
reconcilers := enabledReconcilers(log, features, tt.scoped)
// Test validation: check that the expected reconcilers were enabled
reconcilersSet := make(map[string]struct{}, len(reconcilers))
for _, reconciler := range reconcilers {
reconcilersSet[reconciler.cr] = struct{}{}
}
require.Len(t, reconcilersSet, len(tt.expectedReconcilers))
require.Equal(t, tt.expectedReconcilers, reconcilersSet)
})
}
}
@@ -230,7 +230,9 @@ func (s *TestSetup) StartKubernetesOperator(t *testing.T) {
pong, err := s.TeleportClient.Ping(context.Background())
require.NoError(t, err)
err = resources.SetupAllControllers(setupLog, k8sManager, s.TeleportClient, pong.ServerFeatures)
const scoped = false
err = resources.SetupAllControllers(setupLog, k8sManager, s.TeleportClient, pong.ServerFeatures, scoped)
require.NoError(t, err)
ctx, ctxCancel := context.WithCancel(context.Background())
+2 -1
View File
@@ -86,6 +86,7 @@ func main() {
os.Exit(1)
}
botConfig.Scoped = config.scoped
bot, err := embeddedtbot.New(botConfig, slogLogger.With(teleport.ComponentLabel, "embedded-tbot"))
if err != nil {
setupLog.Error(err, "unable to build tbot")
@@ -132,7 +133,7 @@ func main() {
os.Exit(1)
}
if err = resources.SetupAllControllers(setupLog, mgr, client, pong.ServerFeatures); err != nil {
if err = resources.SetupAllControllers(setupLog, mgr, client, pong.ServerFeatures, config.scoped); err != nil {
setupLog.Error(err, "failed to setup controllers")
os.Exit(1)
}