| ... | ... |
@@ -370,6 +370,11 @@ type MasterNetworkConfig struct {
|
| 370 | 370 |
ClusterNetworkCIDR string |
| 371 | 371 |
HostSubnetLength uint |
| 372 | 372 |
ServiceNetworkCIDR string |
| 373 |
+ // ExternalIPNetworkCIDRs controls what values are acceptable for the service external IP field. If empty, no externalIP |
|
| 374 |
+ // may be set. It may contain a list of CIDRs which are checked for access. If a CIDR is prefixed with !, IPs in that |
|
| 375 |
+ // CIDR will be rejected. Rejections will be applied first, then the IP checked against one of the allowed CIDRs. You |
|
| 376 |
+ // should ensure this range does not overlap with your nodes, pods, or service CIDRs for security reasons. |
|
| 377 |
+ ExternalIPNetworkCIDRs []string `json:"externalIPNetworkCIDRs"` |
|
| 373 | 378 |
} |
| 374 | 379 |
|
| 375 | 380 |
type ImageConfig struct {
|
| ... | ... |
@@ -406,11 +406,12 @@ func (MasterConfig) SwaggerDoc() map[string]string {
|
| 406 | 406 |
} |
| 407 | 407 |
|
| 408 | 408 |
var map_MasterNetworkConfig = map[string]string{
|
| 409 |
- "": "MasterNetworkConfig to be passed to the compiled in network plugin", |
|
| 410 |
- "networkPluginName": "NetworkPluginName is the name of the network plugin to use", |
|
| 411 |
- "clusterNetworkCIDR": "ClusterNetworkCIDR is the CIDR string to specify the global overlay network's L3 space", |
|
| 412 |
- "hostSubnetLength": "HostSubnetLength is the number of bits to allocate to each host's subnet e.g. 8 would mean a /24 network on the host", |
|
| 413 |
- "serviceNetworkCIDR": "ServiceNetwork is the CIDR string to specify the service networks", |
|
| 409 |
+ "": "MasterNetworkConfig to be passed to the compiled in network plugin", |
|
| 410 |
+ "networkPluginName": "NetworkPluginName is the name of the network plugin to use", |
|
| 411 |
+ "clusterNetworkCIDR": "ClusterNetworkCIDR is the CIDR string to specify the global overlay network's L3 space", |
|
| 412 |
+ "hostSubnetLength": "HostSubnetLength is the number of bits to allocate to each host's subnet e.g. 8 would mean a /24 network on the host", |
|
| 413 |
+ "serviceNetworkCIDR": "ServiceNetwork is the CIDR string to specify the service networks", |
|
| 414 |
+ "externalIPNetworkCIDRs": "ExternalIPNetworkCIDRs controls what values are acceptable for the service external IP field. If empty, no externalIP may be set. It may contain a list of CIDRs which are checked for access. If a CIDR is prefixed with !, IPs in that CIDR will be rejected. Rejections will be applied first, then the IP checked against one of the allowed CIDRs. You should ensure this range does not overlap with your nodes, pods, or service CIDRs for security reasons.", |
|
| 414 | 415 |
} |
| 415 | 416 |
|
| 416 | 417 |
func (MasterNetworkConfig) SwaggerDoc() map[string]string {
|
| ... | ... |
@@ -330,6 +330,11 @@ type MasterNetworkConfig struct {
|
| 330 | 330 |
HostSubnetLength uint `json:"hostSubnetLength"` |
| 331 | 331 |
// ServiceNetwork is the CIDR string to specify the service networks |
| 332 | 332 |
ServiceNetworkCIDR string `json:"serviceNetworkCIDR"` |
| 333 |
+ // ExternalIPNetworkCIDRs controls what values are acceptable for the service external IP field. If empty, no externalIP |
|
| 334 |
+ // may be set. It may contain a list of CIDRs which are checked for access. If a CIDR is prefixed with !, IPs in that |
|
| 335 |
+ // CIDR will be rejected. Rejections will be applied first, then the IP checked against one of the allowed CIDRs. You |
|
| 336 |
+ // should ensure this range does not overlap with your nodes, pods, or service CIDRs for security reasons. |
|
| 337 |
+ ExternalIPNetworkCIDRs []string `json:"externalIPNetworkCIDRs"` |
|
| 333 | 338 |
} |
| 334 | 339 |
|
| 335 | 340 |
// ImageConfig holds the necessary configuration options for building image names for system components |
| ... | ... |
@@ -147,6 +147,16 @@ func ValidateMasterConfig(config *api.MasterConfig, fldPath *field.Path) Validat |
| 147 | 147 |
validationResults.AddErrors(field.Invalid(fldPath.Child("networkConfig", "serviceNetworkCIDR"), config.NetworkConfig.ServiceNetworkCIDR, fmt.Sprintf("must match kubernetesMasterConfig.servicesSubnet value of %q", config.KubernetesMasterConfig.ServicesSubnet)))
|
| 148 | 148 |
} |
| 149 | 149 |
} |
| 150 |
+ if len(config.NetworkConfig.ExternalIPNetworkCIDRs) > 0 {
|
|
| 151 |
+ for i, s := range config.NetworkConfig.ExternalIPNetworkCIDRs {
|
|
| 152 |
+ if strings.HasPrefix(s, "!") {
|
|
| 153 |
+ s = s[1:] |
|
| 154 |
+ } |
|
| 155 |
+ if _, _, err := net.ParseCIDR(s); err != nil {
|
|
| 156 |
+ validationResults.AddErrors(field.Invalid(fldPath.Child("networkConfig", "externalIPNetworkCIDRs").Index(i), config.NetworkConfig.ExternalIPNetworkCIDRs[i], "must be a valid CIDR notation IP range (e.g. 172.30.0.0/16) with an optional leading !"))
|
|
| 157 |
+ } |
|
| 158 |
+ } |
|
| 159 |
+ } |
|
| 150 | 160 |
|
| 151 | 161 |
validationResults.AddErrors(ValidateKubeConfig(config.MasterClients.OpenShiftLoopbackKubeConfig, fldPath.Child("masterClients", "openShiftLoopbackKubeConfig"))...)
|
| 152 | 162 |
|
| ... | ... |
@@ -41,10 +41,11 @@ import ( |
| 41 | 41 |
cmdflags "github.com/openshift/origin/pkg/cmd/util/flags" |
| 42 | 42 |
"github.com/openshift/origin/pkg/cmd/util/pluginconfig" |
| 43 | 43 |
overrideapi "github.com/openshift/origin/pkg/quota/admission/clusterresourceoverride/api" |
| 44 |
+ serviceadmit "github.com/openshift/origin/pkg/service/admission" |
|
| 44 | 45 |
) |
| 45 | 46 |
|
| 46 | 47 |
// AdmissionPlugins is the full list of admission control plugins to enable in the order they must run |
| 47 |
-var AdmissionPlugins = []string{"NamespaceLifecycle", "PodNodeConstraints", "OriginPodNodeEnvironment", overrideapi.PluginName, "LimitRanger", "ServiceAccount", "SecurityContextConstraint", "BuildDefaults", "BuildOverrides", "ResourceQuota", "SCCExecRestrictions"}
|
|
| 48 |
+var AdmissionPlugins = []string{"NamespaceLifecycle", "PodNodeConstraints", "OriginPodNodeEnvironment", overrideapi.PluginName, serviceadmit.ExternalIPPluginName, "LimitRanger", "ServiceAccount", "SecurityContextConstraint", "BuildDefaults", "BuildOverrides", "ResourceQuota", "SCCExecRestrictions"}
|
|
| 48 | 49 |
|
| 49 | 50 |
// MasterConfig defines the required values to start a Kubernetes master |
| 50 | 51 |
type MasterConfig struct {
|
| ... | ... |
@@ -132,6 +133,14 @@ func BuildKubernetesMasterConfig(options configapi.MasterConfig, requestContextM |
| 132 | 132 |
plugins := []admission.Interface{}
|
| 133 | 133 |
for _, pluginName := range strings.Split(server.AdmissionControl, ",") {
|
| 134 | 134 |
switch pluginName {
|
| 135 |
+ case serviceadmit.ExternalIPPluginName: |
|
| 136 |
+ // this needs to be moved upstream to be part of core config |
|
| 137 |
+ reject, admit, err := serviceadmit.ParseCIDRRules(options.NetworkConfig.ExternalIPNetworkCIDRs) |
|
| 138 |
+ if err != nil {
|
|
| 139 |
+ // should have been caught with validation |
|
| 140 |
+ return nil, err |
|
| 141 |
+ } |
|
| 142 |
+ plugins = append(plugins, serviceadmit.NewExternalIPRanger(reject, admit)) |
|
| 135 | 143 |
case saadmit.PluginName: |
| 136 | 144 |
// we need to set some custom parameters on the service account admission controller, so create that one by hand |
| 137 | 145 |
saAdmitter := saadmit.NewServiceAccount(internalclientset.FromUnversionedClient(kubeClient)) |
| 138 | 146 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,110 @@ |
| 0 |
+package admission |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "io" |
|
| 4 |
+ "net" |
|
| 5 |
+ "strings" |
|
| 6 |
+ |
|
| 7 |
+ kadmission "k8s.io/kubernetes/pkg/admission" |
|
| 8 |
+ kapi "k8s.io/kubernetes/pkg/api" |
|
| 9 |
+ apierrs "k8s.io/kubernetes/pkg/api/errors" |
|
| 10 |
+ clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" |
|
| 11 |
+ "k8s.io/kubernetes/pkg/util/validation/field" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+const ExternalIPPluginName = "ExternalIPRanger" |
|
| 15 |
+ |
|
| 16 |
+func init() {
|
|
| 17 |
+ kadmission.RegisterPlugin("ExternalIPRanger", func(client clientset.Interface, config io.Reader) (kadmission.Interface, error) {
|
|
| 18 |
+ return NewExternalIPRanger(nil, nil), nil |
|
| 19 |
+ }) |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+type externalIPRanger struct {
|
|
| 23 |
+ *kadmission.Handler |
|
| 24 |
+ reject []*net.IPNet |
|
| 25 |
+ admit []*net.IPNet |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+var _ kadmission.Interface = &externalIPRanger{}
|
|
| 29 |
+ |
|
| 30 |
+// ParseCIDRRules calculates a blacklist and whitelist from a list of string CIDR rules (treating |
|
| 31 |
+// a leading ! as a negation). Returns an error if any rule is invalid. |
|
| 32 |
+func ParseCIDRRules(rules []string) (reject, admit []*net.IPNet, err error) {
|
|
| 33 |
+ for _, s := range rules {
|
|
| 34 |
+ negate := false |
|
| 35 |
+ if strings.HasPrefix(s, "!") {
|
|
| 36 |
+ negate = true |
|
| 37 |
+ s = s[1:] |
|
| 38 |
+ } |
|
| 39 |
+ _, cidr, err := net.ParseCIDR(s) |
|
| 40 |
+ if err != nil {
|
|
| 41 |
+ return nil, nil, err |
|
| 42 |
+ } |
|
| 43 |
+ if negate {
|
|
| 44 |
+ reject = append(reject, cidr) |
|
| 45 |
+ } else {
|
|
| 46 |
+ admit = append(admit, cidr) |
|
| 47 |
+ } |
|
| 48 |
+ } |
|
| 49 |
+ return reject, admit, nil |
|
| 50 |
+} |
|
| 51 |
+ |
|
| 52 |
+// NewConstraint creates a new SCC constraint admission plugin. |
|
| 53 |
+func NewExternalIPRanger(reject, admit []*net.IPNet) *externalIPRanger {
|
|
| 54 |
+ return &externalIPRanger{
|
|
| 55 |
+ Handler: kadmission.NewHandler(kadmission.Create, kadmission.Update), |
|
| 56 |
+ reject: reject, |
|
| 57 |
+ admit: admit, |
|
| 58 |
+ } |
|
| 59 |
+} |
|
| 60 |
+ |
|
| 61 |
+// networkSlice is a helper for checking whether an IP is contained in a range |
|
| 62 |
+// of networks. |
|
| 63 |
+type networkSlice []*net.IPNet |
|
| 64 |
+ |
|
| 65 |
+func (s networkSlice) Contains(ip net.IP) bool {
|
|
| 66 |
+ for _, cidr := range s {
|
|
| 67 |
+ if cidr.Contains(ip) {
|
|
| 68 |
+ return true |
|
| 69 |
+ } |
|
| 70 |
+ } |
|
| 71 |
+ return false |
|
| 72 |
+} |
|
| 73 |
+ |
|
| 74 |
+// Admit determines if the service should be admitted based on the configured network CIDR. |
|
| 75 |
+func (r *externalIPRanger) Admit(a kadmission.Attributes) error {
|
|
| 76 |
+ if a.GetResource() != kapi.Resource("services") {
|
|
| 77 |
+ return nil |
|
| 78 |
+ } |
|
| 79 |
+ |
|
| 80 |
+ svc, ok := a.GetObject().(*kapi.Service) |
|
| 81 |
+ // if we can't convert then we don't handle this object so just return |
|
| 82 |
+ if !ok {
|
|
| 83 |
+ return nil |
|
| 84 |
+ } |
|
| 85 |
+ |
|
| 86 |
+ var errs field.ErrorList |
|
| 87 |
+ switch {
|
|
| 88 |
+ // administrator disabled externalIPs |
|
| 89 |
+ case len(svc.Spec.ExternalIPs) > 0 && len(r.admit) == 0: |
|
| 90 |
+ errs = append(errs, field.Forbidden(field.NewPath("spec", "externalIPs"), "externalIPs have been disabled"))
|
|
| 91 |
+ // administrator has limited the range |
|
| 92 |
+ case len(svc.Spec.ExternalIPs) > 0 && len(r.admit) > 0: |
|
| 93 |
+ for i, s := range svc.Spec.ExternalIPs {
|
|
| 94 |
+ ip := net.ParseIP(s) |
|
| 95 |
+ if ip == nil {
|
|
| 96 |
+ errs = append(errs, field.Forbidden(field.NewPath("spec", "externalIPs").Index(i), "externalIPs must be a valid address"))
|
|
| 97 |
+ continue |
|
| 98 |
+ } |
|
| 99 |
+ if networkSlice(r.reject).Contains(ip) || !networkSlice(r.admit).Contains(ip) {
|
|
| 100 |
+ errs = append(errs, field.Forbidden(field.NewPath("spec", "externalIPs").Index(i), "externalIP is not allowed"))
|
|
| 101 |
+ continue |
|
| 102 |
+ } |
|
| 103 |
+ } |
|
| 104 |
+ } |
|
| 105 |
+ if len(errs) > 0 {
|
|
| 106 |
+ return apierrs.NewInvalid(a.GetKind(), a.GetName(), errs) |
|
| 107 |
+ } |
|
| 108 |
+ return nil |
|
| 109 |
+} |
| 0 | 110 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,188 @@ |
| 0 |
+package admission |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "net" |
|
| 4 |
+ "strings" |
|
| 5 |
+ "testing" |
|
| 6 |
+ |
|
| 7 |
+ "k8s.io/kubernetes/pkg/admission" |
|
| 8 |
+ kapi "k8s.io/kubernetes/pkg/api" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// TestAdmission verifies various scenarios involving pod/project/global node label selectors |
|
| 12 |
+func TestAdmission(t *testing.T) {
|
|
| 13 |
+ svc := &kapi.Service{
|
|
| 14 |
+ ObjectMeta: kapi.ObjectMeta{Name: "test"},
|
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ _, ipv4, err := net.ParseCIDR("172.0.0.0/16")
|
|
| 18 |
+ if err != nil {
|
|
| 19 |
+ t.Fatal(err) |
|
| 20 |
+ } |
|
| 21 |
+ _, ipv4subset, err := net.ParseCIDR("172.0.1.0/24")
|
|
| 22 |
+ if err != nil {
|
|
| 23 |
+ t.Fatal(err) |
|
| 24 |
+ } |
|
| 25 |
+ _, ipv4offset, err := net.ParseCIDR("172.200.0.0/24")
|
|
| 26 |
+ if err != nil {
|
|
| 27 |
+ t.Fatal(err) |
|
| 28 |
+ } |
|
| 29 |
+ _, none, err := net.ParseCIDR("0.0.0.0/32")
|
|
| 30 |
+ if err != nil {
|
|
| 31 |
+ t.Fatal(err) |
|
| 32 |
+ } |
|
| 33 |
+ _, all, err := net.ParseCIDR("0.0.0.0/0")
|
|
| 34 |
+ if err != nil {
|
|
| 35 |
+ t.Fatal(err) |
|
| 36 |
+ } |
|
| 37 |
+ |
|
| 38 |
+ tests := []struct {
|
|
| 39 |
+ testName string |
|
| 40 |
+ rejects, admits []*net.IPNet |
|
| 41 |
+ op admission.Operation |
|
| 42 |
+ externalIPs []string |
|
| 43 |
+ admit bool |
|
| 44 |
+ errFn func(err error) bool |
|
| 45 |
+ }{
|
|
| 46 |
+ {
|
|
| 47 |
+ admit: true, |
|
| 48 |
+ op: admission.Create, |
|
| 49 |
+ testName: "No external IPs on create", |
|
| 50 |
+ }, |
|
| 51 |
+ {
|
|
| 52 |
+ admit: true, |
|
| 53 |
+ op: admission.Update, |
|
| 54 |
+ testName: "No external IPs on update", |
|
| 55 |
+ }, |
|
| 56 |
+ {
|
|
| 57 |
+ admit: false, |
|
| 58 |
+ externalIPs: []string{"1.2.3.4"},
|
|
| 59 |
+ op: admission.Create, |
|
| 60 |
+ testName: "No external IPs allowed on create", |
|
| 61 |
+ errFn: func(err error) bool { return strings.Contains(err.Error(), "externalIPs have been disabled") },
|
|
| 62 |
+ }, |
|
| 63 |
+ {
|
|
| 64 |
+ admit: false, |
|
| 65 |
+ externalIPs: []string{"1.2.3.4"},
|
|
| 66 |
+ op: admission.Update, |
|
| 67 |
+ testName: "No external IPs allowed on update", |
|
| 68 |
+ errFn: func(err error) bool { return strings.Contains(err.Error(), "externalIPs have been disabled") },
|
|
| 69 |
+ }, |
|
| 70 |
+ {
|
|
| 71 |
+ admit: false, |
|
| 72 |
+ admits: []*net.IPNet{ipv4},
|
|
| 73 |
+ externalIPs: []string{"1.2.3.4"},
|
|
| 74 |
+ op: admission.Create, |
|
| 75 |
+ testName: "IP out of range on create", |
|
| 76 |
+ errFn: func(err error) bool {
|
|
| 77 |
+ return strings.Contains(err.Error(), "externalIP is not allowed") && |
|
| 78 |
+ strings.Contains(err.Error(), "spec.externalIPs[0]") |
|
| 79 |
+ }, |
|
| 80 |
+ }, |
|
| 81 |
+ {
|
|
| 82 |
+ admit: false, |
|
| 83 |
+ admits: []*net.IPNet{ipv4},
|
|
| 84 |
+ externalIPs: []string{"1.2.3.4"},
|
|
| 85 |
+ op: admission.Update, |
|
| 86 |
+ testName: "IP out of range on update", |
|
| 87 |
+ errFn: func(err error) bool {
|
|
| 88 |
+ return strings.Contains(err.Error(), "externalIP is not allowed") && |
|
| 89 |
+ strings.Contains(err.Error(), "spec.externalIPs[0]") |
|
| 90 |
+ }, |
|
| 91 |
+ }, |
|
| 92 |
+ {
|
|
| 93 |
+ admit: false, |
|
| 94 |
+ admits: []*net.IPNet{ipv4},
|
|
| 95 |
+ rejects: []*net.IPNet{ipv4subset},
|
|
| 96 |
+ externalIPs: []string{"172.0.1.1"},
|
|
| 97 |
+ op: admission.Update, |
|
| 98 |
+ testName: "IP out of range due to blacklist", |
|
| 99 |
+ errFn: func(err error) bool {
|
|
| 100 |
+ return strings.Contains(err.Error(), "externalIP is not allowed") && |
|
| 101 |
+ strings.Contains(err.Error(), "spec.externalIPs[0]") |
|
| 102 |
+ }, |
|
| 103 |
+ }, |
|
| 104 |
+ {
|
|
| 105 |
+ admit: false, |
|
| 106 |
+ admits: []*net.IPNet{ipv4},
|
|
| 107 |
+ rejects: []*net.IPNet{ipv4offset},
|
|
| 108 |
+ externalIPs: []string{"172.199.1.1"},
|
|
| 109 |
+ op: admission.Update, |
|
| 110 |
+ testName: "IP not in reject or admit", |
|
| 111 |
+ errFn: func(err error) bool {
|
|
| 112 |
+ return strings.Contains(err.Error(), "externalIP is not allowed") && |
|
| 113 |
+ strings.Contains(err.Error(), "spec.externalIPs[0]") |
|
| 114 |
+ }, |
|
| 115 |
+ }, |
|
| 116 |
+ {
|
|
| 117 |
+ admit: true, |
|
| 118 |
+ admits: []*net.IPNet{ipv4},
|
|
| 119 |
+ externalIPs: []string{"172.0.0.1"},
|
|
| 120 |
+ op: admission.Create, |
|
| 121 |
+ testName: "IP in range on create", |
|
| 122 |
+ }, |
|
| 123 |
+ {
|
|
| 124 |
+ admit: true, |
|
| 125 |
+ admits: []*net.IPNet{ipv4},
|
|
| 126 |
+ externalIPs: []string{"172.0.0.1"},
|
|
| 127 |
+ op: admission.Update, |
|
| 128 |
+ testName: "IP in range on update", |
|
| 129 |
+ }, |
|
| 130 |
+ |
|
| 131 |
+ // other checks |
|
| 132 |
+ {
|
|
| 133 |
+ admit: false, |
|
| 134 |
+ admits: []*net.IPNet{ipv4},
|
|
| 135 |
+ externalIPs: []string{"abcd"},
|
|
| 136 |
+ op: admission.Create, |
|
| 137 |
+ testName: "IP unparseable on create", |
|
| 138 |
+ errFn: func(err error) bool {
|
|
| 139 |
+ return strings.Contains(err.Error(), "externalIPs must be a valid address") && |
|
| 140 |
+ strings.Contains(err.Error(), "spec.externalIPs[0]") |
|
| 141 |
+ }, |
|
| 142 |
+ }, |
|
| 143 |
+ {
|
|
| 144 |
+ admit: false, |
|
| 145 |
+ admits: []*net.IPNet{none},
|
|
| 146 |
+ externalIPs: []string{"1.2.3.4"},
|
|
| 147 |
+ op: admission.Create, |
|
| 148 |
+ testName: "IP range is empty", |
|
| 149 |
+ }, |
|
| 150 |
+ {
|
|
| 151 |
+ admit: false, |
|
| 152 |
+ rejects: []*net.IPNet{all},
|
|
| 153 |
+ admits: []*net.IPNet{all},
|
|
| 154 |
+ externalIPs: []string{"1.2.3.4"},
|
|
| 155 |
+ op: admission.Create, |
|
| 156 |
+ testName: "rejections can cover the entire range", |
|
| 157 |
+ }, |
|
| 158 |
+ } |
|
| 159 |
+ for _, test := range tests {
|
|
| 160 |
+ svc.Spec.ExternalIPs = test.externalIPs |
|
| 161 |
+ handler := NewExternalIPRanger(test.rejects, test.admits) |
|
| 162 |
+ |
|
| 163 |
+ err := handler.Admit(admission.NewAttributesRecord(svc, kapi.Kind("Service"), "namespace", svc.ObjectMeta.Name, kapi.Resource("services"), "", test.op, nil))
|
|
| 164 |
+ if test.admit && err != nil {
|
|
| 165 |
+ t.Errorf("%s: expected no error but got: %s", test.testName, err)
|
|
| 166 |
+ } else if !test.admit && err == nil {
|
|
| 167 |
+ t.Errorf("%s: expected an error", test.testName)
|
|
| 168 |
+ } |
|
| 169 |
+ if test.errFn != nil && !test.errFn(err) {
|
|
| 170 |
+ t.Errorf("%s: unexpected error: %v", test.testName, err)
|
|
| 171 |
+ } |
|
| 172 |
+ } |
|
| 173 |
+} |
|
| 174 |
+ |
|
| 175 |
+func TestHandles(t *testing.T) {
|
|
| 176 |
+ for op, shouldHandle := range map[admission.Operation]bool{
|
|
| 177 |
+ admission.Create: true, |
|
| 178 |
+ admission.Update: true, |
|
| 179 |
+ admission.Connect: false, |
|
| 180 |
+ admission.Delete: false, |
|
| 181 |
+ } {
|
|
| 182 |
+ ranger := NewExternalIPRanger(nil, nil) |
|
| 183 |
+ if e, a := shouldHandle, ranger.Handles(op); e != a {
|
|
| 184 |
+ t.Errorf("%v: shouldHandle=%t, handles=%t", op, e, a)
|
|
| 185 |
+ } |
|
| 186 |
+ } |
|
| 187 |
+} |