From 65001c1acf9f76943eca90e1f5e8fbe49ca21594 Mon Sep 17 00:00:00 2001 From: michaelawyu Date: Thu, 14 May 2026 01:57:51 +1000 Subject: [PATCH 1/4] Added VAP manager Signed-off-by: michaelawyu --- pkg/admissionpolicymanager/configs.go | 41 +++ pkg/admissionpolicymanager/manager.go | 328 ++++++++++++++++++ .../manager_integration_test.go | 267 ++++++++++++++ .../podsnreplicasets.go | 145 ++++++++ pkg/admissionpolicymanager/suite_test.go | 119 +++++++ .../svcaccountsntokenreqs.go | 200 +++++++++++ pkg/utils/common.go | 18 +- 7 files changed, 1109 insertions(+), 9 deletions(-) create mode 100644 pkg/admissionpolicymanager/configs.go create mode 100644 pkg/admissionpolicymanager/manager.go create mode 100644 pkg/admissionpolicymanager/manager_integration_test.go create mode 100644 pkg/admissionpolicymanager/podsnreplicasets.go create mode 100644 pkg/admissionpolicymanager/suite_test.go create mode 100644 pkg/admissionpolicymanager/svcaccountsntokenreqs.go diff --git a/pkg/admissionpolicymanager/configs.go b/pkg/admissionpolicymanager/configs.go new file mode 100644 index 000000000..b65c4010d --- /dev/null +++ b/pkg/admissionpolicymanager/configs.go @@ -0,0 +1,41 @@ +/* +Copyright 2026 The KubeFleet Authors. + +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 admissionpolicymanager + +import ( + "github.com/kubefleet-dev/kubefleet/pkg/utils" +) + +// PolicyGeneratorConfigs holds the configurations for all available admission policy +// generators. +// +// This type is exposed so that users can provide a configuration object (in its serialized form) +// that specifies individual configurations for each generator. +type PolicyGeneratorConfigs struct { + PodsAndReplicaSetsVAPGeneratorConfig *PodsAndReplicaSetsValidatingAdmissionPolicyGenerator `json:"denyPodsAndReplicaSetsOutsideReservedNamespaces,omitempty"` + SvcAccountsAndTokenRequestsVAPGeneratorConfig *ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator `json:"denyServiceAccountsAndTokenRequestsInReservedNamespaces,omitempty"` +} + +// DefaultPolicyGeneratorConfigs is the default configuration for all available admission policy generators. +var DefaultPolicyGeneratorConfigs = &PolicyGeneratorConfigs{ + PodsAndReplicaSetsVAPGeneratorConfig: &PodsAndReplicaSetsValidatingAdmissionPolicyGenerator{ + ReservedNamespacePrefixes: []string{utils.FleetPrefix, utils.KubePrefix}, + }, + SvcAccountsAndTokenRequestsVAPGeneratorConfig: &ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator{ + ReservedNamespacePrefixes: []string{utils.FleetPrefix, utils.KubePrefix}, + }, +} diff --git a/pkg/admissionpolicymanager/manager.go b/pkg/admissionpolicymanager/manager.go new file mode 100644 index 000000000..f3bdd72e8 --- /dev/null +++ b/pkg/admissionpolicymanager/manager.go @@ -0,0 +1,328 @@ +/* +Copyright 2026 The KubeFleet Authors. + +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 admissionpolicymanager + +import ( + "context" + "reflect" + "time" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/util/retry" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/kubefleet-dev/kubefleet/pkg/utils/errors" +) + +const ( + // The following labels are added to all policies created by the policy manager, + // so that the agent can track the lifecycle of created policies across different runs + // and act accordingly. + VAPManagedByKubeFleetLabelKey = "app.kubernetes.io/managed-by" + VAPManagedByKubeFleetLabelValue = "fleet" + VAPPartOfKubeFleetLabelKey = "app.kubernetes.io/part-of" + VAPPartOfKubeFleetLabelValue = "fleet" +) + +var ( + // A list of all available policy generators. + AllGenerators = sets.Set[string]{ + PodsAndReplicaSetsVAPGeneratorName: {}, + SvcAccountsAndTokenRequestsVAPGeneratorName: {}, + } +) + +var ( + policyRWOpBackoff = wait.Backoff{ + Steps: 3, + Duration: 1 * time.Second, + Factor: 2.0, + Jitter: 0.1, + } +) + +type ValidatingAdmissionPolicyGenerator interface { + Name() string + Validate() error + Policies() []*admissionregistrationv1.ValidatingAdmissionPolicy + PolicyBindings() []*admissionregistrationv1.ValidatingAdmissionPolicyBinding +} + +type PolicyManager struct { + Client client.Client + + enabledPolicyGenerators map[string]ValidatingAdmissionPolicyGenerator +} + +func New(client client.Client, policyGeneratorConfigs *PolicyGeneratorConfigs, enabledPolicyNames []string) (*PolicyManager, error) { + // Prepare a set of generators based on the list of enabled policies. + enabledPolicyGenerators, err := preparePolicyGenerators(policyGeneratorConfigs, enabledPolicyNames) + if err != nil { + return nil, errors.Wraps(err, "failed to create policy manager") + } + + return &PolicyManager{ + Client: client, + enabledPolicyGenerators: enabledPolicyGenerators, + }, nil +} + +func preparePolicyGenerators( + policyGeneratorConfigs *PolicyGeneratorConfigs, + enabledPolicyNames []string, +) (map[string]ValidatingAdmissionPolicyGenerator, error) { + enabledPolicyNameSet := sets.New(enabledPolicyNames...) + enabledPolicyGenerators := make(map[string]ValidatingAdmissionPolicyGenerator) + + v := reflect.ValueOf(policyGeneratorConfigs).Elem() + for i := range v.NumField() { + field := v.Field(i) + if field.IsNil() { + continue + } + gen, ok := field.Interface().(ValidatingAdmissionPolicyGenerator) + if !ok { + continue + } + if enabledPolicyNameSet.Has(gen.Name()) { + enabledPolicyGenerators[gen.Name()] = gen + } + } + + if len(enabledPolicyNameSet) != len(enabledPolicyGenerators) { + configuredPolicyNames := make([]string, 0, len(enabledPolicyGenerators)) + for name := range enabledPolicyGenerators { + configuredPolicyNames = append(configuredPolicyNames, name) + } + return nil, errors.NewUserError(nil, "some enabled policy generators are not configured properly", "enabledPolicies", enabledPolicyNames, "configuredPolicies", configuredPolicyNames) + } + return enabledPolicyGenerators, nil +} + +func (m *PolicyManager) Start(ctx context.Context) error { + // Generate all policies and policy bindings from the enabled generators, and apply them to the cluster. + createdOrUpdatedPolicyNames, createdOrUpdatedPolicyBindingNames, err := m.createOrUpdatePoliciesAndBindingsForEnabledGenerators(ctx) + if err != nil { + return errors.Wraps(err, "failed to create or update validating admission policies and bindings for enabled generators") + } + + // List all existing policies and policy bindings created by the manager, and delete those that are no longer needed. + if err := m.garbageCollectUnusedPoliciesAndBindings(ctx, createdOrUpdatedPolicyNames, createdOrUpdatedPolicyBindingNames); err != nil { + return errors.Wraps(err, "failed to garbage collect unused validating admission policies and bindings") + } + return nil +} + +// createOrUpdatePoliciesAndBindingsForEnabledGenerators creates or updates validating admission +// policies and their bindings for all enabled generators, and returns the names of +// created or updated policies and policy bindings. +func (m *PolicyManager) createOrUpdatePoliciesAndBindingsForEnabledGenerators(ctx context.Context) (sets.Set[string], sets.Set[string], error) { + createdOrUpdatedPolicyNames := sets.New[string]() + createdOrUpdatedPolicyBindingNames := sets.New[string]() + + for _, gen := range m.enabledPolicyGenerators { + // As a sanity check, do one more round of validation. + // + // Normally this check would never fail as the generators have been validated before + // the manager initializes. + if err := gen.Validate(); err != nil { + return nil, nil, errors.Wraps(err, "policy generator is invalid", "generator", gen.Name()) + } + + policies := gen.Policies() + policyBindings := gen.PolicyBindings() + + for _, policy := range policies { + // Add the managed by and part of labels to the policy, so that the agent can track the + // lifecycle of created policies across different runs and act accordingly. + if policy.Labels == nil { + policy.Labels = make(map[string]string) + } + policy.Labels[VAPManagedByKubeFleetLabelKey] = VAPManagedByKubeFleetLabelValue + policy.Labels[VAPPartOfKubeFleetLabelKey] = VAPPartOfKubeFleetLabelValue + + policyToCreateOrUpdate := &admissionregistrationv1.ValidatingAdmissionPolicy{ + ObjectMeta: policy.ObjectMeta, + } + err := retry.OnError(policyRWOpBackoff, func(err error) bool { + // Retry on any error. Note that nil errors are not passed to this function, + // and AlreadyExists errors will not occur. + return true + }, func() error { + opRes, err := controllerutil.CreateOrUpdate(ctx, m.Client, policyToCreateOrUpdate, func() error { + policyCopy := policy.DeepCopy() + policyToCreateOrUpdate.Spec = policyCopy.Spec + policyToCreateOrUpdate.Labels = policyCopy.Labels + return nil + }) + if err != nil { + return errors.NewAPIServerError(err, + "failed to create/update validating admission policy", + false, + "op", opRes, "policyName", policy.Name, "policyGenerator", gen.Name()) + } + return nil + }) + if err != nil { + // No need to wrap this for another time. The inner error already contains sufficient context about the failure. + return nil, nil, err + } + + createdOrUpdatedPolicyNames.Insert(policy.Name) + + klog.V(2).InfoS("Successfully created or updated validating admission policy", "policyName", policy.Name, "policyGenerator", gen.Name()) + } + + for _, policyBinding := range policyBindings { + // Add the managed by and part of labels to the policy binding, so that the agent can track the + // lifecycle of created policy bindings across different runs and act accordingly. + if policyBinding.Labels == nil { + policyBinding.Labels = make(map[string]string) + } + policyBinding.Labels[VAPManagedByKubeFleetLabelKey] = VAPManagedByKubeFleetLabelValue + policyBinding.Labels[VAPPartOfKubeFleetLabelKey] = VAPPartOfKubeFleetLabelValue + + policyBindingToCreateOrUpdate := &admissionregistrationv1.ValidatingAdmissionPolicyBinding{ + ObjectMeta: policyBinding.ObjectMeta, + } + err := retry.OnError(policyRWOpBackoff, func(err error) bool { + // Retry on any error. Note that nil errors are not passed to this function, + // and AlreadyExists errors will not occur. + return true + }, func() error { + opRes, err := controllerutil.CreateOrUpdate(ctx, m.Client, policyBindingToCreateOrUpdate, func() error { + policyBindingCopy := policyBinding.DeepCopy() + policyBindingToCreateOrUpdate.Spec = policyBindingCopy.Spec + policyBindingToCreateOrUpdate.Labels = policyBindingCopy.Labels + return nil + }) + if err != nil { + return errors.NewAPIServerError(err, + "failed to create/update validating admission policy binding", + false, + "op", opRes, "policyBindingName", policyBinding.Name, "policyGenerator", gen.Name()) + } + return nil + }) + if err != nil { + // No need to wrap this for another time. The inner error already contains sufficient context about the failure. + return nil, nil, err + } + + createdOrUpdatedPolicyBindingNames.Insert(policyBinding.Name) + + klog.V(2).InfoS("Successfully created or updated validating admission policy binding", "policyBindingName", policyBinding.Name, "policyGenerator", gen.Name()) + } + } + + return createdOrUpdatedPolicyNames, createdOrUpdatedPolicyBindingNames, nil +} + +func (m *PolicyManager) garbageCollectUnusedPoliciesAndBindings(ctx context.Context, createdOrUpdatedPolicyNames sets.Set[string], createdOrUpdatedPolicyBindingNames sets.Set[string]) error { + // List all existing policies and policy bindings created by the manager. + existingPolicyList := &admissionregistrationv1.ValidatingAdmissionPolicyList{} + existingPolicyBindingList := &admissionregistrationv1.ValidatingAdmissionPolicyBindingList{} + managedByAndPartOfKubeFleetLabelSelector := client.MatchingLabels{ + VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, + VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, + } + err := retry.OnError(policyRWOpBackoff, func(err error) bool { + // Retry on any error. Note that nil errors are not passed to this function. + return true + }, func() error { + if err := m.Client.List(ctx, existingPolicyList, managedByAndPartOfKubeFleetLabelSelector); err != nil { + return errors.NewAPIServerError(err, "failed to list all validating admission policies managed by KubeFleet", false) + } + return nil + }) + if err != nil { + // No need to wrap this for another time. The inner error already contains sufficient context about the failure. + return err + } + + err = retry.OnError(policyRWOpBackoff, func(err error) bool { + // Retry on any error. Note that nil errors are not passed to this function. + return true + }, func() error { + if err := m.Client.List(ctx, existingPolicyBindingList, managedByAndPartOfKubeFleetLabelSelector); err != nil { + return errors.NewAPIServerError(err, "failed to list all validating admission policy bindings managed by KubeFleet", false) + } + return nil + }) + if err != nil { + // No need to wrap this for another time. The inner error already contains sufficient context about the failure. + return err + } + + // Delete policies that are created by the manager but no longer needed. + for i := range existingPolicyList.Items { + policy := &existingPolicyList.Items[i] + if !createdOrUpdatedPolicyNames.Has(policy.Name) { + err := retry.OnError(policyRWOpBackoff, func(err error) bool { + // Retry on any error. Note that nil errors are not passed to this function, and NotFound errors will not occur. + return true + }, func() error { + if err := m.Client.Delete(ctx, policy); err != nil && !apierrors.IsNotFound(err) { + return errors.NewAPIServerError(err, + "failed to delete validating admission policy", + false, + "policyName", policy.Name) + } + return nil + }) + if err != nil { + // No need to wrap this for another time. The inner error already contains sufficient context about the failure. + return err + } + + klog.V(2).InfoS("Successfully deleted validating admission policy", "policyName", policy.Name) + } + } + + // Delete policy bindings that are created by the manager but no longer needed. + for i := range existingPolicyBindingList.Items { + policyBinding := &existingPolicyBindingList.Items[i] + if !createdOrUpdatedPolicyBindingNames.Has(policyBinding.Name) { + err := retry.OnError(policyRWOpBackoff, func(err error) bool { + // Retry on any error. Note that nil errors are not passed to this function, and NotFound errors will not occur. + return true + }, func() error { + if err := m.Client.Delete(ctx, policyBinding); err != nil && !apierrors.IsNotFound(err) { + return errors.NewAPIServerError(err, + "failed to delete validating admission policy binding", + false, + "policyBindingName", policyBinding.Name) + } + return nil + }) + + if err != nil { + // No need to wrap this for another time. The inner error already contains sufficient context about the failure. + return err + } + + klog.V(2).InfoS("Successfully deleted validating admission policy binding", "policyBindingName", policyBinding.Name) + } + } + + return nil +} diff --git a/pkg/admissionpolicymanager/manager_integration_test.go b/pkg/admissionpolicymanager/manager_integration_test.go new file mode 100644 index 000000000..43e4a1c49 --- /dev/null +++ b/pkg/admissionpolicymanager/manager_integration_test.go @@ -0,0 +1,267 @@ +/* +Copyright 2026 The KubeFleet Authors. + +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 admissionpolicymanager + +import ( + "context" + "fmt" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/kubefleet-dev/kubefleet/pkg/utils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + nsNameTmpl = "work-%s" +) + +var ( + ignoreSystemManagedObjectMetaFields = cmpopts.IgnoreFields(metav1.ObjectMeta{}, "UID", "ResourceVersion", "Generation", "CreationTimestamp", "ManagedFields") + + lessFuncValidatingAdmissionPolicy = func(i, j admissionregistrationv1.ValidatingAdmissionPolicy) bool { + return i.Name < j.Name + } +) + +var _ = Describe("Policies, Policy Bindings and their Effects", Ordered, func() { + nsName := fmt.Sprintf(nsNameTmpl, utils.RandStr()) + + var ns *corev1.Namespace + BeforeAll(func() { + ns = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsName, + }, + } + // Note: due to test environment restrictions (restrictions from the envtest package); + // namespaces created cannot be deleted. As a result, this test node will not perform + // any cleanup. + Expect(hubUncachedClient.Create(ctx, ns)).To(Succeed()) + }) + + It("should have all the expected policies", func() { + wantPolicies := []admissionregistrationv1.ValidatingAdmissionPolicy{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: podsAndReplicaSetsVAPPolicyName, + Labels: map[string]string{ + VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, + VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, + }, + }, + Spec: admissionregistrationv1.ValidatingAdmissionPolicySpec{ + FailurePolicy: ptr.To(admissionregistrationv1.Fail), + MatchConstraints: &admissionregistrationv1.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{}, + ObjectSelector: &metav1.LabelSelector{}, + ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"pods"}, + Scope: ptr.To(admissionregistrationv1.ScopeType("*")), // The system-enforced default. + }, + }, + }, + { + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"apps"}, + APIVersions: []string{"v1"}, + Resources: []string{"replicasets"}, + Scope: ptr.To(admissionregistrationv1.ScopeType("*")), // The system-enforced default. + }, + }, + }, + }, + MatchPolicy: ptr.To(admissionregistrationv1.Equivalent), // The system-enforced default. + }, + Validations: []admissionregistrationv1.Validation{ + { + Expression: `object.metadata.namespace.startsWith("fleet-") || object.metadata.namespace.startsWith("kube-")`, + Message: "creating pods and replicas is disallowed in the fleet hub cluster", + Reason: ptr.To(metav1.StatusReasonForbidden), + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: svcAccountsAndTokenRequestsVAPPolicyName, + Labels: map[string]string{ + VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, + VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, + }, + }, + Spec: admissionregistrationv1.ValidatingAdmissionPolicySpec{ + FailurePolicy: ptr.To(admissionregistrationv1.Fail), + MatchConstraints: &admissionregistrationv1.MatchResources{ + NamespaceSelector: &metav1.LabelSelector{}, + ObjectSelector: &metav1.LabelSelector{}, + ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + admissionregistrationv1.Update, + admissionregistrationv1.Delete, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"serviceaccounts"}, + Scope: ptr.To(admissionregistrationv1.ScopeType("*")), // The system-enforced default. + }, + }, + }, + { + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"serviceaccounts/token"}, + Scope: ptr.To(admissionregistrationv1.ScopeType("*")), // The system-enforced default. + }, + }, + }, + }, + MatchPolicy: ptr.To(admissionregistrationv1.Equivalent), // The system-enforced default. + }, + Validations: []admissionregistrationv1.Validation{ + { + Expression: `!(object.metadata.namespace.startsWith("fleet-") || object.metadata.namespace.startsWith("kube-")) || (request.userInfo.username == "system:kube-scheduler" || request.userInfo.username == "system:kube-controller-manager" || "system:nodes" in request.userInfo.groups || "system:masters" in request.userInfo.groups)`, + Message: "writing service accounts in reserved namespaces or requesting tokens from such service accounts is disallowed", + Reason: ptr.To(metav1.StatusReasonForbidden), + }, + }, + }, + }, + } + + Eventually(func() error { + policyList := &admissionregistrationv1.ValidatingAdmissionPolicyList{} + matchingLabels := map[string]string{ + VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, + VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, + } + if err := hubUncachedClient.List(context.TODO(), policyList, client.MatchingLabels(matchingLabels)); err != nil { + return fmt.Errorf("failed to list matching policies: %w", err) + } + + policies := policyList.Items + if len(policies) != len(wantPolicies) { + return fmt.Errorf("number of policies mismatch: got %d, want %d", len(policies), len(wantPolicies)) + } + + if diff := cmp.Diff( + policies, wantPolicies, + cmpopts.EquateEmpty(), + cmpopts.SortSlices(lessFuncValidatingAdmissionPolicy), + ignoreSystemManagedObjectMetaFields, + ); diff != "" { + return fmt.Errorf("policies mismatch (-got +want):\n%s", diff) + } + return nil + }, eventuallyDuration, eventuallyInterval).Should(Succeed(), "Received unexpected list of policies or an error has occurred") + }) + + It("should have all the expected bindings", func() { + wantPolicyBindings := []admissionregistrationv1.ValidatingAdmissionPolicyBinding{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: podsAndReplicaSetsVAPPolicyBindingName, + Labels: map[string]string{ + VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, + VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, + }, + }, + Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: podsAndReplicaSetsVAPPolicyName, + ValidationActions: []admissionregistrationv1.ValidationAction{ + admissionregistrationv1.Deny, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: svcAccountsAndTokenRequestsVAPPolicyBindingName, + Labels: map[string]string{ + VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, + VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, + }, + }, + Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: svcAccountsAndTokenRequestsVAPPolicyName, + ValidationActions: []admissionregistrationv1.ValidationAction{ + admissionregistrationv1.Deny, + }, + }, + }, + } + + lessFuncValidatingAdmissionPolicyBinding := func(i, j admissionregistrationv1.ValidatingAdmissionPolicyBinding) bool { + return i.Name < j.Name + } + + Eventually(func() error { + bindingList := &admissionregistrationv1.ValidatingAdmissionPolicyBindingList{} + matchingLabels := map[string]string{ + VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, + VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, + } + if err := hubUncachedClient.List(context.TODO(), bindingList, client.MatchingLabels(matchingLabels)); err != nil { + return fmt.Errorf("failed to list matching policy bindings: %w", err) + } + + bindings := bindingList.Items + if len(bindings) != len(wantPolicyBindings) { + return fmt.Errorf("number of policy bindings mismatch: got %d, want %d", len(bindings), len(wantPolicyBindings)) + } + + if diff := cmp.Diff( + bindings, wantPolicyBindings, + cmpopts.EquateEmpty(), + cmpopts.SortSlices(lessFuncValidatingAdmissionPolicyBinding), + ignoreSystemManagedObjectMetaFields, + ); diff != "" { + return fmt.Errorf("policy bindings mismatch (-got +want):\n%s", diff) + } + return nil + }, eventuallyDuration, eventuallyInterval).Should(Succeed(), "Received unexpected list of policy bindings or an error has occurred") + }) + + // Note: in the integration test environment, it appears that validating admission policies have no effect or are + // not being enforced as expected. As a result the effects are validated using E2E tests instead. +}) diff --git a/pkg/admissionpolicymanager/podsnreplicasets.go b/pkg/admissionpolicymanager/podsnreplicasets.go new file mode 100644 index 000000000..a6e42f15b --- /dev/null +++ b/pkg/admissionpolicymanager/podsnreplicasets.go @@ -0,0 +1,145 @@ +/* +Copyright 2026 The KubeFleet Authors. + +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 admissionpolicymanager + +import ( + "fmt" + "regexp" + "strings" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + "github.com/kubefleet-dev/kubefleet/pkg/utils/errors" +) + +const ( + PodsAndReplicaSetsVAPGeneratorName = "DenyPodsAndReplicaSetsOutsideReservedNamespaces" +) + +const ( + podsAndReplicaSetsVAPPolicyName = "deny-pods-and-replicasets-outside-reserved-namespaces" + podsAndReplicaSetsVAPPolicyBindingName = "deny-pods-and-replicasets-outside-reserved-namespaces-binding" +) + +// reservedNamespacePrefixRegExp matches valid namespace prefix characters (DNS label subset). +var reservedNamespacePrefixRegExp = regexp.MustCompile(`^[a-z0-9-]+$`) + +// Verify that PodsAndReplicaSetsValidatingAdmissionPolicyGenerator implements +// the ValidatingAdmissionPolicyGenerator interface. +var _ ValidatingAdmissionPolicyGenerator = &PodsAndReplicaSetsValidatingAdmissionPolicyGenerator{} + +// PodsAndReplicaSetsValidatingAdmissionPolicyGenerator generates a ValidatingAdmissionPolicy +// and its binding that denies creation of pods and replicasets in non-reserved namespaces. +type PodsAndReplicaSetsValidatingAdmissionPolicyGenerator struct { + ReservedNamespacePrefixes []string +} + +// Name returns the name of the generator, which is used to determine if a specific generator +// has been enabled or not. +func (g *PodsAndReplicaSetsValidatingAdmissionPolicyGenerator) Name() string { + return PodsAndReplicaSetsVAPGeneratorName +} + +// Validate validates the configuration of the generator. +func (g *PodsAndReplicaSetsValidatingAdmissionPolicyGenerator) Validate() error { + if len(g.ReservedNamespacePrefixes) == 0 { + return errors.NewUserError(nil, "at least one prefix must be specified") + } + // Check if any of the prefixes includes illegal characters. + for _, prefix := range g.ReservedNamespacePrefixes { + if !reservedNamespacePrefixRegExp.MatchString(prefix) { + return errors.NewUserError(nil, "prefix contains illegal characters; only lowercase alphanumeric characters and hyphens are allowed", "prefix", prefix) + } + } + return nil +} + +// Policies generates a ValidatingAdmissionPolicy that denies creation of pods and +// replicasets in non-reserved namespaces. +func (g *PodsAndReplicaSetsValidatingAdmissionPolicyGenerator) Policies() []*admissionregistrationv1.ValidatingAdmissionPolicy { + celExprSegs := []string{} + for _, prefix := range g.ReservedNamespacePrefixes { + celExprSegs = append(celExprSegs, fmt.Sprintf(`object.metadata.namespace.startsWith("%s")`, prefix)) + } + celExpr := strings.Join(celExprSegs, " || ") + + policy := &admissionregistrationv1.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: podsAndReplicaSetsVAPPolicyName, + }, + Spec: admissionregistrationv1.ValidatingAdmissionPolicySpec{ + FailurePolicy: ptr.To(admissionregistrationv1.Fail), + MatchConstraints: &admissionregistrationv1.MatchResources{ + ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"pods"}, + }, + }, + }, + { + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"apps"}, + APIVersions: []string{"v1"}, + Resources: []string{"replicasets"}, + }, + }, + }, + }, + }, + Validations: []admissionregistrationv1.Validation{ + { + Expression: celExpr, + // The error message has been set to match with the checks in some of the E2E tests + // in our existing release pipeline. + Message: "creating pods and replicas is disallowed in the fleet hub cluster", + Reason: ptr.To(metav1.StatusReasonForbidden), + }, + }, + }, + } + return []*admissionregistrationv1.ValidatingAdmissionPolicy{policy} +} + +// PolicyBindings generates a ValidatingAdmissionPolicyBinding for the ValidatingAdmissionPolicy +// generated by the Policies() method. +func (g *PodsAndReplicaSetsValidatingAdmissionPolicyGenerator) PolicyBindings() []*admissionregistrationv1.ValidatingAdmissionPolicyBinding { + binding := &admissionregistrationv1.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: podsAndReplicaSetsVAPPolicyBindingName, + }, + Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: podsAndReplicaSetsVAPPolicyName, + ValidationActions: []admissionregistrationv1.ValidationAction{ + admissionregistrationv1.Deny, + }, + }, + } + return []*admissionregistrationv1.ValidatingAdmissionPolicyBinding{binding} +} diff --git a/pkg/admissionpolicymanager/suite_test.go b/pkg/admissionpolicymanager/suite_test.go new file mode 100644 index 000000000..9e258f7da --- /dev/null +++ b/pkg/admissionpolicymanager/suite_test.go @@ -0,0 +1,119 @@ +/* +Copyright 2026 The KubeFleet Authors. + +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 admissionpolicymanager + +import ( + "context" + "flag" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/metrics/server" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. +var ( + cfg *rest.Config + testEnv *envtest.Environment + hubUncachedClient client.Client + hubMgr ctrl.Manager + + ctx context.Context + cancel context.CancelFunc +) + +var ( + eventuallyDuration = time.Second * 10 + eventuallyInterval = time.Millisecond * 500 +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Admission Policy Manager Integration Test Suite") +} + +var _ = BeforeSuite(func() { + ctx, cancel = context.WithCancel(context.TODO()) + + By("Setup klog") + fs := flag.NewFlagSet("klog", flag.ContinueOnError) + klog.InitFlags(fs) + Expect(fs.Parse([]string{"--v", "5", "-add_dir_header", "true"})).Should(Succeed()) + + logger := zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)) + klog.SetLogger(logger) + ctrl.SetLogger(logger) + + By("Bootstrapping test environment") + testEnv = &envtest.Environment{} + + var err error + cfg, err = testEnv.Start() + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + By("Setting up the controller manager") + hubMgr, err = ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + Metrics: server.Options{ + BindAddress: "0", // disable the metrics server. + }, + Logger: logger, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(hubMgr).ToNot(BeNil()) + + By("Building the K8s client") + hubUncachedClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).ToNot(HaveOccurred()) + Expect(hubUncachedClient).ToNot(BeNil()) + + By("Setting up the policy manager") + enabledGeneratorNames := []string{} + for name := range AllGenerators { + enabledGeneratorNames = append(enabledGeneratorNames, name) + } + policyManager, err := New(hubUncachedClient, DefaultPolicyGeneratorConfigs, enabledGeneratorNames) + Expect(err).ToNot(HaveOccurred()) + Expect(policyManager).ToNot(BeNil()) + Expect(policyManager.Start(ctx)).To(Succeed()) + + By("Starting the controller manager") + go func() { + defer GinkgoRecover() + Expect(hubMgr.Start(ctx)).To(Succeed()) + }() +}) + +var _ = AfterSuite(func() { + defer klog.Flush() + + cancel() + By("Tearing down the test environment") + Expect(testEnv.Stop()).To(Succeed()) +}) diff --git a/pkg/admissionpolicymanager/svcaccountsntokenreqs.go b/pkg/admissionpolicymanager/svcaccountsntokenreqs.go new file mode 100644 index 000000000..6009c9f34 --- /dev/null +++ b/pkg/admissionpolicymanager/svcaccountsntokenreqs.go @@ -0,0 +1,200 @@ +/* +Copyright 2026 The KubeFleet Authors. + +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 admissionpolicymanager + +import ( + "fmt" + "strings" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + "github.com/kubefleet-dev/kubefleet/pkg/utils/errors" +) + +const ( + SvcAccountsAndTokenRequestsVAPGeneratorName = "DenyServiceAccountsAndTokenRequestsInReservedNamespaces" +) + +const ( + svcAccountsAndTokenRequestsVAPPolicyName = "deny-serviceaccounts-and-tokenrequests-in-reserved-namespaces" + svcAccountsAndTokenRequestsVAPPolicyBindingName = "deny-serviceaccounts-and-tokenrequests-in-reserved-namespaces-binding" +) + +const ( + kubeSchedulerUserName = "system:kube-scheduler" + kubeControllerManagerUserName = "system:kube-controller-manager" + + kubeNodeUserGroup = "system:nodes" + adminUserGroup = "system:masters" +) + +// Verify that ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator implements +// the ValidatingAdmissionPolicyGenerator interface. +var _ ValidatingAdmissionPolicyGenerator = &ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator{} + +// ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator generates a +// ValidatingAdmissionPolicy and its binding that denies creation/update/deletion of service accounts +// and creation of token requests in reserved namespaces, except for requests from certain +// whitelisted users and user groups. +// +// TO-DO (chenyu1): evaluate if it is appropriate to ban service accounts ops under +// all namespaces. +type ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator struct { + WhitelistedUsernames []string + WhitelistedUserGroups []string + ReservedNamespacePrefixes []string +} + +// Name returns the name of the generator, which is used to determine if a specific generator +// has been enabled or not. +func (g *ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator) Name() string { + return SvcAccountsAndTokenRequestsVAPGeneratorName +} + +// Validate validates the configuration of the generator. +func (g *ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator) Validate() error { + if len(g.ReservedNamespacePrefixes) == 0 { + return errors.NewUserError(nil, "at least one prefix must be specified") + } + // Check if any of the prefixes includes illegal characters. + for _, prefix := range g.ReservedNamespacePrefixes { + if !reservedNamespacePrefixRegExp.MatchString(prefix) { + return errors.NewUserError(nil, "prefix contains illegal characters; only lowercase alphanumeric characters and hyphens are allowed", "prefix", prefix) + } + } + // Check if any whitelisted username or user group contains characters that are + // illegal in a CEL string literal (\ and "). + for _, username := range g.WhitelistedUsernames { + if strings.ContainsAny(username, `"\`) { + return errors.NewUserError(nil, "whitelisted username contains illegal characters for a CEL expression", "username", username) + } + } + for _, userGroup := range g.WhitelistedUserGroups { + if strings.ContainsAny(userGroup, `"\`) { + return errors.NewUserError(nil, "whitelisted user group contains illegal characters for a CEL expression", "userGroup", userGroup) + } + } + return nil +} + +// Policies generates a ValidatingAdmissionPolicy that denies creation/update/deletion of service accounts +// and creation of token requests in reserved namespaces, except for requests from certain +// whitelisted users and user groups. +func (g *ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator) Policies() []*admissionregistrationv1.ValidatingAdmissionPolicy { + celExprAccSegs := []string{} + + // Exempt whitelisted users from this admission policy. + for _, username := range g.WhitelistedUsernames { + celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`request.userInfo.username == "%s"`, username)) + } + // Exempt whitelisted user groups from this admission policy. + for _, userGroup := range g.WhitelistedUserGroups { + celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`"%s" in request.userInfo.groups`, userGroup)) + } + // Exempt requests from the Kubernetes scheduler, any of the nodes, and (esp.) the + // Kubernetes controller manager from this admission policy. + // + // Important: the Kubernetes controller manager, when deployed with the option + // --use-service-account-credentials=true, creates a service account token for many of its controllers + // and uses those tokens to authenticate to the Kubernetes API server. It retrieves a token + // via the TokenRequest API; failure to exempt this scenario may lead to critical errors. + celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`request.userInfo.username == "%s"`, kubeSchedulerUserName)) + celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`request.userInfo.username == "%s"`, kubeControllerManagerUserName)) + celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`"%s" in request.userInfo.groups`, kubeNodeUserGroup)) + // Exempt requests from admin users from this admission policy. + celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`"%s" in request.userInfo.groups`, adminUserGroup)) + + celExprAcc := strings.Join(celExprAccSegs, " || ") + + celExprNSSegs := []string{} + for _, prefix := range g.ReservedNamespacePrefixes { + celExprNSSegs = append(celExprNSSegs, fmt.Sprintf(`object.metadata.namespace.startsWith("%s")`, prefix)) + } + celExprNS := strings.Join(celExprNSSegs, " || ") + + celExpr := fmt.Sprintf("!(%s) || (%s)", celExprNS, celExprAcc) + + policy := &admissionregistrationv1.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: svcAccountsAndTokenRequestsVAPPolicyName, + }, + Spec: admissionregistrationv1.ValidatingAdmissionPolicySpec{ + FailurePolicy: ptr.To(admissionregistrationv1.Fail), + MatchConstraints: &admissionregistrationv1.MatchResources{ + ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + admissionregistrationv1.Update, + admissionregistrationv1.Delete, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"serviceaccounts"}, + }, + }, + }, + { + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + // TokenRequest API is implemented as a subresource (token) of service + // accounts. It only supports the Create operation. + Resources: []string{"serviceaccounts/token"}, + }, + }, + }, + }, + }, + Validations: []admissionregistrationv1.Validation{ + { + Expression: celExpr, + // The error message has been set to match with the checks in some of the E2E tests + // in our existing release pipeline. + Message: "writing service accounts in reserved namespaces or requesting tokens from such service accounts is disallowed", + Reason: ptr.To(metav1.StatusReasonForbidden), + }, + }, + }, + } + return []*admissionregistrationv1.ValidatingAdmissionPolicy{policy} +} + +// PolicyBindings generates a ValidatingAdmissionPolicyBinding for the ValidatingAdmissionPolicy +// generated by the Policies() method. +func (g *ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator) PolicyBindings() []*admissionregistrationv1.ValidatingAdmissionPolicyBinding { + binding := &admissionregistrationv1.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: svcAccountsAndTokenRequestsVAPPolicyBindingName, + }, + Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: svcAccountsAndTokenRequestsVAPPolicyName, + ValidationActions: []admissionregistrationv1.ValidationAction{ + admissionregistrationv1.Deny, + }, + }, + } + return []*admissionregistrationv1.ValidatingAdmissionPolicyBinding{binding} +} diff --git a/pkg/utils/common.go b/pkg/utils/common.go index 50fdc74fb..762353d46 100644 --- a/pkg/utils/common.go +++ b/pkg/utils/common.go @@ -49,13 +49,13 @@ import ( ) const ( - kubePrefix = "kube-" - fleetPrefix = "fleet-" - fleetMemberNamespacePrefix = fleetPrefix + "member-" - FleetSystemNamespace = fleetPrefix + "system" - NamespaceNameFormat = fleetMemberNamespacePrefix + "%s" - RoleNameFormat = fleetPrefix + "role-%s" - RoleBindingNameFormat = fleetPrefix + "rolebinding-%s" + KubePrefix = "kube-" + FleetPrefix = "fleet-" + FleetMemberNamespacePrefix = FleetPrefix + "member-" + FleetSystemNamespace = FleetPrefix + "system" + NamespaceNameFormat = FleetMemberNamespacePrefix + "%s" + RoleNameFormat = FleetPrefix + "role-%s" + RoleBindingNameFormat = FleetPrefix + "rolebinding-%s" ValidationPathFmt = "/validate-%s-%s-%s" MutatingPathFmt = "/mutate-%s-%s-%s" lessGroupsStringFormat = "groups: %v" @@ -501,12 +501,12 @@ func CheckCRDInstalled(discoveryClient discovery.DiscoveryInterface, gvk schema. // IsReservedNamespace indicates if an argued namespace is reserved. func IsReservedNamespace(namespace string) bool { - return strings.HasPrefix(namespace, fleetPrefix) || strings.HasPrefix(namespace, kubePrefix) + return strings.HasPrefix(namespace, FleetPrefix) || strings.HasPrefix(namespace, KubePrefix) } // IsFleetMemberNamespace indicates if an argued namespace is a fleet member namespace. func IsFleetMemberNamespace(namespace string) bool { - return strings.HasPrefix(namespace, fleetMemberNamespacePrefix) + return strings.HasPrefix(namespace, FleetMemberNamespacePrefix) } // ShouldPropagateNamespace decides if we should propagate the resources in the namespace. From 82268c5951750098459227bdc75435e7a2777c95 Mon Sep 17 00:00:00 2001 From: michaelawyu Date: Thu, 14 May 2026 02:13:18 +1000 Subject: [PATCH 2/4] Minor fixes Signed-off-by: michaelawyu --- pkg/admissionpolicymanager/manager.go | 4 ++++ .../manager_integration_test.go | 16 +++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/pkg/admissionpolicymanager/manager.go b/pkg/admissionpolicymanager/manager.go index f3bdd72e8..5c6d82882 100644 --- a/pkg/admissionpolicymanager/manager.go +++ b/pkg/admissionpolicymanager/manager.go @@ -74,6 +74,10 @@ type PolicyManager struct { } func New(client client.Client, policyGeneratorConfigs *PolicyGeneratorConfigs, enabledPolicyNames []string) (*PolicyManager, error) { + if policyGeneratorConfigs == nil { + klog.V(2).Info("No admission policy generator configuration provided, falling back to the default configuration") + policyGeneratorConfigs = DefaultPolicyGeneratorConfigs + } // Prepare a set of generators based on the list of enabled policies. enabledPolicyGenerators, err := preparePolicyGenerators(policyGeneratorConfigs, enabledPolicyNames) if err != nil { diff --git a/pkg/admissionpolicymanager/manager_integration_test.go b/pkg/admissionpolicymanager/manager_integration_test.go index 43e4a1c49..21a8514a7 100644 --- a/pkg/admissionpolicymanager/manager_integration_test.go +++ b/pkg/admissionpolicymanager/manager_integration_test.go @@ -17,7 +17,6 @@ limitations under the License. package admissionpolicymanager import ( - "context" "fmt" "github.com/google/go-cmp/cmp" @@ -42,6 +41,9 @@ var ( lessFuncValidatingAdmissionPolicy = func(i, j admissionregistrationv1.ValidatingAdmissionPolicy) bool { return i.Name < j.Name } + lessFuncValidatingAdmissionPolicyBinding = func(i, j admissionregistrationv1.ValidatingAdmissionPolicyBinding) bool { + return i.Name < j.Name + } ) var _ = Describe("Policies, Policy Bindings and their Effects", Ordered, func() { @@ -170,13 +172,13 @@ var _ = Describe("Policies, Policy Bindings and their Effects", Ordered, func() }, } - Eventually(func() error { + Eventually(ctx, func() error { policyList := &admissionregistrationv1.ValidatingAdmissionPolicyList{} matchingLabels := map[string]string{ VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, } - if err := hubUncachedClient.List(context.TODO(), policyList, client.MatchingLabels(matchingLabels)); err != nil { + if err := hubUncachedClient.List(ctx, policyList, client.MatchingLabels(matchingLabels)); err != nil { return fmt.Errorf("failed to list matching policies: %w", err) } @@ -231,17 +233,13 @@ var _ = Describe("Policies, Policy Bindings and their Effects", Ordered, func() }, } - lessFuncValidatingAdmissionPolicyBinding := func(i, j admissionregistrationv1.ValidatingAdmissionPolicyBinding) bool { - return i.Name < j.Name - } - - Eventually(func() error { + Eventually(ctx, func() error { bindingList := &admissionregistrationv1.ValidatingAdmissionPolicyBindingList{} matchingLabels := map[string]string{ VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, } - if err := hubUncachedClient.List(context.TODO(), bindingList, client.MatchingLabels(matchingLabels)); err != nil { + if err := hubUncachedClient.List(ctx, bindingList, client.MatchingLabels(matchingLabels)); err != nil { return fmt.Errorf("failed to list matching policy bindings: %w", err) } From 21a5b94a4b8b5f05ca96c0e9a4c3a5d2ad5aebef Mon Sep 17 00:00:00 2001 From: michaelawyu Date: Tue, 19 May 2026 10:11:16 +1000 Subject: [PATCH 3/4] Minor fixes Signed-off-by: michaelawyu --- pkg/admissionpolicymanager/configs.go | 4 +- pkg/admissionpolicymanager/manager.go | 48 ++++++++++++++++--- .../manager_integration_test.go | 13 +++-- .../podsnreplicasets.go | 10 ++-- pkg/admissionpolicymanager/suite_test.go | 2 +- .../svcaccountsntokenreqs.go | 9 ++-- 6 files changed, 66 insertions(+), 20 deletions(-) diff --git a/pkg/admissionpolicymanager/configs.go b/pkg/admissionpolicymanager/configs.go index b65c4010d..cd9106e22 100644 --- a/pkg/admissionpolicymanager/configs.go +++ b/pkg/admissionpolicymanager/configs.go @@ -33,9 +33,9 @@ type PolicyGeneratorConfigs struct { // DefaultPolicyGeneratorConfigs is the default configuration for all available admission policy generators. var DefaultPolicyGeneratorConfigs = &PolicyGeneratorConfigs{ PodsAndReplicaSetsVAPGeneratorConfig: &PodsAndReplicaSetsValidatingAdmissionPolicyGenerator{ - ReservedNamespacePrefixes: []string{utils.FleetPrefix, utils.KubePrefix}, + ReservedNamespacePrefixes: []string{utils.FleetNSNamePrefix, utils.KubeNSNamePrefix}, }, SvcAccountsAndTokenRequestsVAPGeneratorConfig: &ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator{ - ReservedNamespacePrefixes: []string{utils.FleetPrefix, utils.KubePrefix}, + ReservedNamespacePrefixes: []string{utils.FleetNSNamePrefix, utils.KubeNSNamePrefix}, }, } diff --git a/pkg/admissionpolicymanager/manager.go b/pkg/admissionpolicymanager/manager.go index 5c6d82882..91adf7fb1 100644 --- a/pkg/admissionpolicymanager/manager.go +++ b/pkg/admissionpolicymanager/manager.go @@ -37,20 +37,27 @@ const ( // The following labels are added to all policies created by the policy manager, // so that the agent can track the lifecycle of created policies across different runs // and act accordingly. - VAPManagedByKubeFleetLabelKey = "app.kubernetes.io/managed-by" - VAPManagedByKubeFleetLabelValue = "fleet" - VAPPartOfKubeFleetLabelKey = "app.kubernetes.io/part-of" - VAPPartOfKubeFleetLabelValue = "fleet" + VAPManagedByKubeFleetLabelKey = "app.kubernetes.io/managed-by" + VAPManagedByKubeFleetLabelValue = "fleet-hub-agent" + VAPPartOfKubeFleetLabelKey = "app.kubernetes.io/part-of" + VAPPartOfKubeFleetLabelValue = "fleet" + VAPComponentKubeFleetLabelKey = "app.kubernetes.io/component" + VAPComponentAdmissionPolicyManagerLabelValue = "admission-policy-manager" ) var ( // A list of all available policy generators. - AllGenerators = sets.Set[string]{ + allGenerators = sets.Set[string]{ PodsAndReplicaSetsVAPGeneratorName: {}, SvcAccountsAndTokenRequestsVAPGeneratorName: {}, } ) +// AllGenerators returns a copy of all available policy generators. +func AllGenerators() sets.Set[string] { + return allGenerators.Clone() +} + var ( policyRWOpBackoff = wait.Backoff{ Steps: 3, @@ -163,13 +170,18 @@ func (m *PolicyManager) createOrUpdatePoliciesAndBindingsForEnabledGenerators(ct } policy.Labels[VAPManagedByKubeFleetLabelKey] = VAPManagedByKubeFleetLabelValue policy.Labels[VAPPartOfKubeFleetLabelKey] = VAPPartOfKubeFleetLabelValue + policy.Labels[VAPComponentKubeFleetLabelKey] = VAPComponentAdmissionPolicyManagerLabelValue policyToCreateOrUpdate := &admissionregistrationv1.ValidatingAdmissionPolicy{ ObjectMeta: policy.ObjectMeta, } err := retry.OnError(policyRWOpBackoff, func(err error) bool { - // Retry on any error. Note that nil errors are not passed to this function, + // Retry on any error expect for context cancellation. Note that nil errors are not passed to this function, // and AlreadyExists errors will not occur. + if ctx.Err() != nil { + // The main context has been cancelled. No need to retry anymore. + return false + } return true }, func() error { opRes, err := controllerutil.CreateOrUpdate(ctx, m.Client, policyToCreateOrUpdate, func() error { @@ -204,13 +216,18 @@ func (m *PolicyManager) createOrUpdatePoliciesAndBindingsForEnabledGenerators(ct } policyBinding.Labels[VAPManagedByKubeFleetLabelKey] = VAPManagedByKubeFleetLabelValue policyBinding.Labels[VAPPartOfKubeFleetLabelKey] = VAPPartOfKubeFleetLabelValue + policyBinding.Labels[VAPComponentKubeFleetLabelKey] = VAPComponentAdmissionPolicyManagerLabelValue policyBindingToCreateOrUpdate := &admissionregistrationv1.ValidatingAdmissionPolicyBinding{ ObjectMeta: policyBinding.ObjectMeta, } err := retry.OnError(policyRWOpBackoff, func(err error) bool { - // Retry on any error. Note that nil errors are not passed to this function, + // Retry on any error expect for context cancellation. Note that nil errors are not passed to this function, // and AlreadyExists errors will not occur. + if ctx.Err() != nil { + // The main context has been cancelled. No need to retry anymore. + return false + } return true }, func() error { opRes, err := controllerutil.CreateOrUpdate(ctx, m.Client, policyBindingToCreateOrUpdate, func() error { @@ -248,9 +265,14 @@ func (m *PolicyManager) garbageCollectUnusedPoliciesAndBindings(ctx context.Cont managedByAndPartOfKubeFleetLabelSelector := client.MatchingLabels{ VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, + VAPComponentKubeFleetLabelKey: VAPComponentAdmissionPolicyManagerLabelValue, } err := retry.OnError(policyRWOpBackoff, func(err error) bool { // Retry on any error. Note that nil errors are not passed to this function. + if ctx.Err() != nil { + // The main context has been cancelled. No need to retry anymore. + return false + } return true }, func() error { if err := m.Client.List(ctx, existingPolicyList, managedByAndPartOfKubeFleetLabelSelector); err != nil { @@ -265,6 +287,10 @@ func (m *PolicyManager) garbageCollectUnusedPoliciesAndBindings(ctx context.Cont err = retry.OnError(policyRWOpBackoff, func(err error) bool { // Retry on any error. Note that nil errors are not passed to this function. + if ctx.Err() != nil { + // The main context has been cancelled. No need to retry anymore. + return false + } return true }, func() error { if err := m.Client.List(ctx, existingPolicyBindingList, managedByAndPartOfKubeFleetLabelSelector); err != nil { @@ -283,6 +309,10 @@ func (m *PolicyManager) garbageCollectUnusedPoliciesAndBindings(ctx context.Cont if !createdOrUpdatedPolicyNames.Has(policy.Name) { err := retry.OnError(policyRWOpBackoff, func(err error) bool { // Retry on any error. Note that nil errors are not passed to this function, and NotFound errors will not occur. + if ctx.Err() != nil { + // The main context has been cancelled. No need to retry anymore. + return false + } return true }, func() error { if err := m.Client.Delete(ctx, policy); err != nil && !apierrors.IsNotFound(err) { @@ -308,6 +338,10 @@ func (m *PolicyManager) garbageCollectUnusedPoliciesAndBindings(ctx context.Cont if !createdOrUpdatedPolicyBindingNames.Has(policyBinding.Name) { err := retry.OnError(policyRWOpBackoff, func(err error) bool { // Retry on any error. Note that nil errors are not passed to this function, and NotFound errors will not occur. + if ctx.Err() != nil { + // The main context has been cancelled. No need to retry anymore. + return false + } return true }, func() error { if err := m.Client.Delete(ctx, policyBinding); err != nil && !apierrors.IsNotFound(err) { diff --git a/pkg/admissionpolicymanager/manager_integration_test.go b/pkg/admissionpolicymanager/manager_integration_test.go index 21a8514a7..6c9d076ed 100644 --- a/pkg/admissionpolicymanager/manager_integration_test.go +++ b/pkg/admissionpolicymanager/manager_integration_test.go @@ -21,7 +21,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/kubefleet-dev/kubefleet/pkg/utils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" @@ -29,6 +28,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kubefleet-dev/kubefleet/pkg/utils" ) const ( @@ -70,6 +71,7 @@ var _ = Describe("Policies, Policy Bindings and their Effects", Ordered, func() Labels: map[string]string{ VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, + VAPComponentKubeFleetLabelKey: VAPComponentAdmissionPolicyManagerLabelValue, }, }, Spec: admissionregistrationv1.ValidatingAdmissionPolicySpec{ @@ -109,7 +111,7 @@ var _ = Describe("Policies, Policy Bindings and their Effects", Ordered, func() }, Validations: []admissionregistrationv1.Validation{ { - Expression: `object.metadata.namespace.startsWith("fleet-") || object.metadata.namespace.startsWith("kube-")`, + Expression: `request.namespace.startsWith("fleet-") || request.namespace.startsWith("kube-")`, Message: "creating pods and replicas is disallowed in the fleet hub cluster", Reason: ptr.To(metav1.StatusReasonForbidden), }, @@ -122,6 +124,7 @@ var _ = Describe("Policies, Policy Bindings and their Effects", Ordered, func() Labels: map[string]string{ VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, + VAPComponentKubeFleetLabelKey: VAPComponentAdmissionPolicyManagerLabelValue, }, }, Spec: admissionregistrationv1.ValidatingAdmissionPolicySpec{ @@ -163,7 +166,7 @@ var _ = Describe("Policies, Policy Bindings and their Effects", Ordered, func() }, Validations: []admissionregistrationv1.Validation{ { - Expression: `!(object.metadata.namespace.startsWith("fleet-") || object.metadata.namespace.startsWith("kube-")) || (request.userInfo.username == "system:kube-scheduler" || request.userInfo.username == "system:kube-controller-manager" || "system:nodes" in request.userInfo.groups || "system:masters" in request.userInfo.groups)`, + Expression: `!(request.namespace.startsWith("fleet-") || request.namespace.startsWith("kube-")) || (request.userInfo.username == "system:kube-scheduler" || request.userInfo.username == "system:kube-controller-manager" || "system:nodes" in request.userInfo.groups || "system:masters" in request.userInfo.groups)`, Message: "writing service accounts in reserved namespaces or requesting tokens from such service accounts is disallowed", Reason: ptr.To(metav1.StatusReasonForbidden), }, @@ -177,6 +180,7 @@ var _ = Describe("Policies, Policy Bindings and their Effects", Ordered, func() matchingLabels := map[string]string{ VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, + VAPComponentKubeFleetLabelKey: VAPComponentAdmissionPolicyManagerLabelValue, } if err := hubUncachedClient.List(ctx, policyList, client.MatchingLabels(matchingLabels)); err != nil { return fmt.Errorf("failed to list matching policies: %w", err) @@ -207,6 +211,7 @@ var _ = Describe("Policies, Policy Bindings and their Effects", Ordered, func() Labels: map[string]string{ VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, + VAPComponentKubeFleetLabelKey: VAPComponentAdmissionPolicyManagerLabelValue, }, }, Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{ @@ -222,6 +227,7 @@ var _ = Describe("Policies, Policy Bindings and their Effects", Ordered, func() Labels: map[string]string{ VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, + VAPComponentKubeFleetLabelKey: VAPComponentAdmissionPolicyManagerLabelValue, }, }, Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{ @@ -238,6 +244,7 @@ var _ = Describe("Policies, Policy Bindings and their Effects", Ordered, func() matchingLabels := map[string]string{ VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, + VAPComponentKubeFleetLabelKey: VAPComponentAdmissionPolicyManagerLabelValue, } if err := hubUncachedClient.List(ctx, bindingList, client.MatchingLabels(matchingLabels)); err != nil { return fmt.Errorf("failed to list matching policy bindings: %w", err) diff --git a/pkg/admissionpolicymanager/podsnreplicasets.go b/pkg/admissionpolicymanager/podsnreplicasets.go index a6e42f15b..458991e8c 100644 --- a/pkg/admissionpolicymanager/podsnreplicasets.go +++ b/pkg/admissionpolicymanager/podsnreplicasets.go @@ -37,8 +37,8 @@ const ( podsAndReplicaSetsVAPPolicyBindingName = "deny-pods-and-replicasets-outside-reserved-namespaces-binding" ) -// reservedNamespacePrefixRegExp matches valid namespace prefix characters (DNS label subset). -var reservedNamespacePrefixRegExp = regexp.MustCompile(`^[a-z0-9-]+$`) +// reservedNamespacePrefixRegexp matches valid namespace prefix characters (DNS label subset). +var reservedNamespacePrefixRegexp = regexp.MustCompile(`^[a-z0-9-]+$`) // Verify that PodsAndReplicaSetsValidatingAdmissionPolicyGenerator implements // the ValidatingAdmissionPolicyGenerator interface. @@ -63,7 +63,7 @@ func (g *PodsAndReplicaSetsValidatingAdmissionPolicyGenerator) Validate() error } // Check if any of the prefixes includes illegal characters. for _, prefix := range g.ReservedNamespacePrefixes { - if !reservedNamespacePrefixRegExp.MatchString(prefix) { + if !reservedNamespacePrefixRegexp.MatchString(prefix) { return errors.NewUserError(nil, "prefix contains illegal characters; only lowercase alphanumeric characters and hyphens are allowed", "prefix", prefix) } } @@ -72,10 +72,12 @@ func (g *PodsAndReplicaSetsValidatingAdmissionPolicyGenerator) Validate() error // Policies generates a ValidatingAdmissionPolicy that denies creation of pods and // replicasets in non-reserved namespaces. +// +// For simplicity reasons, the code here assumes that the generator has been validated before Policies() is called. func (g *PodsAndReplicaSetsValidatingAdmissionPolicyGenerator) Policies() []*admissionregistrationv1.ValidatingAdmissionPolicy { celExprSegs := []string{} for _, prefix := range g.ReservedNamespacePrefixes { - celExprSegs = append(celExprSegs, fmt.Sprintf(`object.metadata.namespace.startsWith("%s")`, prefix)) + celExprSegs = append(celExprSegs, fmt.Sprintf(`request.namespace.startsWith("%s")`, prefix)) } celExpr := strings.Join(celExprSegs, " || ") diff --git a/pkg/admissionpolicymanager/suite_test.go b/pkg/admissionpolicymanager/suite_test.go index 9e258f7da..45df92091 100644 --- a/pkg/admissionpolicymanager/suite_test.go +++ b/pkg/admissionpolicymanager/suite_test.go @@ -95,7 +95,7 @@ var _ = BeforeSuite(func() { By("Setting up the policy manager") enabledGeneratorNames := []string{} - for name := range AllGenerators { + for name := range allGenerators { enabledGeneratorNames = append(enabledGeneratorNames, name) } policyManager, err := New(hubUncachedClient, DefaultPolicyGeneratorConfigs, enabledGeneratorNames) diff --git a/pkg/admissionpolicymanager/svcaccountsntokenreqs.go b/pkg/admissionpolicymanager/svcaccountsntokenreqs.go index 6009c9f34..9ff9927b9 100644 --- a/pkg/admissionpolicymanager/svcaccountsntokenreqs.go +++ b/pkg/admissionpolicymanager/svcaccountsntokenreqs.go @@ -28,7 +28,8 @@ import ( ) const ( - SvcAccountsAndTokenRequestsVAPGeneratorName = "DenyServiceAccountsAndTokenRequestsInReservedNamespaces" + // Exempt the value from the linter as it miscategorizes it as a credential. + SvcAccountsAndTokenRequestsVAPGeneratorName = "DenyServiceAccountsAndTokenRequestsInReservedNamespaces" //nolint:gosec ) const ( @@ -74,7 +75,7 @@ func (g *ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator) Vali } // Check if any of the prefixes includes illegal characters. for _, prefix := range g.ReservedNamespacePrefixes { - if !reservedNamespacePrefixRegExp.MatchString(prefix) { + if !reservedNamespacePrefixRegexp.MatchString(prefix) { return errors.NewUserError(nil, "prefix contains illegal characters; only lowercase alphanumeric characters and hyphens are allowed", "prefix", prefix) } } @@ -96,6 +97,8 @@ func (g *ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator) Vali // Policies generates a ValidatingAdmissionPolicy that denies creation/update/deletion of service accounts // and creation of token requests in reserved namespaces, except for requests from certain // whitelisted users and user groups. +// +// For simplicity reasons, the code here assumes that the generator has been validated before Policies() is called. func (g *ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator) Policies() []*admissionregistrationv1.ValidatingAdmissionPolicy { celExprAccSegs := []string{} @@ -124,7 +127,7 @@ func (g *ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator) Poli celExprNSSegs := []string{} for _, prefix := range g.ReservedNamespacePrefixes { - celExprNSSegs = append(celExprNSSegs, fmt.Sprintf(`object.metadata.namespace.startsWith("%s")`, prefix)) + celExprNSSegs = append(celExprNSSegs, fmt.Sprintf(`request.namespace.startsWith("%s")`, prefix)) } celExprNS := strings.Join(celExprNSSegs, " || ") From a873ccaf4db698a1c13a84dc44b3bda0a186eb76 Mon Sep 17 00:00:00 2001 From: michaelawyu Date: Tue, 19 May 2026 11:05:22 +1000 Subject: [PATCH 4/4] Minor fixes Signed-off-by: michaelawyu --- pkg/utils/common.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/utils/common.go b/pkg/utils/common.go index 762353d46..c772f8c9f 100644 --- a/pkg/utils/common.go +++ b/pkg/utils/common.go @@ -49,13 +49,13 @@ import ( ) const ( - KubePrefix = "kube-" - FleetPrefix = "fleet-" - FleetMemberNamespacePrefix = FleetPrefix + "member-" - FleetSystemNamespace = FleetPrefix + "system" + KubeNSNamePrefix = "kube-" + FleetNSNamePrefix = "fleet-" + FleetMemberNamespacePrefix = FleetNSNamePrefix + "member-" + FleetSystemNamespace = FleetNSNamePrefix + "system" NamespaceNameFormat = FleetMemberNamespacePrefix + "%s" - RoleNameFormat = FleetPrefix + "role-%s" - RoleBindingNameFormat = FleetPrefix + "rolebinding-%s" + RoleNameFormat = FleetNSNamePrefix + "role-%s" + RoleBindingNameFormat = FleetNSNamePrefix + "rolebinding-%s" ValidationPathFmt = "/validate-%s-%s-%s" MutatingPathFmt = "/mutate-%s-%s-%s" lessGroupsStringFormat = "groups: %v" @@ -501,7 +501,7 @@ func CheckCRDInstalled(discoveryClient discovery.DiscoveryInterface, gvk schema. // IsReservedNamespace indicates if an argued namespace is reserved. func IsReservedNamespace(namespace string) bool { - return strings.HasPrefix(namespace, FleetPrefix) || strings.HasPrefix(namespace, KubePrefix) + return strings.HasPrefix(namespace, FleetNSNamePrefix) || strings.HasPrefix(namespace, KubeNSNamePrefix) } // IsFleetMemberNamespace indicates if an argued namespace is a fleet member namespace.