mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-22 05:57:02 +00:00
* chore: save progress Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> --------- Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
663 lines
16 KiB
Go
663 lines
16 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
package ruleengine
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"regexp"
|
|
"strings"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
k8smeta "k8s.io/apimachinery/pkg/api/meta"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
k8svalidation "k8s.io/apimachinery/pkg/util/validation"
|
|
|
|
"github.com/projectcapsule/capsule/pkg/api/rules"
|
|
"github.com/projectcapsule/capsule/pkg/api/runtime"
|
|
workloadruntime "github.com/projectcapsule/capsule/pkg/runtime/workloads"
|
|
)
|
|
|
|
func ValidateRuleStatusBody(
|
|
mapper k8smeta.RESTMapper,
|
|
bodies []*rules.NamespaceRuleBodyNamespace,
|
|
) error {
|
|
quotaNames := make(map[string]string)
|
|
|
|
for i, rule := range bodies {
|
|
if rule == nil {
|
|
continue
|
|
}
|
|
|
|
if err := validateAudience(i, rule.Audience); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := validateQuotaRules(i, rule.Quota, quotaNames); err != nil {
|
|
return err
|
|
}
|
|
|
|
if rule.Enforce == nil {
|
|
continue
|
|
}
|
|
|
|
if err := validateWorkloadRules(i, rule.Enforce.Workloads); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := validateServiceRules(i, rule.Enforce.Services); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := validateIngressRules(i, rule.Enforce.Ingress); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := validateMetadataRules(i, rule.Enforce.Metadata, mapper); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateQuotaRules(ruleIndex int, quotas []rules.ResourceQuotaRule, names map[string]string) error {
|
|
for quotaIndex, quota := range quotas {
|
|
path := fmt.Sprintf("rules[%d].quota[%d]", ruleIndex, quotaIndex)
|
|
if errs := k8svalidation.IsDNS1123Label(quota.Name); len(errs) > 0 {
|
|
return fmt.Errorf("%s.name %q is invalid: %s", path, quota.Name, strings.Join(errs, "; "))
|
|
}
|
|
|
|
if previous, found := names[quota.Name]; found {
|
|
return fmt.Errorf(
|
|
"%s.name %q is invalid: quota name is already used by %s",
|
|
path,
|
|
quota.Name,
|
|
previous,
|
|
)
|
|
}
|
|
|
|
names[quota.Name] = path
|
|
|
|
if len(quota.Hard) == 0 {
|
|
return fmt.Errorf("%s.hard is invalid: at least one resource is required", path)
|
|
}
|
|
|
|
for name, quantity := range quota.Hard {
|
|
if quantity.Sign() < 0 {
|
|
return fmt.Errorf(
|
|
"rules[%d].quota[%d].hard[%q] is invalid: quantity must not be negative",
|
|
ruleIndex,
|
|
quotaIndex,
|
|
name,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateIngressRules(
|
|
ruleIndex int,
|
|
ingress rules.NamespaceRuleEnforceIngressBody,
|
|
) error {
|
|
if len(ingress.Types) == 0 && len(ingress.Hostnames) > 0 {
|
|
return fmt.Errorf(
|
|
"rules[%d].enforce.ingress.types is invalid: types must be configured when hostnames are configured",
|
|
ruleIndex,
|
|
)
|
|
}
|
|
|
|
if len(ingress.Types) > 0 && len(ingress.Hostnames) == 0 {
|
|
return fmt.Errorf(
|
|
"rules[%d].enforce.ingress.hostnames is invalid: hostnames must be configured when types are configured",
|
|
ruleIndex,
|
|
)
|
|
}
|
|
|
|
for i, resourceType := range ingress.Types {
|
|
switch resourceType {
|
|
case rules.IngressTypeIngress, rules.IngressTypeRoute,
|
|
rules.IngressTypeListenerSet,
|
|
rules.IngressTypeHTTPRoute,
|
|
rules.IngressTypeGateway,
|
|
rules.IngressTypeTLSRoute,
|
|
rules.IngressTypeGRPCRoute:
|
|
default:
|
|
return fmt.Errorf(
|
|
"rules[%d].enforce.ingress.types[%d] %q is invalid: unsupported ingress resource type",
|
|
ruleIndex,
|
|
i,
|
|
resourceType,
|
|
)
|
|
}
|
|
}
|
|
|
|
for i, hostname := range ingress.Hostnames {
|
|
if err := validateExpressionMatch(
|
|
hostname,
|
|
fmt.Sprintf("rules[%d].enforce.ingress.hostnames[%d]", ruleIndex, i),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateAudience(ruleIndex int, audience []rules.Audience) error {
|
|
for i, subject := range audience {
|
|
path := fmt.Sprintf("rules[%d].audience[%d]", ruleIndex, i)
|
|
if strings.TrimSpace(subject.Name) == "" {
|
|
return fmt.Errorf("%s.name is invalid: name is empty", path)
|
|
}
|
|
|
|
switch subject.Kind {
|
|
case rules.AudienceKindUser, rules.AudienceKindGroup, rules.AudienceKindServiceAccount:
|
|
case rules.AudienceKindCustom:
|
|
switch rules.CustomAudience(subject.Name) {
|
|
case rules.CustomAudienceCapsuleUser, rules.CustomAudienceAdministrator, rules.CustomAudienceTenantOwner, rules.CustomAudienceController:
|
|
default:
|
|
return fmt.Errorf("%s.name %q is invalid: unsupported custom audience", path, subject.Name)
|
|
}
|
|
default:
|
|
return fmt.Errorf("%s.kind %q is invalid: unsupported audience kind", path, subject.Kind)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateWorkloadRules(
|
|
ruleIndex int,
|
|
workloads rules.NamespaceRuleEnforceWorkloadsBody,
|
|
) error {
|
|
for j, target := range workloads.Targets {
|
|
switch target {
|
|
case rules.DeprecatedValidateImages,
|
|
rules.ValidatePod,
|
|
rules.ValidateInitContainers,
|
|
rules.ValidateEphemeralContainers,
|
|
rules.ValidateContainers,
|
|
rules.ValidateVolumes:
|
|
default:
|
|
return fmt.Errorf(
|
|
"rules[%d].enforce.workloads.targets[%d] %q is invalid: unsupported workload target",
|
|
ruleIndex,
|
|
j,
|
|
target,
|
|
)
|
|
}
|
|
}
|
|
|
|
if err := validateWorkloadResourceRules(ruleIndex, workloads); err != nil {
|
|
return err
|
|
}
|
|
|
|
for j, registry := range workloads.Registries {
|
|
if err := validateExpression(
|
|
registry.Expression,
|
|
fmt.Sprintf("rules[%d].enforce.workloads.registries[%d].exp", ruleIndex, j),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for j, scheduler := range workloads.Schedulers {
|
|
if err := validateExpression(
|
|
scheduler.Expression,
|
|
fmt.Sprintf("rules[%d].enforce.workloads.schedulers[%d].exp", ruleIndex, j),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateWorkloadResourceRules(
|
|
ruleIndex int,
|
|
workloads rules.NamespaceRuleEnforceWorkloadsBody,
|
|
) error {
|
|
resources := workloads.Resources
|
|
if resources == nil {
|
|
return nil
|
|
}
|
|
|
|
path := fmt.Sprintf("rules[%d].enforce.workloads.resources", ruleIndex)
|
|
if len(resources.Requests) == 0 && len(resources.Limits) == 0 {
|
|
return fmt.Errorf("%s is invalid: at least one request or limit policy is required", path)
|
|
}
|
|
|
|
podTarget, err := validateWorkloadResourceTargets(path, workloads.Targets)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := validateWorkloadRequestPolicies(path, resources.Requests, podTarget); err != nil {
|
|
return err
|
|
}
|
|
|
|
return validateWorkloadLimitPolicies(path, resources, podTarget)
|
|
}
|
|
|
|
func validateWorkloadResourceTargets(
|
|
path string,
|
|
targets []rules.WorkloadValidationTarget,
|
|
) (bool, error) {
|
|
podTarget := false
|
|
|
|
for _, target := range targets {
|
|
switch target {
|
|
case rules.ValidatePod:
|
|
podTarget = true
|
|
case rules.ValidateContainers, rules.ValidateInitContainers:
|
|
case rules.ValidateEphemeralContainers, rules.ValidateVolumes, rules.DeprecatedValidateImages:
|
|
return false, fmt.Errorf(
|
|
"%s is invalid: workload target %q does not support resource policies",
|
|
path,
|
|
target,
|
|
)
|
|
}
|
|
}
|
|
|
|
return podTarget, nil
|
|
}
|
|
|
|
func validateWorkloadRequestPolicies(
|
|
path string,
|
|
policies map[corev1.ResourceName]rules.WorkloadResourceRequestPolicy,
|
|
podTarget bool,
|
|
) error {
|
|
for name, policy := range policies {
|
|
policyPath := fmt.Sprintf("%s.requests[%q]", path, name)
|
|
if err := validateWorkloadResourceName(name, podTarget); err != nil {
|
|
return fmt.Errorf("%s is invalid: %w", policyPath, err)
|
|
}
|
|
|
|
switch policy.Policy {
|
|
case rules.WorkloadResourceRequestPolicyPreserve,
|
|
rules.WorkloadResourceRequestPolicyRemove:
|
|
if policy.Value != nil {
|
|
return fmt.Errorf("%s.value is invalid: value is only supported by the Default policy", policyPath)
|
|
}
|
|
case rules.WorkloadResourceRequestPolicyDefault:
|
|
if err := validateDefaultResourceQuantity(policyPath, policy.Value); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("%s.policy %q is invalid: unsupported request policy", policyPath, policy.Policy)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateWorkloadLimitPolicies(
|
|
path string,
|
|
resources *rules.WorkloadResourceRules,
|
|
podTarget bool,
|
|
) error {
|
|
one := resource.MustParse("1")
|
|
|
|
for name, policy := range resources.Limits {
|
|
policyPath := fmt.Sprintf("%s.limits[%q]", path, name)
|
|
if err := validateWorkloadResourceName(name, podTarget); err != nil {
|
|
return fmt.Errorf("%s is invalid: %w", policyPath, err)
|
|
}
|
|
|
|
switch policy.Policy {
|
|
case rules.WorkloadResourceLimitPolicyPreserve,
|
|
rules.WorkloadResourceLimitPolicyRemove,
|
|
rules.WorkloadResourceLimitPolicyMatchRequest:
|
|
if policy.Value != nil {
|
|
return fmt.Errorf(
|
|
"%s.value is invalid: value is only supported by the Default and Ratio policies",
|
|
policyPath,
|
|
)
|
|
}
|
|
case rules.WorkloadResourceLimitPolicyDefault:
|
|
if err := validateDefaultResourceQuantity(policyPath, policy.Value); err != nil {
|
|
return err
|
|
}
|
|
case rules.WorkloadResourceLimitPolicyRatio:
|
|
if policy.Value == nil {
|
|
return fmt.Errorf("%s.value is invalid: Ratio requires a value", policyPath)
|
|
}
|
|
|
|
if !workloadruntime.RatioSupportedResource(name) {
|
|
return fmt.Errorf(
|
|
"%s.policy is invalid: Ratio is only supported for cpu, memory, and ephemeral-storage",
|
|
policyPath,
|
|
)
|
|
}
|
|
|
|
if policy.Value.Cmp(one) < 0 {
|
|
return fmt.Errorf("%s.value is invalid: Ratio must be greater than or equal to 1", policyPath)
|
|
}
|
|
default:
|
|
return fmt.Errorf("%s.policy %q is invalid: unsupported limit policy", policyPath, policy.Policy)
|
|
}
|
|
|
|
if requestPolicy, found := resources.Requests[name]; found &&
|
|
requestPolicy.Policy == rules.WorkloadResourceRequestPolicyRemove &&
|
|
(policy.Policy == rules.WorkloadResourceLimitPolicyMatchRequest ||
|
|
policy.Policy == rules.WorkloadResourceLimitPolicyRatio) {
|
|
return fmt.Errorf(
|
|
"%s is invalid: %s requires a request which is removed by the request policy",
|
|
policyPath,
|
|
policy.Policy,
|
|
)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateDefaultResourceQuantity(path string, value *resource.Quantity) error {
|
|
if value == nil {
|
|
return fmt.Errorf("%s.value is invalid: Default requires a value", path)
|
|
}
|
|
|
|
if value.Sign() < 0 {
|
|
return fmt.Errorf("%s.value is invalid: quantity must not be negative", path)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateWorkloadResourceName(name corev1.ResourceName, podTarget bool) error {
|
|
if errs := k8svalidation.IsQualifiedName(string(name)); len(errs) > 0 {
|
|
return fmt.Errorf("resource name is invalid: %s", strings.Join(errs, "; "))
|
|
}
|
|
|
|
if !podTarget {
|
|
return nil
|
|
}
|
|
|
|
if workloadruntime.PodLevelResourceSupported(name) {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("resource %q is not supported by pod-level resources", name)
|
|
}
|
|
|
|
func validateServiceRules(
|
|
ruleIndex int,
|
|
services rules.NamespaceRuleEnforceServicesBody,
|
|
) error {
|
|
for j, serviceType := range services.Types {
|
|
if err := validateServiceType(serviceType); err != nil {
|
|
return fmt.Errorf(
|
|
"rules[%d].enforce.services.types[%d] %q is invalid: %w",
|
|
ruleIndex,
|
|
j,
|
|
serviceType,
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
|
|
if services.LoadBalancers != nil {
|
|
for j, cidr := range services.LoadBalancers.CIDRs {
|
|
if err := validateCIDR(cidr); err != nil {
|
|
return fmt.Errorf(
|
|
"rules[%d].enforce.services.loadBalancers.cidrs[%d] %q is invalid: %w",
|
|
ruleIndex,
|
|
j,
|
|
cidr,
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
if services.ExternalNames != nil {
|
|
for j, hostname := range services.ExternalNames.Hostnames {
|
|
if err := validateExpressionMatch(
|
|
hostname,
|
|
fmt.Sprintf("rules[%d].enforce.services.externalNames.hostnames[%d]", ruleIndex, j),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if services.NodePorts != nil {
|
|
for j, portRange := range services.NodePorts.Ports {
|
|
if err := validateNodePortRange(portRange); err != nil {
|
|
return fmt.Errorf(
|
|
"rules[%d].enforce.services.nodePorts.ports[%d] is invalid: %w",
|
|
ruleIndex,
|
|
j,
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateMetadataRules(
|
|
ruleIndex int,
|
|
metadata []rules.MetadataRule,
|
|
mapper k8smeta.RESTMapper,
|
|
) error {
|
|
for j, rule := range metadata {
|
|
fieldPath := fmt.Sprintf("rules[%d].enforce.metadata[%d]", ruleIndex, j)
|
|
|
|
if rule.HasWildcard() && metadataRuleHasManagedValues(rule) {
|
|
return fmt.Errorf("%s is invalid: managed metadata requires concrete apiGroups and kinds", fieldPath)
|
|
}
|
|
|
|
if err := validateMetadataTargets(fieldPath, rule, mapper); err != nil {
|
|
return err
|
|
}
|
|
|
|
for key, policy := range rule.Labels {
|
|
if err := validateMetadataKey(key); err != nil {
|
|
return fmt.Errorf(
|
|
"%s.labels[%q] is invalid: %w",
|
|
fieldPath,
|
|
key,
|
|
err,
|
|
)
|
|
}
|
|
|
|
if err := validateMutableMetadataKey(key, policy); err != nil {
|
|
return fmt.Errorf("%s.labels[%q] is invalid: %w", fieldPath, key, err)
|
|
}
|
|
|
|
for k, matcher := range policy.Values {
|
|
if err := validateExpressionMatch(
|
|
matcher,
|
|
fmt.Sprintf("%s.labels[%q].values[%d]", fieldPath, key, k),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
for key, policy := range rule.Annotations {
|
|
if err := validateMetadataKey(key); err != nil {
|
|
return fmt.Errorf(
|
|
"%s.annotations[%q] is invalid: %w",
|
|
fieldPath,
|
|
key,
|
|
err,
|
|
)
|
|
}
|
|
|
|
if err := validateMutableMetadataKey(key, policy); err != nil {
|
|
return fmt.Errorf("%s.annotations[%q] is invalid: %w", fieldPath, key, err)
|
|
}
|
|
|
|
for k, matcher := range policy.Values {
|
|
if err := validateExpressionMatch(
|
|
matcher,
|
|
fmt.Sprintf("%s.annotations[%q].values[%d]", fieldPath, key, k),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func metadataRuleHasManagedValues(rule rules.MetadataRule) bool {
|
|
for _, policy := range rule.Labels {
|
|
if policy.Managed != nil {
|
|
return true
|
|
}
|
|
}
|
|
|
|
for _, policy := range rule.Annotations {
|
|
if policy.Managed != nil {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func validateMutableMetadataKey(key string, policy rules.MetadataValueRule) error {
|
|
if policy.Default == nil && policy.Managed == nil {
|
|
return nil
|
|
}
|
|
|
|
if errs := k8svalidation.IsQualifiedName(strings.TrimSpace(key)); len(errs) > 0 {
|
|
return errors.New("default and managed require a concrete metadata key")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateMetadataKey(key string) error {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
return errors.New("key is empty")
|
|
}
|
|
|
|
if !strings.ContainsAny(key, "*[](){}+?|^$\\") {
|
|
if errs := k8svalidation.IsQualifiedName(key); len(errs) > 0 {
|
|
return errors.New(strings.Join(errs, ", "))
|
|
}
|
|
}
|
|
|
|
expression := rules.MetadataKeyExpression(key)
|
|
if _, err := regexp.Compile(expression.Expression); err != nil {
|
|
return fmt.Errorf("invalid key expression %q: %w", key, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateExpressionMatch(match runtime.ExpressionMatch, fieldPath string) error {
|
|
if err := validateExpression(match.Expression, fieldPath+".exp"); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateExpression(expression string, fieldPath string) error {
|
|
if strings.TrimSpace(expression) == "" {
|
|
return nil
|
|
}
|
|
|
|
if _, err := regexp.Compile(expression); err != nil {
|
|
return fmt.Errorf("%s %q is invalid: %w", fieldPath, expression, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateServiceType(serviceType rules.ServiceType) error {
|
|
switch serviceType {
|
|
case rules.ServiceTypeClusterIP,
|
|
rules.ServiceTypeNodePort,
|
|
rules.ServiceTypeLoadBalancer,
|
|
rules.ServiceTypeExternalName:
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unsupported service type")
|
|
}
|
|
}
|
|
|
|
func validateCIDR(raw string) error {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return fmt.Errorf("CIDR is empty")
|
|
}
|
|
|
|
if !strings.Contains(raw, "/") {
|
|
ip := net.ParseIP(raw)
|
|
if ip == nil {
|
|
return fmt.Errorf("must be a valid IP or CIDR")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if _, _, err := net.ParseCIDR(raw); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateNodePortRange(portRange rules.ServiceNodePortRange) error {
|
|
if portRange.From < 1 || portRange.From > 65535 {
|
|
return fmt.Errorf("from %d must be between 1 and 65535", portRange.From)
|
|
}
|
|
|
|
if portRange.To < 1 || portRange.To > 65535 {
|
|
return fmt.Errorf("to %d must be between 1 and 65535", portRange.To)
|
|
}
|
|
|
|
if portRange.From > portRange.To {
|
|
return fmt.Errorf("from %d must be lower than or equal to %d", portRange.From, portRange.To)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateMetadataTargets(
|
|
fieldPath string,
|
|
rule rules.MetadataRule,
|
|
mapper k8smeta.RESTMapper,
|
|
) error {
|
|
if len(rule.Kinds) == 0 {
|
|
return fmt.Errorf("%s.kinds is invalid: at least one kind must be configured", fieldPath)
|
|
}
|
|
|
|
for i, kind := range rule.Kinds {
|
|
kind = strings.TrimSpace(kind)
|
|
if kind == "" {
|
|
return fmt.Errorf("%s.kinds[%d] is invalid: kind is empty", fieldPath, i)
|
|
}
|
|
}
|
|
|
|
if mapper == nil {
|
|
return nil
|
|
}
|
|
|
|
if err := rule.ValidateKnownKindsWithScope(mapper, fieldPath, func(
|
|
gvk schema.GroupVersionKind,
|
|
scope k8smeta.RESTScope,
|
|
) bool {
|
|
return scope.Name() == k8smeta.RESTScopeNameNamespace ||
|
|
(gvk.Group == "" && gvk.Version == "v1" && gvk.Kind == "Namespace")
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|