diff --git a/pkg/admissionpolicymanager/configs.go b/pkg/admissionpolicymanager/configs.go new file mode 100644 index 000000000..cd9106e22 --- /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.FleetNSNamePrefix, utils.KubeNSNamePrefix}, + }, + SvcAccountsAndTokenRequestsVAPGeneratorConfig: &ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator{ + ReservedNamespacePrefixes: []string{utils.FleetNSNamePrefix, utils.KubeNSNamePrefix}, + }, +} diff --git a/pkg/admissionpolicymanager/manager.go b/pkg/admissionpolicymanager/manager.go new file mode 100644 index 000000000..91adf7fb1 --- /dev/null +++ b/pkg/admissionpolicymanager/manager.go @@ -0,0 +1,366 @@ +/* +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-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]{ + 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, + 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) { + 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 { + 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 + policy.Labels[VAPComponentKubeFleetLabelKey] = VAPComponentAdmissionPolicyManagerLabelValue + + policyToCreateOrUpdate := &admissionregistrationv1.ValidatingAdmissionPolicy{ + ObjectMeta: policy.ObjectMeta, + } + err := retry.OnError(policyRWOpBackoff, func(err error) bool { + // 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 { + 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 + policyBinding.Labels[VAPComponentKubeFleetLabelKey] = VAPComponentAdmissionPolicyManagerLabelValue + + policyBindingToCreateOrUpdate := &admissionregistrationv1.ValidatingAdmissionPolicyBinding{ + ObjectMeta: policyBinding.ObjectMeta, + } + err := retry.OnError(policyRWOpBackoff, func(err error) bool { + // 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 { + 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, + 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 { + 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. + 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 { + 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. + 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) { + 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. + 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) { + 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..6c9d076ed --- /dev/null +++ b/pkg/admissionpolicymanager/manager_integration_test.go @@ -0,0 +1,272 @@ +/* +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" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + . "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" + + "github.com/kubefleet-dev/kubefleet/pkg/utils" +) + +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 + } + lessFuncValidatingAdmissionPolicyBinding = func(i, j admissionregistrationv1.ValidatingAdmissionPolicyBinding) 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, + VAPComponentKubeFleetLabelKey: VAPComponentAdmissionPolicyManagerLabelValue, + }, + }, + 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: `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), + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: svcAccountsAndTokenRequestsVAPPolicyName, + Labels: map[string]string{ + VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, + VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, + VAPComponentKubeFleetLabelKey: VAPComponentAdmissionPolicyManagerLabelValue, + }, + }, + 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: `!(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), + }, + }, + }, + }, + } + + Eventually(ctx, func() error { + policyList := &admissionregistrationv1.ValidatingAdmissionPolicyList{} + 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) + } + + 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, + VAPComponentKubeFleetLabelKey: VAPComponentAdmissionPolicyManagerLabelValue, + }, + }, + Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: podsAndReplicaSetsVAPPolicyName, + ValidationActions: []admissionregistrationv1.ValidationAction{ + admissionregistrationv1.Deny, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: svcAccountsAndTokenRequestsVAPPolicyBindingName, + Labels: map[string]string{ + VAPManagedByKubeFleetLabelKey: VAPManagedByKubeFleetLabelValue, + VAPPartOfKubeFleetLabelKey: VAPPartOfKubeFleetLabelValue, + VAPComponentKubeFleetLabelKey: VAPComponentAdmissionPolicyManagerLabelValue, + }, + }, + Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{ + PolicyName: svcAccountsAndTokenRequestsVAPPolicyName, + ValidationActions: []admissionregistrationv1.ValidationAction{ + admissionregistrationv1.Deny, + }, + }, + }, + } + + Eventually(ctx, func() error { + bindingList := &admissionregistrationv1.ValidatingAdmissionPolicyBindingList{} + 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) + } + + 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..458991e8c --- /dev/null +++ b/pkg/admissionpolicymanager/podsnreplicasets.go @@ -0,0 +1,147 @@ +/* +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. +// +// 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(`request.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..45df92091 --- /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..9ff9927b9 --- /dev/null +++ b/pkg/admissionpolicymanager/svcaccountsntokenreqs.go @@ -0,0 +1,203 @@ +/* +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 ( + // Exempt the value from the linter as it miscategorizes it as a credential. + SvcAccountsAndTokenRequestsVAPGeneratorName = "DenyServiceAccountsAndTokenRequestsInReservedNamespaces" //nolint:gosec +) + +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. +// +// 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{} + + // 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(`request.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..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" - NamespaceNameFormat = fleetMemberNamespacePrefix + "%s" - RoleNameFormat = fleetPrefix + "role-%s" - RoleBindingNameFormat = fleetPrefix + "rolebinding-%s" + KubeNSNamePrefix = "kube-" + FleetNSNamePrefix = "fleet-" + FleetMemberNamespacePrefix = FleetNSNamePrefix + "member-" + FleetSystemNamespace = FleetNSNamePrefix + "system" + NamespaceNameFormat = FleetMemberNamespacePrefix + "%s" + RoleNameFormat = FleetNSNamePrefix + "role-%s" + RoleBindingNameFormat = FleetNSNamePrefix + "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, FleetNSNamePrefix) || strings.HasPrefix(namespace, KubeNSNamePrefix) } // 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.