From 45ad69765e8a6c11aa704a64dffd8b97b7b4f24f Mon Sep 17 00:00:00 2001 From: m1093782566 Date: Sun, 5 Nov 2017 19:23:29 +0800 Subject: [PATCH] wrapper ipset util --- pkg/util/BUILD | 1 + pkg/util/ipset/BUILD | 41 +++ pkg/util/ipset/ipset.go | 325 ++++++++++++++++++++++ pkg/util/ipset/ipset_test.go | 483 +++++++++++++++++++++++++++++++++ pkg/util/ipset/testing/BUILD | 23 ++ pkg/util/ipset/testing/fake.go | 83 ++++++ pkg/util/ipset/types.go | 70 +++++ 7 files changed, 1026 insertions(+) create mode 100644 pkg/util/ipset/BUILD create mode 100644 pkg/util/ipset/ipset.go create mode 100644 pkg/util/ipset/ipset_test.go create mode 100644 pkg/util/ipset/testing/BUILD create mode 100644 pkg/util/ipset/testing/fake.go create mode 100644 pkg/util/ipset/types.go diff --git a/pkg/util/BUILD b/pkg/util/BUILD index a206466463..d724c38842 100644 --- a/pkg/util/BUILD +++ b/pkg/util/BUILD @@ -27,6 +27,7 @@ filegroup( "//pkg/util/interrupt:all-srcs", "//pkg/util/io:all-srcs", "//pkg/util/ipconfig:all-srcs", + "//pkg/util/ipset:all-srcs", "//pkg/util/iptables:all-srcs", "//pkg/util/ipvs:all-srcs", "//pkg/util/keymutex:all-srcs", diff --git a/pkg/util/ipset/BUILD b/pkg/util/ipset/BUILD new file mode 100644 index 0000000000..20f172c010 --- /dev/null +++ b/pkg/util/ipset/BUILD @@ -0,0 +1,41 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "ipset.go", + "types.go", + ], + importpath = "k8s.io/kubernetes/pkg/util/ipset", + visibility = ["//visibility:public"], + deps = ["//vendor/k8s.io/utils/exec:go_default_library"], +) + +go_test( + name = "go_default_test", + srcs = ["ipset_test.go"], + importpath = "k8s.io/kubernetes/pkg/util/ipset", + library = ":go_default_library", + deps = [ + "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", + "//vendor/k8s.io/utils/exec:go_default_library", + "//vendor/k8s.io/utils/exec/testing:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [ + ":package-srcs", + "//pkg/util/ipset/testing:all-srcs", + ], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/pkg/util/ipset/ipset.go b/pkg/util/ipset/ipset.go new file mode 100644 index 0000000000..ba92bff74b --- /dev/null +++ b/pkg/util/ipset/ipset.go @@ -0,0 +1,325 @@ +/* +Copyright 2017 The Kubernetes 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 ipset + +import ( + "bytes" + "fmt" + "regexp" + "strconv" + "strings" + + utilexec "k8s.io/utils/exec" +) + +// Interface is an injectable interface for running ipset commands. Implementations must be goroutine-safe. +type Interface interface { + // FlushSet deletes all entries from a named set. + FlushSet(set string) error + // DestroySet deletes a named set. + DestroySet(set string) error + // DestroyAllSets deletes all sets. + DestroyAllSets() error + // CreateSet creates a new set, it will ignore error when the set already exists if ignoreExistErr=true. + CreateSet(set *IPSet, ignoreExistErr bool) error + // AddEntry adds a new entry to the named set. + AddEntry(entry string, set string, ignoreExistErr bool) error + // DelEntry deletes one entry from the named set + DelEntry(entry string, set string) error + // Test test if an entry exists in the named set + TestEntry(entry string, set string) (bool, error) + // ListEntries lists all the entries from a named set + ListEntries(set string) ([]string, error) + // ListSets list all set names from kernel + ListSets() ([]string, error) + // GetVersion returns the "X.Y" version string for ipset. + GetVersion() (string, error) +} + +// IPSetCmd represents the ipset util. We use ipset command for ipset execute. +const IPSetCmd = "ipset" + +// EntryMemberPattern is the regular expression pattern of ipset member list. +// The raw output of ipset command `ipset list {set}` is similar to, +//Name: foobar +//Type: hash:ip,port +//Revision: 2 +//Header: family inet hashsize 1024 maxelem 65536 +//Size in memory: 16592 +//References: 0 +//Members: +//192.168.1.2,tcp:8080 +//192.168.1.1,udp:53 +var EntryMemberPattern = "(?m)^(.*\n)*Members:\n" + +// VersionPattern is the regular expression pattern of ipset version string. +// ipset version output is similar to "v6.10". +var VersionPattern = "v[0-9]+\\.[0-9]+" + +// IPSet implements an Interface to an set. +type IPSet struct { + // Name is the set name. + Name string + // SetType specifies the ipset type. + SetType Type + // HashFamily specifies the protocol family of the IP addresses to be stored in the set. + // The default is inet, i.e IPv4. If users want to use IPv6, they should specify inet6. + HashFamily string + // HashSize specifies the hash table size of ipset. + HashSize int + // MaxElem specifies the max element number of ipset. + MaxElem int + // PortRange specifies the port range of bitmap:port type ipset. + PortRange string +} + +// Entry represents a ipset entry. +type Entry struct { + // IP is the entry's IP. The IP address protocol corresponds to the HashFamily of IPSet. + // All entries' IP addresses in the same ip set has same the protocol, IPv4 or IPv6. + IP string + // Port is the entry's Port. + Port int + // Protocol is the entry's Protocol. The protocols of entries in the same ip set are all + // the same. The accepted protocols are TCP and UDP. + Protocol string + // Net is the entry's IP network address. Network address with zero prefix size can NOT + // be stored. + Net string + // IP2 is the entry's second IP. IP2 may not be empty for `hash:ip,port,ip` type ip set. + IP2 string + // SetType specifies the type of ip set where the entry exists. + SetType Type +} + +func (e *Entry) String() string { + switch e.SetType { + case HashIPPort: + // Entry{192.168.1.1, udp, 53} -> 192.168.1.1,udp:53 + // Entry{192.168.1.2, tcp, 8080} -> 192.168.1.2,tcp:8080 + return fmt.Sprintf("%s,%s:%s", e.IP, e.Protocol, strconv.Itoa(e.Port)) + case HashIPPortIP: + // Entry{192.168.1.1, udp, 53, 10.0.0.1} -> 192.168.1.1,udp:53,10.0.0.1 + // Entry{192.168.1.2, tcp, 8080, 192.168.1.2} -> 192.168.1.2,tcp:8080,192.168.1.2 + return fmt.Sprintf("%s,%s:%s,%s", e.IP, e.Protocol, strconv.Itoa(e.Port), e.IP2) + case HashIPPortNet: + // Entry{192.168.1.2, udp, 80, 10.0.1.0/24} -> 192.168.1.2,udp:80,10.0.1.0/24 + // Entry{192.168.2,25, tcp, 8080, 10.1.0.0/16} -> 192.168.2,25,tcp:8080,10.1.0.0/16 + return fmt.Sprintf("%s,%s:%s,%s", e.IP, e.Protocol, strconv.Itoa(e.Port), e.Net) + case BitmapPort: + // Entry{53} -> 53 + // Entry{8080} -> 8080 + return strconv.Itoa(e.Port) + } + return "" +} + +type runner struct { + exec utilexec.Interface +} + +// New returns a new Interface which will exec ipset. +func New(exec utilexec.Interface) Interface { + return &runner{ + exec: exec, + } +} + +// CreateSet creates a new set, it will ignore error when the set already exists if ignoreExistErr=true. +func (runner *runner) CreateSet(set *IPSet, ignoreExistErr bool) error { + // Using default values. + if set.HashSize == 0 { + set.HashSize = 1024 + } + if set.MaxElem == 0 { + set.MaxElem = 65536 + } + if set.HashFamily == "" { + set.HashFamily = ProtocolFamilyIPV4 + } + if len(set.HashFamily) != 0 && set.HashFamily != ProtocolFamilyIPV4 && set.HashFamily != ProtocolFamilyIPV6 { + return fmt.Errorf("Currently supported protocol families are: %s and %s, %s is not supported", ProtocolFamilyIPV4, ProtocolFamilyIPV6, set.HashFamily) + } + // Default ipset type is "hash:ip,port" + if len(set.SetType) == 0 { + set.SetType = HashIPPort + } + // Check if setType is supported + if !IsValidIPSetType(set.SetType) { + return fmt.Errorf("Currently supported ipset types are: %v, %s is not supported", ValidIPSetTypes, set.SetType) + } + + return runner.createSet(set, ignoreExistErr) +} + +// If ignoreExistErr is set to true, then the -exist option of ipset will be specified, ipset ignores the error +// otherwise raised when the same set (setname and create parameters are identical) already exists. +func (runner *runner) createSet(set *IPSet, ignoreExistErr bool) error { + args := []string{"create", set.Name, string(set.SetType)} + if set.SetType == HashIPPortIP || set.SetType == HashIPPort { + args = append(args, + "family", set.HashFamily, + "hashsize", strconv.Itoa(set.HashSize), + "maxelem", strconv.Itoa(set.MaxElem), + ) + } + if set.SetType == BitmapPort { + if len(set.PortRange) == 0 { + set.PortRange = DefaultPortRange + } + if !validatePortRange(set.PortRange) { + return fmt.Errorf("invalid port range for %s type ip set: %s, expect: a-b", BitmapPort, set.PortRange) + } + args = append(args, "range", set.PortRange) + } + if ignoreExistErr { + args = append(args, "-exist") + } + if _, err := runner.exec.Command(IPSetCmd, args...).CombinedOutput(); err != nil { + return fmt.Errorf("error creating ipset %s, error: %v", set.Name, err) + } + return nil +} + +// AddEntry adds a new entry to the named set. +// If the -exist option is specified, ipset ignores the error otherwise raised when +// the same set (setname and create parameters are identical) already exists. +func (runner *runner) AddEntry(entry string, set string, ignoreExistErr bool) error { + args := []string{"add", set, entry} + if ignoreExistErr { + args = append(args, "-exist") + } + if _, err := runner.exec.Command(IPSetCmd, args...).CombinedOutput(); err != nil { + return fmt.Errorf("error adding entry %s, error: %v", entry, err) + } + return nil +} + +// DelEntry is used to delete the specified entry from the set. +func (runner *runner) DelEntry(entry string, set string) error { + if _, err := runner.exec.Command(IPSetCmd, "del", set, entry).CombinedOutput(); err != nil { + return fmt.Errorf("error deleting entry %s: from set: %s, error: %v", entry, set, err) + } + return nil +} + +// TestEntry is used to check whether the specified entry is in the set or not. +func (runner *runner) TestEntry(entry string, set string) (bool, error) { + if out, err := runner.exec.Command(IPSetCmd, "test", set, entry).CombinedOutput(); err == nil { + reg, e := regexp.Compile("NOT") + if e == nil && reg.MatchString(string(out)) { + return false, nil + } else if e == nil { + return true, nil + } else { + return false, fmt.Errorf("error testing entry: %s, error: %v", entry, e) + } + } else { + return false, fmt.Errorf("error testing entry %s: %v (%s)", entry, err, out) + } +} + +// FlushSet deletes all entries from a named set. +func (runner *runner) FlushSet(set string) error { + if _, err := runner.exec.Command(IPSetCmd, "flush", set).CombinedOutput(); err != nil { + return fmt.Errorf("error flushing set: %s, error: %v", set, err) + } + return nil +} + +// DestroySet is used to destroy a named set. +func (runner *runner) DestroySet(set string) error { + if _, err := runner.exec.Command(IPSetCmd, "destroy", set).CombinedOutput(); err != nil { + return fmt.Errorf("error destroying set %s:, error: %v", set, err) + } + return nil +} + +// DestroyAllSets is used to destroy all sets. +func (runner *runner) DestroyAllSets() error { + if _, err := runner.exec.Command(IPSetCmd, "destroy").CombinedOutput(); err != nil { + return fmt.Errorf("error destroying all sets, error: %v", err) + } + return nil +} + +// ListSets list all set names from kernel +func (runner *runner) ListSets() ([]string, error) { + out, err := runner.exec.Command(IPSetCmd, "list", "-n").CombinedOutput() + if err != nil { + return nil, fmt.Errorf("error listing all sets, error: %v", err) + } + return strings.Split(string(out), "\n"), nil +} + +// ListEntries lists all the entries from a named set. +func (runner *runner) ListEntries(set string) ([]string, error) { + if len(set) == 0 { + return nil, fmt.Errorf("set name can't be nil") + } + out, err := runner.exec.Command(IPSetCmd, "list", set).CombinedOutput() + if err != nil { + return nil, fmt.Errorf("error listing set: %s, error: %v", set, err) + } + memberMatcher := regexp.MustCompile(EntryMemberPattern) + list := memberMatcher.ReplaceAllString(string(out[:]), "") + strs := strings.Split(list, "\n") + results := make([]string, 0) + for i := range strs { + if len(strs[i]) > 0 { + results = append(results, strs[i]) + } + } + return results, nil +} + +// GetVersion returns the version string. +func (runner *runner) GetVersion() (string, error) { + return getIPSetVersionString(runner.exec) +} + +// getIPSetVersionString runs "ipset --version" to get the version string +// in the form of "X.Y", i.e "6.19" +func getIPSetVersionString(exec utilexec.Interface) (string, error) { + cmd := exec.Command(IPSetCmd, "--version") + cmd.SetStdin(bytes.NewReader([]byte{})) + bytes, err := cmd.CombinedOutput() + if err != nil { + return "", err + } + versionMatcher := regexp.MustCompile(VersionPattern) + match := versionMatcher.FindStringSubmatch(string(bytes)) + if match == nil { + return "", fmt.Errorf("no ipset version found in string: %s", bytes) + } + return match[0], nil +} + +func validatePortRange(portRange string) bool { + strs := strings.Split(portRange, "-") + if len(strs) != 2 { + return false + } + for i := range strs { + if _, err := strconv.Atoi(strs[i]); err != nil { + return false + } + } + return true +} + +var _ = Interface(&runner{}) diff --git a/pkg/util/ipset/ipset_test.go b/pkg/util/ipset/ipset_test.go new file mode 100644 index 0000000000..71d4ce26d5 --- /dev/null +++ b/pkg/util/ipset/ipset_test.go @@ -0,0 +1,483 @@ +/* +Copyright 2017 The Kubernetes 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 ipset + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/utils/exec" + fakeexec "k8s.io/utils/exec/testing" +) + +func TestCheckIPSetVersion(t *testing.T) { + testCases := []struct { + vstring string + Expect string + Err bool + }{ + {"ipset v4.0, protocol version: 4", "v4.0", false}, + {"ipset v5.1, protocol version: 5", "v5.1", false}, + {"ipset v6.0, protocol version: 6", "v6.0", false}, + {"ipset v6.1, protocol version: 6", "v6.1", false}, + {"ipset v6.19, protocol version: 6", "v6.19", false}, + {"total junk", "", true}, + } + + for i := range testCases { + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeCombinedOutputAction{ + // ipset version response + func() ([]byte, error) { return []byte(testCases[i].vstring), nil }, + }, + } + + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + }, + } + + gotVersion, err := getIPSetVersionString(&fexec) + if (err != nil) != testCases[i].Err { + t.Errorf("Expected error: %v, Got error: %v", testCases[i].Err, err) + } + if err == nil { + if testCases[i].Expect != gotVersion { + t.Errorf("Expected result: %v, Got result: %v", testCases[i].Expect, gotVersion) + } + } + } +} + +func TestFlushSet(t *testing.T) { + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeCombinedOutputAction{ + // Success + func() ([]byte, error) { return []byte{}, nil }, + // Success + func() ([]byte, error) { return []byte{}, nil }, + }, + } + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + }, + } + runner := New(&fexec) + // Success. + err := runner.FlushSet("FOOBAR") + if err != nil { + t.Errorf("expected success, got %v", err) + } + if fcmd.CombinedOutputCalls != 1 { + t.Errorf("expected 1 CombinedOutput() calls, got %d", fcmd.CombinedOutputCalls) + } + if !sets.NewString(fcmd.CombinedOutputLog[0]...).HasAll("ipset", "flush", "FOOBAR") { + t.Errorf("wrong CombinedOutput() log, got %s", fcmd.CombinedOutputLog[0]) + } + // Flush again + err = runner.FlushSet("FOOBAR") + if err != nil { + t.Errorf("expected success, got %v", err) + } +} + +func TestDestroySet(t *testing.T) { + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeCombinedOutputAction{ + // Success + func() ([]byte, error) { return []byte{}, nil }, + // Failure + func() ([]byte, error) { + return []byte("ipset v6.19: The set with the given name does not exist"), &fakeexec.FakeExitError{Status: 1} + }, + }, + } + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + }, + } + runner := New(&fexec) + // Success + err := runner.DestroySet("FOOBAR") + if err != nil { + t.Errorf("expected success, got %v", err) + } + if fcmd.CombinedOutputCalls != 1 { + t.Errorf("expected 1 CombinedOutput() calls, got %d", fcmd.CombinedOutputCalls) + } + if !sets.NewString(fcmd.CombinedOutputLog[0]...).HasAll("ipset", "destroy", "FOOBAR") { + t.Errorf("wrong CombinedOutput() log, got %s", fcmd.CombinedOutputLog[0]) + } + // Failure + err = runner.DestroySet("FOOBAR") + if err == nil { + t.Errorf("expected failure, got nil") + } +} + +func TestDestroyAllSets(t *testing.T) { + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeCombinedOutputAction{ + // Success + func() ([]byte, error) { return []byte{}, nil }, + // Success + func() ([]byte, error) { return []byte{}, nil }, + }, + } + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + }, + } + runner := New(&fexec) + // Success + err := runner.DestroyAllSets() + if err != nil { + t.Errorf("expected success, got %v", err) + } + if fcmd.CombinedOutputCalls != 1 { + t.Errorf("expected 1 CombinedOutput() calls, got %d", fcmd.CombinedOutputCalls) + } + if !sets.NewString(fcmd.CombinedOutputLog[0]...).HasAll("ipset", "destroy") { + t.Errorf("wrong CombinedOutput() log, got %s", fcmd.CombinedOutputLog[0]) + } + // Success + err = runner.DestroyAllSets() + if err != nil { + t.Errorf("Unexpected failure: %v", err) + } +} + +func TestCreateSet(t *testing.T) { + testSet := IPSet{ + Name: "FOOBAR", + SetType: HashIPPort, + HashFamily: ProtocolFamilyIPV4, + } + + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeCombinedOutputAction{ + // Success + func() ([]byte, error) { return []byte{}, nil }, + // Success + func() ([]byte, error) { return []byte{}, nil }, + // Failure + func() ([]byte, error) { + return []byte("ipset v6.19: Set cannot be created: set with the same name already exists"), &fakeexec.FakeExitError{Status: 1} + }, + }, + } + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + }, + } + runner := New(&fexec) + // Create with ignoreExistErr = false, expect success + err := runner.CreateSet(&testSet, false) + if err != nil { + t.Errorf("expected success, got %v", err) + } + if fcmd.CombinedOutputCalls != 1 { + t.Errorf("expected 1 CombinedOutput() calls, got %d", fcmd.CombinedOutputCalls) + } + if !sets.NewString(fcmd.CombinedOutputLog[0]...).HasAll("ipset", "create", "FOOBAR", "hash:ip,port", "family", "inet", "hashsize", "1024", "maxelem", "65536") { + t.Errorf("wrong CombinedOutput() log, got %s", fcmd.CombinedOutputLog[0]) + } + // Create with ignoreExistErr = true, expect success + err = runner.CreateSet(&testSet, true) + if err != nil { + t.Errorf("expected success, got %v", err) + } + if fcmd.CombinedOutputCalls != 2 { + t.Errorf("expected 2 CombinedOutput() calls, got %d", fcmd.CombinedOutputCalls) + } + if !sets.NewString(fcmd.CombinedOutputLog[1]...).HasAll("ipset", "create", "FOOBAR", "hash:ip,port", "family", "inet", "hashsize", "1024", "maxelem", "65536", "-exist") { + t.Errorf("wrong CombinedOutput() log, got %s", fcmd.CombinedOutputLog[1]) + } + // Create with ignoreExistErr = false, expect failure + err = runner.CreateSet(&testSet, false) + if err == nil { + t.Errorf("expected failure, got nil") + } +} + +func TestAddEntry(t *testing.T) { + testEntry := &Entry{ + IP: "192.168.1.1", + Port: 53, + Protocol: ProtocolUDP, + SetType: HashIPPort, + } + + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeCombinedOutputAction{ + // Success + func() ([]byte, error) { return []byte{}, nil }, + // Success + func() ([]byte, error) { return []byte{}, nil }, + // Failure + func() ([]byte, error) { + return []byte("ipset v6.19: Set cannot be created: set with the same name already exists"), &fakeexec.FakeExitError{Status: 1} + }, + }, + } + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + }, + } + runner := New(&fexec) + // Create with ignoreExistErr = false, expect success + err := runner.AddEntry(testEntry.String(), "FOOBAR", false) + if err != nil { + t.Errorf("expected success, got %v", err) + } + if fcmd.CombinedOutputCalls != 1 { + t.Errorf("expected 1 CombinedOutput() calls, got %d", fcmd.CombinedOutputCalls) + } + if !sets.NewString(fcmd.CombinedOutputLog[0]...).HasAll("ipset", "add", "FOOBAR", "192.168.1.1,udp:53") { + t.Errorf("wrong CombinedOutput() log, got %s", fcmd.CombinedOutputLog[0]) + } + // Create with ignoreExistErr = true, expect success + err = runner.AddEntry(testEntry.String(), "FOOBAR", true) + if err != nil { + t.Errorf("expected success, got %v", err) + } + if fcmd.CombinedOutputCalls != 2 { + t.Errorf("expected 3 CombinedOutput() calls, got %d", fcmd.CombinedOutputCalls) + } + if !sets.NewString(fcmd.CombinedOutputLog[1]...).HasAll("ipset", "add", "FOOBAR", "192.168.1.1,udp:53", "-exist") { + t.Errorf("wrong CombinedOutput() log, got %s", fcmd.CombinedOutputLog[1]) + } + // Create with ignoreExistErr = false, expect failure + err = runner.AddEntry(testEntry.String(), "FOOBAR", false) + if err == nil { + t.Errorf("expected failure, got nil") + } +} + +func TestDelEntry(t *testing.T) { + // TODO: Test more set type + testEntry := &Entry{ + IP: "192.168.1.1", + Port: 53, + Protocol: ProtocolUDP, + SetType: HashIPPort, + } + + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeCombinedOutputAction{ + // Success + func() ([]byte, error) { return []byte{}, nil }, + // Failure + func() ([]byte, error) { + return []byte("ipset v6.19: Element cannot be deleted from the set: it's not added"), &fakeexec.FakeExitError{Status: 1} + }, + }, + } + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + }, + } + runner := New(&fexec) + err := runner.DelEntry(testEntry.String(), "FOOBAR") + if err != nil { + t.Errorf("expected success, got %v", err) + } + if fcmd.CombinedOutputCalls != 1 { + t.Errorf("expected 1 CombinedOutput() calls, got %d", fcmd.CombinedOutputCalls) + } + if !sets.NewString(fcmd.CombinedOutputLog[0]...).HasAll("ipset", "del", "FOOBAR", "192.168.1.1,udp:53") { + t.Errorf("wrong CombinedOutput() log, got %s", fcmd.CombinedOutputLog[0]) + } + err = runner.DelEntry(testEntry.String(), "FOOBAR") + if err == nil { + t.Errorf("expected failure, got nil") + } +} + +func TestTestEntry(t *testing.T) { + // TODO: IPv6? + testEntry := &Entry{ + IP: "10.120.7.100", + Port: 8080, + Protocol: ProtocolTCP, + SetType: HashIPPort, + } + + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeCombinedOutputAction{ + // Success + func() ([]byte, error) { return []byte("10.120.7.100,tcp:8080 is in set FOOBAR."), nil }, + // Failure + func() ([]byte, error) { + return []byte("192.168.1.3,tcp:8080 is NOT in set FOOBAR."), &fakeexec.FakeExitError{Status: 1} + }, + }, + } + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + }, + } + runner := New(&fexec) + // Success + ok, err := runner.TestEntry(testEntry.String(), "FOOBAR") + if err != nil { + t.Errorf("expected success, got %v", err) + } + if fcmd.CombinedOutputCalls != 1 { + t.Errorf("expected 2 CombinedOutput() calls, got %d", fcmd.CombinedOutputCalls) + } + if !sets.NewString(fcmd.CombinedOutputLog[0]...).HasAll("ipset", "test", "FOOBAR", "10.120.7.100,tcp:8080") { + t.Errorf("wrong CombinedOutput() log, got %s", fcmd.CombinedOutputLog[0]) + } + if !ok { + t.Errorf("expect entry exists in test set, got not") + } + // Failure + ok, err = runner.TestEntry(testEntry.String(), "FOOBAR") + if err == nil || ok { + t.Errorf("expect entry doesn't exist in test set") + } +} + +func TestListEntries(t *testing.T) { + + output := `Name: foobar +Type: hash:ip,port +Revision: 2 +Header: family inet hashsize 1024 maxelem 65536 +Size in memory: 16592 +References: 0 +Members: +192.168.1.2,tcp:8080 +192.168.1.1,udp:53` + + emptyOutput := `Name: KUBE-NODE-PORT +Type: bitmap:port +Revision: 1 +Header: range 0-65535 +Size in memory: 524432 +References: 1 +Members: + +` + + testCases := []struct { + output string + expected []string + }{ + { + output: output, + expected: []string{"192.168.1.2,tcp:8080", "192.168.1.1,udp:53"}, + }, + { + output: emptyOutput, + expected: []string{}, + }, + } + + for i := range testCases { + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeCombinedOutputAction{ + // Success + func() ([]byte, error) { + return []byte(testCases[i].output), nil + }, + }, + } + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { + return fakeexec.InitFakeCmd(&fcmd, cmd, args...) + }, + }, + } + runner := New(&fexec) + // Success + entries, err := runner.ListEntries("foobar") + if err != nil { + t.Errorf("expected success, got: %v", err) + } + if fcmd.CombinedOutputCalls != 1 { + t.Errorf("expected 1 CombinedOutput() calls, got: %d", fcmd.CombinedOutputCalls) + } + if !sets.NewString(fcmd.CombinedOutputLog[0]...).HasAll("ipset", "list", "foobar") { + t.Errorf("wrong CombinedOutput() log, got: %s", fcmd.CombinedOutputLog[0]) + } + if len(entries) != len(testCases[i].expected) { + t.Errorf("expected %d ipset entries, got: %d", len(testCases[i].expected), len(entries)) + } + if !reflect.DeepEqual(entries, testCases[i].expected) { + t.Errorf("expected entries: %v, got: %v", testCases[i].expected, entries) + } + } +} + +func TestListSets(t *testing.T) { + output := `foo +bar +baz` + + expected := []string{"foo", "bar", "baz"} + + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeCombinedOutputAction{ + // Success + func() ([]byte, error) { return []byte(output), nil }, + }, + } + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + }, + } + runner := New(&fexec) + // Success + list, err := runner.ListSets() + if err != nil { + t.Errorf("expected success, got: %v", err) + } + if fcmd.CombinedOutputCalls != 1 { + t.Errorf("expected 1 CombinedOutput() calls, got: %d", fcmd.CombinedOutputCalls) + } + if !sets.NewString(fcmd.CombinedOutputLog[0]...).HasAll("ipset", "list", "-n") { + t.Errorf("wrong CombinedOutput() log, got: %s", fcmd.CombinedOutputLog[0]) + } + if len(list) != len(expected) { + t.Errorf("expected %d sets, got: %d", len(expected), len(list)) + } + if !reflect.DeepEqual(list, expected) { + t.Errorf("expected sets: %v, got: %v", expected, list) + } +} diff --git a/pkg/util/ipset/testing/BUILD b/pkg/util/ipset/testing/BUILD new file mode 100644 index 0000000000..593b04157c --- /dev/null +++ b/pkg/util/ipset/testing/BUILD @@ -0,0 +1,23 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["fake.go"], + importpath = "k8s.io/kubernetes/pkg/util/ipset/testing", + visibility = ["//visibility:public"], + deps = ["//pkg/util/ipset:go_default_library"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/pkg/util/ipset/testing/fake.go b/pkg/util/ipset/testing/fake.go new file mode 100644 index 0000000000..aedf3d21b3 --- /dev/null +++ b/pkg/util/ipset/testing/fake.go @@ -0,0 +1,83 @@ +/* +Copyright 2017 The Kubernetes 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 testing + +import ( + "k8s.io/kubernetes/pkg/util/ipset" +) + +// FakeIPSet is a no-op implementation of ipset Interface +type FakeIPSet struct { + Lines []byte +} + +// NewFake create a new fake ipset interface. +func NewFake() *FakeIPSet { + return &FakeIPSet{} +} + +// GetVersion is part of interface. +func (*FakeIPSet) GetVersion() (string, error) { + return "0.0", nil +} + +// FlushSet is part of interface. +func (*FakeIPSet) FlushSet(set string) error { + return nil +} + +// DestroySet is part of interface. +func (*FakeIPSet) DestroySet(set string) error { + return nil +} + +// DestroyAllSets is part of interface. +func (*FakeIPSet) DestroyAllSets() error { + return nil +} + +// CreateSet is part of interface. +func (*FakeIPSet) CreateSet(set *ipset.IPSet, ignoreExistErr bool) error { + return nil +} + +// AddEntry is part of interface. +func (*FakeIPSet) AddEntry(entry string, set string, ignoreExistErr bool) error { + return nil +} + +// DelEntry is part of interface. +func (*FakeIPSet) DelEntry(entry string, set string) error { + return nil +} + +// TestEntry is part of interface. +func (*FakeIPSet) TestEntry(entry string, set string) (bool, error) { + return true, nil +} + +// ListEntries is part of interface. +func (*FakeIPSet) ListEntries(set string) ([]string, error) { + return nil, nil +} + +// ListSets is part of interface. +func (*FakeIPSet) ListSets() ([]string, error) { + return nil, nil +} + +var _ = ipset.Interface(&FakeIPSet{}) diff --git a/pkg/util/ipset/types.go b/pkg/util/ipset/types.go new file mode 100644 index 0000000000..d2406c0087 --- /dev/null +++ b/pkg/util/ipset/types.go @@ -0,0 +1,70 @@ +/* +Copyright 2017 The Kubernetes 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 ipset + +// Type represents the ipset type +type Type string + +const ( + // HashIPPort represents the `hash:ip,port` type ipset. The hash:ip,port is similar to hash:ip but + // you can store IP address and protocol-port pairs in it. TCP, SCTP, UDP, UDPLITE, ICMP and ICMPv6 are supported + // with port numbers/ICMP(v6) types and other protocol numbers without port information. + HashIPPort Type = "hash:ip,port" + // HashIPPortIP represents the `hash:ip,port,ip` type ipset. The hash:ip,port,ip set type uses a hash to store + // IP address, port number and a second IP address triples. The port number is interpreted together with a + // protocol (default TCP) and zero protocol number cannot be used. + HashIPPortIP Type = "hash:ip,port,ip" + // HashIPPortNet represents the `hash:ip,port,net` type ipset. The hash:ip,port,net set type uses a hash to store IP address, port number and IP network address triples. The port + // number is interpreted together with a protocol (default TCP) and zero protocol number cannot be used. Network address + // with zero prefix size cannot be stored either. + HashIPPortNet Type = "hash:ip,port,net" + // BitmapPort represents the `bitmap:port` type ipset. The bitmap:port set type uses a memory range, where each bit + // represents one TCP/UDP port. A bitmap:port type of set can store up to 65535 ports. + BitmapPort Type = "bitmap:port" +) + +// DefaultPortRange defines the default bitmap:port valid port range. +const DefaultPortRange string = "0-65535" + +const ( + // ProtocolFamilyIPV4 represents IPv4 protocol. + ProtocolFamilyIPV4 = "inet" + // ProtocolFamilyIPV6 represents IPv6 protocol. + ProtocolFamilyIPV6 = "inet6" + // ProtocolTCP represents TCP protocol. + ProtocolTCP = "tcp" + // ProtocolUDP represents UDP protocol. + ProtocolUDP = "udp" +) + +// ValidIPSetTypes defines the supported ip set type. +var ValidIPSetTypes = []Type{ + HashIPPort, + HashIPPortIP, + BitmapPort, + HashIPPortNet, +} + +// IsValidIPSetType checks if the given ipset type is valid. +func IsValidIPSetType(set Type) bool { + for _, valid := range ValidIPSetTypes { + if set == valid { + return true + } + } + return false +}