From 1f950f63f0e4bda31e0c555b0f8a9e8c7485e0d8 Mon Sep 17 00:00:00 2001 From: mozhuli <21621232@zju.edu.cn> Date: Tue, 22 Aug 2017 21:33:47 +0800 Subject: [PATCH] Add fake files including * add fake openstack client framework. * add fake iptables. * add proxier unit tests. Change-Id: I8e47ecd33a103ac736e0619e35cfe59cca8ef2e2 Implements: blueprint enhance-unit-testing Signed-off-by: mozhuli <21621232@zju.edu.cn> --- cmd/kubestack/kubestack.go | 2 +- .../network_controller_helper.go | 6 +- pkg/openstack/client.go | 38 +- pkg/openstack/openstack_fake.go | 400 +++++++++++ pkg/proxy/iptables_fake.go | 99 +++ pkg/proxy/proxier.go | 2 +- pkg/proxy/proxier_test.go | 637 ++++++++++++++++++ pkg/service-controller/service_controller.go | 2 +- 8 files changed, 1164 insertions(+), 22 deletions(-) create mode 100644 pkg/openstack/openstack_fake.go create mode 100644 pkg/proxy/iptables_fake.go create mode 100644 pkg/proxy/proxier_test.go diff --git a/cmd/kubestack/kubestack.go b/cmd/kubestack/kubestack.go index 73e7847..b563a78 100644 --- a/cmd/kubestack/kubestack.go +++ b/cmd/kubestack/kubestack.go @@ -75,7 +75,7 @@ func (os *OpenStack) getNetworkIDByNamespace(namespace string) (string, error) { // Only support one network and network's name is same with namespace. // TODO: make it general after multi-network is supported. networkName := util.BuildNetworkName(namespace, namespace) - network, err := os.Client.GetNetwork(networkName) + network, err := os.Client.GetNetworkByName(networkName) if err != nil { glog.Errorf("Get network by name %q failed: %v", networkName, err) return "", err diff --git a/pkg/network-controller/network_controller_helper.go b/pkg/network-controller/network_controller_helper.go index a93b8df..4ecaf99 100644 --- a/pkg/network-controller/network_controller_helper.go +++ b/pkg/network-controller/network_controller_helper.go @@ -82,8 +82,8 @@ func (c *NetworkController) addNetworkToDriver(kubeNetwork *crv1.Network) error glog.V(4).Infof("[NetworkController]: adding network %s", driverNetwork.Name) - // Check if tenant id exist - check, err := c.driver.CheckTenantID(driverNetwork.TenantID) + // Check if tenant exist or not by tenantID. + check, err := c.driver.CheckTenantByID(driverNetwork.TenantID) if err != nil { glog.Errorf("[NetworkController]: check tenantID failed: %v", err) return err @@ -107,7 +107,7 @@ func (c *NetworkController) addNetworkToDriver(kubeNetwork *crv1.Network) error newNetworkStatus = crv1.NetworkFailed } else { // Check if provider network has already created - _, err := c.driver.GetNetwork(networkName) + _, err := c.driver.GetNetworkByName(networkName) if err == nil { glog.Infof("[NetworkController]: network %s has already created", networkName) } else if err.Error() == util.ErrNotFound.Error() { diff --git a/pkg/openstack/client.go b/pkg/openstack/client.go index 05c2697..072cda4 100644 --- a/pkg/openstack/client.go +++ b/pkg/openstack/client.go @@ -71,8 +71,8 @@ type Interface interface { DeleteTenant(tenantName string) error // GetTenantIDFromName gets tenantID by tenantName. GetTenantIDFromName(tenantName string) (string, error) - // CheckTenantID checks tenant exist or not. - CheckTenantID(tenantID string) (bool, error) + // CheckTenantByID checks tenant exist or not by tenantID. + CheckTenantByID(tenantID string) (bool, error) // CreateUser creates user with username, password in the tenant. CreateUser(username, password, tenantID string) error // DeleteAllUsersOnTenant deletes all users on the tenant. @@ -81,8 +81,8 @@ type Interface interface { CreateNetwork(network *drivertypes.Network) error // GetNetworkByID gets network by networkID. GetNetworkByID(networkID string) (*drivertypes.Network, error) - // GetNetwork gets networks by networkName. - GetNetwork(networkName string) (*drivertypes.Network, error) + // GetNetworkByName gets network by networkName. + GetNetworkByName(networkName string) (*drivertypes.Network, error) // DeleteNetwork deletes network by networkName. DeleteNetwork(networkName string) error // GetProviderSubnet gets provider subnet by id @@ -235,6 +235,7 @@ func (os *Client) GetIntegrationBridge() string { return os.IntegrationBridge } +// GetTenantIDFromName gets tenantID by tenantName. func (os *Client) GetTenantIDFromName(tenantName string) (string, error) { if util.IsSystemNamespace(tenantName) { tenantName = util.SystemTenant @@ -276,6 +277,7 @@ func (os *Client) GetTenantIDFromName(tenantName string) (string, error) { return tenantID, nil } +// CreateTenant creates tenant by tenantname. func (os *Client) CreateTenant(tenantName string) (string, error) { createOpts := tenants.CreateOpts{ Name: tenantName, @@ -296,6 +298,7 @@ func (os *Client) CreateTenant(tenantName string) (string, error) { return tenantID, nil } +// DeleteTenant deletes tenant by tenantName. func (os *Client) DeleteTenant(tenantName string) error { return tenants.List(os.Identity, nil).EachPage(func(page pagination.Page) (bool, error) { tenantList, err := tenants.ExtractTenants(page) @@ -317,6 +320,7 @@ func (os *Client) DeleteTenant(tenantName string) error { }) } +// CreateUser creates user with username, password in the tenant. func (os *Client) CreateUser(username, password, tenantID string) error { opts := users.CreateOpts{ Name: username, @@ -333,6 +337,7 @@ func (os *Client) CreateUser(username, password, tenantID string) error { return nil } +// DeleteAllUsersOnTenant deletes all users on the tenant. func (os *Client) DeleteAllUsersOnTenant(tenantName string) error { tenantID, err := os.GetTenantIDFromName(tenantName) if err != nil { @@ -368,7 +373,7 @@ func reasonForError(err error) int { return 0 } -// Get tenant's network by tenantID(tenant and network are one to one mapping in stackube) +// GetOpenStackNetworkByTenantID gets tenant's network by tenantID(tenant and network are one to one mapping in stackube) func (os *Client) GetOpenStackNetworkByTenantID(tenantID string) (*networks.Network, error) { opts := networks.ListOpts{TenantID: tenantID} return os.getOpenStackNetwork(&opts) @@ -410,7 +415,7 @@ func (os *Client) getOpenStackNetwork(opts *networks.ListOpts) (*networks.Networ return osNetwork, err } -// Get provider subnet by id +// GetProviderSubnet gets provider subnet by subnetID func (os *Client) GetProviderSubnet(osSubnetID string) (*drivertypes.Subnet, error) { s, err := subnets.Get(os.Network, osSubnetID).Extract() if err != nil { @@ -439,7 +444,7 @@ func (os *Client) GetProviderSubnet(osSubnetID string) (*drivertypes.Subnet, err return &providerSubnet, nil } -// Get network by networkID +// GetNetworkByID gets network by networkID func (os *Client) GetNetworkByID(networkID string) (*drivertypes.Network, error) { osNetwork, err := os.getOpenStackNetworkByID(networkID) if err != nil { @@ -450,8 +455,8 @@ func (os *Client) GetNetworkByID(networkID string) (*drivertypes.Network, error) return os.OSNetworktoProviderNetwork(osNetwork) } -// Get network by networkName -func (os *Client) GetNetwork(networkName string) (*drivertypes.Network, error) { +// GetNetworkByName gets network by networkName +func (os *Client) GetNetworkByName(networkName string) (*drivertypes.Network, error) { osNetwork, err := os.getOpenStackNetworkByName(networkName) if err != nil { glog.Warningf("try to fetch openstack network by name: %v but failed: %v", networkName, err) @@ -495,7 +500,7 @@ func (os *Client) ToProviderStatus(status string) string { } } -// Create network +// CreateNetwork creates network. func (os *Client) CreateNetwork(network *drivertypes.Network) error { if len(network.Subnets) == 0 { return errors.New("Subnets is null") @@ -573,7 +578,7 @@ func (os *Client) CreateNetwork(network *drivertypes.Network) error { return nil } -// Update network +// UpdateNetwork updates network. func (os *Client) UpdateNetwork(network *drivertypes.Network) error { // TODO: update network subnets return nil @@ -601,7 +606,7 @@ func (os *Client) getRouterByName(name string) (*routers.Router, error) { return result, nil } -// Delete network by networkName +// DeleteNetwork deletes network by networkName. func (os *Client) DeleteNetwork(networkName string) error { osNetwork, err := os.getOpenStackNetworkByName(networkName) if err != nil { @@ -681,8 +686,8 @@ func (os *Client) DeleteNetwork(networkName string) error { return nil } -// Check the tenant id exist -func (os *Client) CheckTenantID(tenantID string) (bool, error) { +// CheckTenantByID checks tenant exist or not by tenantID. +func (os *Client) CheckTenantByID(tenantID string) (bool, error) { opts := tenants.ListOpts{} pager := tenants.List(os.Identity, &opts) @@ -710,6 +715,7 @@ func (os *Client) CheckTenantID(tenantID string) (bool, error) { return found, err } +// GetPort gets port by portName. func (os *Client) GetPort(name string) (*ports.Port, error) { opts := ports.ListOpts{Name: name} pager := ports.List(os.Network, opts) @@ -831,7 +837,7 @@ func (os *Client) ensureSecurityGroup(tenantID string) (string, error) { return securitygroup.ID, nil } -// Create an port +// CreatePort creates port by neworkID, tenantID and portName. func (os *Client) CreatePort(networkID, tenantID, portName string) (*portsbinding.Port, error) { securitygroup, err := os.ensureSecurityGroup(tenantID) if err != nil { @@ -860,7 +866,7 @@ func (os *Client) CreatePort(networkID, tenantID, portName string) (*portsbindin return port, nil } -// List all ports in the network +// ListPorts lists ports by networkID and deviceOwner. func (os *Client) ListPorts(networkID, deviceOwner string) ([]ports.Port, error) { var results []ports.Port opts := ports.ListOpts{ diff --git a/pkg/openstack/openstack_fake.go b/pkg/openstack/openstack_fake.go new file mode 100644 index 0000000..0057c9b --- /dev/null +++ b/pkg/openstack/openstack_fake.go @@ -0,0 +1,400 @@ +/* +Copyright (c) 2017 OpenStack Foundation. + +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 openstack + +import ( + "crypto/sha1" + "errors" + "fmt" + "io" + + crv1 "git.openstack.org/openstack/stackube/pkg/apis/v1" + crdClient "git.openstack.org/openstack/stackube/pkg/kubecrd" + drivertypes "git.openstack.org/openstack/stackube/pkg/openstack/types" + "git.openstack.org/openstack/stackube/pkg/util" + "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants" + "github.com/gophercloud/gophercloud/openstack/identity/v2/users" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" + "github.com/gophercloud/gophercloud/openstack/networking/v2/subnets" +) + +var ErrAlreadyExist = errors.New("AlreadyExist") + +// FakeOSClient is a simple fake openstack client, so that stackube +// can be run for testing without requiring a real openstack setup. +type FakeOSClient struct { + Tenants map[string]*tenants.Tenant + Users map[string]*users.User + Networks map[string]*drivertypes.Network + Subnets map[string]*subnets.Subnet + Routers map[string]*routers.Router + Ports map[string][]ports.Port + // TODO(mozhuli): Add fakeCRDClient. + CRDClient *crdClient.CRDClient + PluginName string + IntegrationBridge string +} + +var _ = Interface(&FakeOSClient{}) + +// NewFake creates a new FakeOSClient. +func NewFake() *FakeOSClient { + return &FakeOSClient{ + Tenants: make(map[string]*tenants.Tenant), + Users: make(map[string]*users.User), + Networks: make(map[string]*drivertypes.Network), + Subnets: make(map[string]*subnets.Subnet), + Routers: make(map[string]*routers.Router), + Ports: make(map[string][]ports.Port), + PluginName: "ovs", + IntegrationBridge: "bi-int", + } +} + +// SetTenant injects fake tenant. +func (os *FakeOSClient) SetTenant(tenantName, tenantID string) { + tenant := &tenants.Tenant{ + Name: tenantName, + ID: tenantID, + } + os.Tenants[tenantName] = tenant +} + +// SetUser injects fake user. +func (os *FakeOSClient) SetUser(userName, userID, tenantID string) { + user := &users.User{ + Username: userName, + ID: userID, + TenantID: tenantID, + } + os.Users[tenantID] = user +} + +// SetNetwork injects fake network. +func (os *FakeOSClient) SetNetwork(networkName, networkID string) { + network := &drivertypes.Network{ + Name: networkName, + Uid: networkID, + } + os.Networks[networkName] = network +} + +// SetPort injects fake port. +func (os *FakeOSClient) SetPort(networkID, deviceOwner, deviceID string) { + netPorts, ok := os.Ports[networkID] + p := ports.Port{ + NetworkID: networkID, + DeviceOwner: deviceOwner, + DeviceID: deviceID, + } + if !ok { + var ps []ports.Port + ps = append(ps, p) + os.Ports[networkID] = ps + } + netPorts = append(netPorts, p) + os.Ports[networkID] = netPorts +} + +func tenantIDHash(tenantName string) string { + return idHash(tenantName) +} + +func userIDHash(userName, tenantID string) string { + return idHash(userName) +} + +func networkIDHash(networkName string) string { + return idHash(networkName) +} + +func subnetIDHash(subnetName string) string { + return idHash(subnetName) +} + +func routerIDHash(routerName string) string { + return idHash(routerName) +} + +func portdeviceIDHash(networkID, deviceOwner string) string { + return idHash(networkID, deviceOwner) +} + +func idHash(data ...string) string { + var s string + for _, d := range data { + s += d + } + h := sha1.New() + io.WriteString(h, s) + return fmt.Sprintf("%x", h.Sum(nil))[:16] +} + +// CreateTenant creates tenant by tenantname. +func (os *FakeOSClient) CreateTenant(tenantName string) (string, error) { + if t, ok := os.Tenants[tenantName]; ok { + return t.ID, nil + } + tenant := &tenants.Tenant{ + Name: tenantName, + ID: tenantIDHash(tenantName), + } + os.Tenants[tenantName] = tenant + return tenant.ID, nil +} + +// DeleteTenant deletes tenant by tenantName. +func (os *FakeOSClient) DeleteTenant(tenantName string) error { + delete(os.Tenants, tenantName) + return nil +} + +// GetTenantIDFromName gets tenantID by tenantName. +func (os *FakeOSClient) GetTenantIDFromName(tenantName string) (string, error) { + if util.IsSystemNamespace(tenantName) { + tenantName = util.SystemTenant + } + + // If tenantID is specified, return it directly + var ( + tenant *crv1.Tenant + err error + ) + // TODO(mozhuli): use fakeCRDClient. + if tenant, err = os.CRDClient.GetTenant(tenantName); err != nil { + return "", err + } + if tenant.Spec.TenantID != "" { + return tenant.Spec.TenantID, nil + } + + t, ok := os.Tenants[tenantName] + if !ok { + return "", nil + } + + return t.ID, nil +} + +// CheckTenantByID checks tenant exist or not by tenantID. +func (os *FakeOSClient) CheckTenantByID(tenantID string) (bool, error) { + for _, tenent := range os.Tenants { + if tenent.ID == tenantID { + return true, nil + } + } + return false, ErrNotFound +} + +// CreateUser creates user with username, password in the tenant. +func (os *FakeOSClient) CreateUser(username, password, tenantID string) error { + user := &users.User{ + Name: username, + TenantID: tenantID, + ID: userIDHash(username, tenantID), + } + os.Users[tenantID] = user + return nil +} + +// DeleteAllUsersOnTenant deletes all users on the tenant. +func (os *FakeOSClient) DeleteAllUsersOnTenant(tenantName string) error { + tenant := os.Tenants[tenantName] + + delete(os.Users, tenant.ID) + return nil +} + +func (os *FakeOSClient) createNetwork(networkName, tenantID string) error { + if _, ok := os.Networks[networkName]; ok { + return ErrAlreadyExist + } + + network := &drivertypes.Network{ + Name: networkName, + Uid: networkIDHash(networkName), + TenantID: tenantID, + } + os.Networks[networkName] = network + return nil +} + +func (os *FakeOSClient) deleteNetwork(networkName string) error { + delete(os.Networks, networkName) + return nil +} + +func (os *FakeOSClient) createRouter(routerName, tenantID string) error { + if _, ok := os.Routers[routerName]; ok { + return ErrAlreadyExist + } + + router := &routers.Router{ + Name: routerName, + TenantID: tenantID, + ID: routerIDHash(routerName), + } + os.Routers[routerName] = router + return nil +} + +func (os *FakeOSClient) deleteRouter(routerName string) error { + delete(os.Routers, routerName) + return nil +} + +func (os *FakeOSClient) createSubnet(subnetName, networkID, tenantID string) error { + if _, ok := os.Subnets[subnetName]; ok { + return ErrAlreadyExist + } + + subnet := &subnets.Subnet{ + Name: subnetName, + TenantID: tenantID, + NetworkID: networkID, + ID: subnetIDHash(subnetName), + } + os.Subnets[subnetName] = subnet + return nil +} + +// CreateNetwork creates network. +// TODO(mozhuli): make it more general. +func (os *FakeOSClient) CreateNetwork(network *drivertypes.Network) error { + if len(network.Subnets) == 0 { + return errors.New("Subnets is null") + } + + // create network + err := os.createNetwork(network.Name, network.TenantID) + if err != nil { + return errors.New("Create network failed") + } + // create router, and use network name as router name for convenience. + err = os.createRouter(network.Name, network.TenantID) + if err != nil { + os.deleteNetwork(network.Name) + return errors.New("Create router failed") + } + // create subnets and connect them to router + err = os.createSubnet(network.Subnets[0].Name, network.Uid, network.TenantID) + if err != nil { + os.deleteRouter(network.Name) + os.deleteNetwork(network.Name) + return errors.New("Create subnet failed") + } + return nil +} + +// GetNetworkByID gets network by networkID. +func (os *FakeOSClient) GetNetworkByID(networkID string) (*drivertypes.Network, error) { + return nil, nil +} + +// GetNetworkByName get network by networkName +func (os *FakeOSClient) GetNetworkByName(networkName string) (*drivertypes.Network, error) { + network, ok := os.Networks[networkName] + if !ok { + return nil, ErrNotFound + } + + return network, nil +} + +// DeleteNetwork deletes network by networkName. +func (os *FakeOSClient) DeleteNetwork(networkName string) error { + return nil +} + +// GetProviderSubnet gets provider subnet by id +func (os *FakeOSClient) GetProviderSubnet(osSubnetID string) (*drivertypes.Subnet, error) { + return nil, nil +} + +// CreatePort creates port by neworkID, tenantID and portName. +func (os *FakeOSClient) CreatePort(networkID, tenantID, portName string) (*portsbinding.Port, error) { + return nil, nil +} + +// GetPort gets port by portName. +func (os *FakeOSClient) GetPort(name string) (*ports.Port, error) { + return nil, nil +} + +// ListPorts list all ports which have the deviceOwner in the network. +func (os *FakeOSClient) ListPorts(networkID, deviceOwner string) ([]ports.Port, error) { + var results []ports.Port + portList, ok := os.Ports[networkID] + if !ok { + return results, nil + } + + for _, port := range portList { + if port.DeviceOwner == deviceOwner { + results = append(results, port) + } + } + return results, nil +} + +// DeletePortByName deletes port by portName. +func (os *FakeOSClient) DeletePortByName(portName string) error { + return nil +} + +// DeletePortByID deletes port by portID. +func (os *FakeOSClient) DeletePortByID(portID string) error { + return nil +} + +// UpdatePortsBinding updates port binding. +func (os *FakeOSClient) UpdatePortsBinding(portID, deviceOwner string) error { + return nil +} + +// LoadBalancerExist returns whether a load balancer has already been exist. +func (os *FakeOSClient) LoadBalancerExist(name string) (bool, error) { + return true, nil +} + +// EnsureLoadBalancer ensures a load balancer is created. +func (os *FakeOSClient) EnsureLoadBalancer(lb *LoadBalancer) (*LoadBalancerStatus, error) { + return nil, nil +} + +// EnsureLoadBalancerDeleted ensures a load balancer is deleted. +func (os *FakeOSClient) EnsureLoadBalancerDeleted(name string) error { + return nil +} + +// GetCRDClient returns the CRDClient. +// TODO(mozhuli): use fakeCRDClient. +func (os *FakeOSClient) GetCRDClient() *crdClient.CRDClient { + return os.CRDClient +} + +// GetPluginName returns the plugin name. +func (os *FakeOSClient) GetPluginName() string { + return os.PluginName +} + +// GetIntegrationBridge returns the integration bridge name. +func (os *FakeOSClient) GetIntegrationBridge() string { + return os.IntegrationBridge +} diff --git a/pkg/proxy/iptables_fake.go b/pkg/proxy/iptables_fake.go new file mode 100644 index 0000000..ef12b22 --- /dev/null +++ b/pkg/proxy/iptables_fake.go @@ -0,0 +1,99 @@ +/* +Copyright (c) 2017 OpenStack Foundation. + +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 proxy + +import ( + "fmt" + "strings" +) + +const ( + Destination = "-d " + Source = "-s " + DPort = "--dport " + Protocol = "-p " + Jump = "-j " + ToDest = "--to-destination " +) + +// Rule represents chain's rule. +type Rule map[string]string + +// FakeIPTables have noop implementation of fake iptables function. +type FakeIPTables struct { + namespace string + NSLines map[string][]byte +} + +// NewFake return new FakeIPTables. +func NewFake() *FakeIPTables { + return &FakeIPTables{ + NSLines: make(map[string][]byte), + } +} + +func (f *FakeIPTables) ensureChain() error { + return nil +} + +func (f *FakeIPTables) ensureRule(op, chain string, args []string) error { + return nil +} + +func (f *FakeIPTables) restoreAll(data []byte) error { + d := make([]byte, len(data)) + copy(d, data) + f.NSLines[f.namespace] = d + return nil +} + +func (f *FakeIPTables) netnsExist() bool { + return true +} + +func (f *FakeIPTables) setNetns(netns string) { + f.namespace = netns +} + +func getToken(line, seperator string) string { + tokens := strings.Split(line, seperator) + if len(tokens) == 2 { + return strings.Split(tokens[1], " ")[0] + } + return "" +} + +// GetRules returns a list of rules for the given chain. +// The chain name must match exactly. +// The matching is pretty dumb, don't rely on it for anything but testing. +func (f *FakeIPTables) GetRules(chainName, namespace string) (rules []Rule) { + for _, l := range strings.Split(string(f.NSLines[namespace]), "\n") { + if strings.Contains(l, fmt.Sprintf("-A %v", chainName)) { + newRule := Rule(map[string]string{}) + for _, arg := range []string{Destination, Source, DPort, Protocol, Jump, ToDest} { + tok := getToken(l, arg) + if tok != "" { + newRule[arg] = tok + } + } + rules = append(rules, newRule) + } + } + return +} + +var _ = iptablesInterface(&FakeIPTables{}) diff --git a/pkg/proxy/proxier.go b/pkg/proxy/proxier.go index ab7cc80..ff75091 100644 --- a/pkg/proxy/proxier.go +++ b/pkg/proxy/proxier.go @@ -201,7 +201,7 @@ func (p *Proxier) getRouterForNamespace(namespace string) (string, error) { // Only support one network and network's name is same with namespace. // TODO: make it general after multi-network is supported. networkName := util.BuildNetworkName(namespace, namespace) - network, err := p.osClient.GetNetwork(networkName) + network, err := p.osClient.GetNetworkByName(networkName) if err != nil { glog.Errorf("Get network by name %q failed: %v", networkName, err) return "", err diff --git a/pkg/proxy/proxier_test.go b/pkg/proxy/proxier_test.go new file mode 100644 index 0000000..e292f32 --- /dev/null +++ b/pkg/proxy/proxier_test.go @@ -0,0 +1,637 @@ +/* +Copyright (c) 2017 OpenStack Foundation. + +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 proxy + +import ( + "fmt" + "net" + "testing" + "time" + + "github.com/davecgh/go-spew/spew" + + "git.openstack.org/openstack/stackube/pkg/openstack" + "git.openstack.org/openstack/stackube/pkg/util" + + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/kubernetes/pkg/util/async" +) + +func newFakeServiceInfo(serviceName string, ip net.IP) *serviceInfo { + return &serviceInfo{ + name: serviceName, + clusterIP: ip, + } +} + +func Test_getServiceIP(t *testing.T) { + fp := NewFakeProxier(nil, nil) + + testCases := []struct { + serviceInfo *serviceInfo + expected string + }{{ + // Case[0]: kube-dns service. + serviceInfo: newFakeServiceInfo("kube-dns", net.IPv4(1, 2, 3, 4)), + expected: testclusterDNS, + }, { + // Case[1]: other service. + serviceInfo: newFakeServiceInfo("test", net.IPv4(1, 2, 3, 4)), + expected: "1.2.3.4", + }, + } + + for tci, tc := range testCases { + // outputs + clusterIP := fp.getServiceIP(tc.serviceInfo) + + if clusterIP != tc.expected { + t.Errorf("Case[%d] expected %#v, got %#v", tci, tc.expected, clusterIP) + } + } +} + +const testclusterDNS = "10.20.30.40" + +func NewFakeProxier(ipt iptablesInterface, osClient openstack.Interface) *Proxier { + p := &Proxier{ + clusterDNS: testclusterDNS, + osClient: osClient, + iptables: ipt, + endpointsChanges: newEndpointsChangeMap(""), + serviceChanges: newServiceChangeMap(), + namespaceChanges: newNamespaceChangeMap(), + serviceMap: make(proxyServiceMap), + endpointsMap: make(proxyEndpointsMap), + namespaceMap: make(map[string]*namespaceInfo), + serviceNSMap: make(map[string]proxyServiceMap), + } + + p.syncRunner = async.NewBoundedFrequencyRunner("test-sync-runner", p.syncProxyRules, 0, time.Minute, 1) + return p +} + +func hasDNAT(rules []Rule, endpoint string) bool { + for _, r := range rules { + if r[ToDest] == endpoint { + return true + } + } + return false +} + +func makeTestService(namespace, name string, svcFunc func(*v1.Service)) *v1.Service { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{}, + }, + Spec: v1.ServiceSpec{}, + Status: v1.ServiceStatus{}, + } + svcFunc(svc) + return svc +} + +func makeServiceMap(proxier *Proxier, allServices ...*v1.Service) { + for i := range allServices { + proxier.onServiceAdded(allServices[i]) + } + + proxier.mu.Lock() + defer proxier.mu.Unlock() + proxier.servicesSynced = true +} + +func makeTestEndpoints(namespace, name string, eptFunc func(*v1.Endpoints)) *v1.Endpoints { + ept := &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } + eptFunc(ept) + return ept +} + +func makeEndpointsMap(proxier *Proxier, allEndpoints ...*v1.Endpoints) { + for i := range allEndpoints { + proxier.onEndpointsAdded(allEndpoints[i]) + } + + proxier.mu.Lock() + defer proxier.mu.Unlock() + proxier.endpointsSynced = true +} + +func makeTestNamespace(name string) *v1.Namespace { + ns := &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Annotations: map[string]string{}, + }, + Spec: v1.NamespaceSpec{}, + Status: v1.NamespaceStatus{}, + } + return ns +} + +func makeNamespaceMap(proxier *Proxier, allNamespaces ...*v1.Namespace) { + for i := range allNamespaces { + proxier.onNamespaceAdded(allNamespaces[i]) + } + + proxier.mu.Lock() + defer proxier.mu.Unlock() + proxier.namespaceSynced = true +} + +func makeNSN(namespace, name string) types.NamespacedName { + return types.NamespacedName{Namespace: namespace, Name: name} +} + +func makeServicePortName(ns, name, port string) servicePortName { + return servicePortName{ + NamespacedName: makeNSN(ns, name), + Port: port, + } +} + +func errorf(msg string, rules []Rule, t *testing.T) { + for _, r := range rules { + t.Logf("%q", r) + } + t.Errorf("%v", msg) +} + +func TestClusterNoEndpoint(t *testing.T) { + testNamespace := "test" + svcIP := "1.2.3.4" + svcPort := 80 + svcPortName := servicePortName{ + NamespacedName: makeNSN(testNamespace, "svc1"), + Port: "80", + } + + //Creates fake iptables. + ipt := NewFake() + //Create a fake openstack client. + osClient := openstack.NewFake() + // Injects fake network. + networkName := util.BuildNetworkName(testNamespace, testNamespace) + osClient.SetNetwork(networkName, "123") + // Injects fake port. + osClient.SetPort("123", "network:router_interface", "123") + // Creates a new fake proxier. + fp := NewFakeProxier(ipt, osClient) + + makeServiceMap(fp, + makeTestService(svcPortName.Namespace, svcPortName.Name, func(svc *v1.Service) { + svc.Spec.ClusterIP = svcIP + svc.Spec.Type = v1.ServiceTypeNodePort + svc.Spec.Ports = []v1.ServicePort{{ + Name: svcPortName.Port, + Port: int32(svcPort), + Protocol: v1.ProtocolTCP, + }} + }), + ) + + makeEndpointsMap(fp) + + makeNamespaceMap(fp, makeTestNamespace(svcPortName.Namespace)) + + fp.syncProxyRules() + + stackubeRules := ipt.GetRules(ChainSKPrerouting, "qrouter-123") + if len(stackubeRules) != 0 { + errorf(fmt.Sprintf("Unexpected rule for chain %v without endpoints in namespace %v", ChainSKPrerouting, svcPortName.Namespace), stackubeRules, t) + } +} + +func noClusterIPType(svcType v1.ServiceType) []Rule { + testNamespace := "test" + svcIP := "1.2.3.4" + svcPort := 80 + svcPortName := servicePortName{ + NamespacedName: makeNSN(testNamespace, "svc1"), + Port: "80", + } + + // Creates fake iptables. + ipt := NewFake() + // Create a fake openstack client. + osClient := openstack.NewFake() + // Injects fake network. + networkName := util.BuildNetworkName(testNamespace, testNamespace) + osClient.SetNetwork(networkName, "123") + // Injects fake port. + osClient.SetPort("123", "network:router_interface", "123") + // Creates a new fake proxier. + fp := NewFakeProxier(ipt, osClient) + + makeServiceMap(fp, + makeTestService(svcPortName.Namespace, svcPortName.Namespace, func(svc *v1.Service) { + svc.Spec.ClusterIP = svcIP + svc.Spec.Type = svcType + svc.Spec.Ports = []v1.ServicePort{{ + Name: svcPortName.Port, + Port: int32(svcPort), + Protocol: v1.ProtocolTCP, + }} + }), + ) + + makeEndpointsMap(fp) + + makeNamespaceMap(fp, makeTestNamespace(svcPortName.Namespace)) + + fp.syncProxyRules() + + stackubeRules := ipt.GetRules(ChainSKPrerouting, "qrouter-123") + return stackubeRules +} + +func TestNoClusterIPType(t *testing.T) { + testCases := map[string]v1.ServiceType{ + "case 1": v1.ServiceTypeNodePort, + "case 2": v1.ServiceTypeLoadBalancer, + "case 3": v1.ServiceTypeExternalName, + } + + for k, tc := range testCases { + got := noClusterIPType(tc) + if len(got) != 0 { + errorf(fmt.Sprintf("%v: unexpected rule for chain %v without ClusterIP service type", k, ChainSKPrerouting), got, t) + } + } +} + +func TestClusterIPEndpointsJump(t *testing.T) { + testNamespace := "test" + svcIP := "1.2.3.4" + svcPort := 80 + svcPortName := servicePortName{ + NamespacedName: makeNSN(testNamespace, "svc1"), + Port: "80", + } + + // Creates fake iptables. + ipt := NewFake() + // Create a fake openstack client. + osClient := openstack.NewFake() + // Injects fake network. + networkName := util.BuildNetworkName(testNamespace, testNamespace) + osClient.SetNetwork(networkName, "123") + // Injects fake port. + osClient.SetPort("123", "network:router_interface", "123") + // Creates a new fake proxier. + fp := NewFakeProxier(ipt, osClient) + + makeServiceMap(fp, + makeTestService(svcPortName.Namespace, svcPortName.Name, func(svc *v1.Service) { + svc.Spec.ClusterIP = svcIP + svc.Spec.Ports = []v1.ServicePort{{ + Name: svcPortName.Port, + Port: int32(svcPort), + Protocol: v1.ProtocolTCP, + }} + }), + ) + + epIP := "192.168.0.1" + makeEndpointsMap(fp, + makeTestEndpoints(svcPortName.Namespace, svcPortName.Name, func(ept *v1.Endpoints) { + ept.Subsets = []v1.EndpointSubset{{ + Addresses: []v1.EndpointAddress{{ + IP: epIP, + }}, + Ports: []v1.EndpointPort{{ + Name: svcPortName.Port, + Port: int32(svcPort), + }}, + }} + }), + ) + + makeNamespaceMap(fp, makeTestNamespace(svcPortName.Namespace)) + + fp.syncProxyRules() + + epStr := fmt.Sprintf("%s:%d", epIP, svcPort) + + stackubeRules := ipt.GetRules(string(ChainSKPrerouting), "qrouter-123") + if len(stackubeRules) == 0 { + errorf(fmt.Sprintf("Unexpected rule for chain %v with endpoints in namespace %v", ChainSKPrerouting, svcPortName.Namespace), stackubeRules, t) + } + if !hasDNAT(stackubeRules, epStr) { + errorf(fmt.Sprintf("Chain %v lacks DNAT to %v", ChainSKPrerouting, epStr), stackubeRules, t) + } +} + +func TestMultiNamespacesService(t *testing.T) { + ns1 := "ns1" + svcIP1 := "1.2.3.4" + svcPort1 := 80 + svcPortName1 := servicePortName{ + NamespacedName: makeNSN(ns1, "svc1"), + Port: "80", + } + + ns2 := "ns2" + svcIP2 := "1.2.3.5" + svcPort2 := 8080 + svcPortName2 := servicePortName{ + NamespacedName: makeNSN(ns2, "svc1"), + Port: "8080", + } + + // Creates fake iptables. + ipt := NewFake() + // Create a fake openstack client. + osClient := openstack.NewFake() + // Injects fake network. + networkName1 := util.BuildNetworkName(ns1, ns1) + osClient.SetNetwork(networkName1, "123") + networkName2 := util.BuildNetworkName(ns2, ns2) + osClient.SetNetwork(networkName2, "456") + // Injects fake port. + osClient.SetPort("123", "network:router_interface", "123") + osClient.SetPort("456", "network:router_interface", "456") + // Creates a new fake proxier. + fp := NewFakeProxier(ipt, osClient) + + makeServiceMap(fp, + makeTestService(svcPortName1.Namespace, svcPortName1.Name, func(svc *v1.Service) { + svc.Spec.ClusterIP = svcIP1 + svc.Spec.Ports = []v1.ServicePort{{ + Name: svcPortName1.Port, + Port: int32(svcPort1), + Protocol: v1.ProtocolTCP, + }} + }), + makeTestService(svcPortName2.Namespace, svcPortName2.Name, func(svc *v1.Service) { + svc.Spec.ClusterIP = svcIP2 + svc.Spec.Ports = []v1.ServicePort{{ + Name: svcPortName2.Port, + Port: int32(svcPort2), + Protocol: v1.ProtocolTCP, + }} + }), + ) + + epIP1 := "192.168.0.1" + epIP2 := "192.168.1.1" + makeEndpointsMap(fp, + makeTestEndpoints(svcPortName1.Namespace, svcPortName1.Name, func(ept *v1.Endpoints) { + ept.Subsets = []v1.EndpointSubset{{ + Addresses: []v1.EndpointAddress{{ + IP: epIP1, + }}, + Ports: []v1.EndpointPort{{ + Name: svcPortName1.Port, + Port: int32(svcPort1), + }}, + }} + }), + makeTestEndpoints(svcPortName2.Namespace, svcPortName2.Name, func(ept *v1.Endpoints) { + ept.Subsets = []v1.EndpointSubset{{ + Addresses: []v1.EndpointAddress{{ + IP: epIP2, + }}, + Ports: []v1.EndpointPort{{ + Name: svcPortName2.Port, + Port: int32(svcPort2), + }}, + }} + }), + ) + + makeNamespaceMap(fp, + makeTestNamespace(svcPortName1.Namespace), + makeTestNamespace(svcPortName2.Namespace), + ) + + fp.syncProxyRules() + + epStr1 := fmt.Sprintf("%s:%d", epIP1, svcPort1) + + ns1Rules := ipt.GetRules(string(ChainSKPrerouting), "qrouter-123") + if len(ns1Rules) == 0 { + errorf(fmt.Sprintf("Unexpected rule for chain %v with endpoints in namespace %v", ChainSKPrerouting, svcPortName1.Namespace), ns1Rules, t) + } + if !hasDNAT(ns1Rules, epStr1) { + errorf(fmt.Sprintf("Chain %v lacks DNAT to %v", ChainSKPrerouting, epStr1), ns1Rules, t) + } + + epStr2 := fmt.Sprintf("%s:%d", epIP2, svcPort2) + ns2Rules := ipt.GetRules(string(ChainSKPrerouting), "qrouter-456") + if len(ns2Rules) == 0 { + errorf(fmt.Sprintf("Unexpected rule for chain %v with endpoints in namespace %v", ChainSKPrerouting, svcPortName2.Namespace), ns2Rules, t) + } + if !hasDNAT(ns2Rules, epStr2) { + errorf(fmt.Sprintf("Chain %v lacks DNAT to %v", ChainSKPrerouting, epStr2), ns2Rules, t) + } +} + +// This is a coarse test, but it offers some modicum of confidence as the code is evolved. +func Test_endpointsToEndpointsMap(t *testing.T) { + testCases := []struct { + newEndpoints *v1.Endpoints + expected map[servicePortName][]*endpointsInfo + }{{ + // Case[0]: nothing + newEndpoints: makeTestEndpoints("ns1", "ep1", func(ept *v1.Endpoints) {}), + expected: map[servicePortName][]*endpointsInfo{}, + }, { + // Case[1]: no changes, unnamed port + newEndpoints: makeTestEndpoints("ns1", "ep1", func(ept *v1.Endpoints) { + ept.Subsets = []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{{ + IP: "1.1.1.1", + }}, + Ports: []v1.EndpointPort{{ + Name: "", + Port: 11, + }}, + }, + } + }), + expected: map[servicePortName][]*endpointsInfo{ + makeServicePortName("ns1", "ep1", ""): { + {endpoint: "1.1.1.1:11", isLocal: false}, + }, + }, + }, { + // Case[2]: no changes, named port + newEndpoints: makeTestEndpoints("ns1", "ep1", func(ept *v1.Endpoints) { + ept.Subsets = []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{{ + IP: "1.1.1.1", + }}, + Ports: []v1.EndpointPort{{ + Name: "port", + Port: 11, + }}, + }, + } + }), + expected: map[servicePortName][]*endpointsInfo{ + makeServicePortName("ns1", "ep1", "port"): { + {endpoint: "1.1.1.1:11", isLocal: false}, + }, + }, + }, { + // Case[3]: new port + newEndpoints: makeTestEndpoints("ns1", "ep1", func(ept *v1.Endpoints) { + ept.Subsets = []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{{ + IP: "1.1.1.1", + }}, + Ports: []v1.EndpointPort{{ + Port: 11, + }}, + }, + } + }), + expected: map[servicePortName][]*endpointsInfo{ + makeServicePortName("ns1", "ep1", ""): { + {endpoint: "1.1.1.1:11", isLocal: false}, + }, + }, + }, { + // Case[4]: remove port + newEndpoints: makeTestEndpoints("ns1", "ep1", func(ept *v1.Endpoints) {}), + expected: map[servicePortName][]*endpointsInfo{}, + }, { + // Case[5]: new IP and port + newEndpoints: makeTestEndpoints("ns1", "ep1", func(ept *v1.Endpoints) { + ept.Subsets = []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{{ + IP: "1.1.1.1", + }, { + IP: "2.2.2.2", + }}, + Ports: []v1.EndpointPort{{ + Name: "p1", + Port: 11, + }, { + Name: "p2", + Port: 22, + }}, + }, + } + }), + expected: map[servicePortName][]*endpointsInfo{ + makeServicePortName("ns1", "ep1", "p1"): { + {endpoint: "1.1.1.1:11", isLocal: false}, + {endpoint: "2.2.2.2:11", isLocal: false}, + }, + makeServicePortName("ns1", "ep1", "p2"): { + {endpoint: "1.1.1.1:22", isLocal: false}, + {endpoint: "2.2.2.2:22", isLocal: false}, + }, + }, + }, { + // Case[6]: remove IP and port + newEndpoints: makeTestEndpoints("ns1", "ep1", func(ept *v1.Endpoints) { + ept.Subsets = []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{{ + IP: "1.1.1.1", + }}, + Ports: []v1.EndpointPort{{ + Name: "p1", + Port: 11, + }}, + }, + } + }), + expected: map[servicePortName][]*endpointsInfo{ + makeServicePortName("ns1", "ep1", "p1"): { + {endpoint: "1.1.1.1:11", isLocal: false}, + }, + }, + }, { + // Case[7]: rename port + newEndpoints: makeTestEndpoints("ns1", "ep1", func(ept *v1.Endpoints) { + ept.Subsets = []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{{ + IP: "1.1.1.1", + }}, + Ports: []v1.EndpointPort{{ + Name: "p2", + Port: 11, + }}, + }, + } + }), + expected: map[servicePortName][]*endpointsInfo{ + makeServicePortName("ns1", "ep1", "p2"): { + {endpoint: "1.1.1.1:11", isLocal: false}, + }, + }, + }, { + // Case[8]: renumber port + newEndpoints: makeTestEndpoints("ns1", "ep1", func(ept *v1.Endpoints) { + ept.Subsets = []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{{ + IP: "1.1.1.1", + }}, + Ports: []v1.EndpointPort{{ + Name: "p1", + Port: 22, + }}, + }, + } + }), + expected: map[servicePortName][]*endpointsInfo{ + makeServicePortName("ns1", "ep1", "p1"): { + {endpoint: "1.1.1.1:22", isLocal: false}, + }, + }, + }} + + for tci, tc := range testCases { + // outputs + newEndpoints := endpointsToEndpointsMap(tc.newEndpoints, "host") + + if len(newEndpoints) != len(tc.expected) { + t.Errorf("[%d] expected %d new, got %d: %v", tci, len(tc.expected), len(newEndpoints), spew.Sdump(newEndpoints)) + } + for x := range tc.expected { + if len(newEndpoints[x]) != len(tc.expected[x]) { + t.Errorf("[%d] expected %d endpoints for %v, got %d", tci, len(tc.expected[x]), x, len(newEndpoints[x])) + } else { + for i := range newEndpoints[x] { + if *(newEndpoints[x][i]) != *(tc.expected[x][i]) { + t.Errorf("[%d] expected new[%v][%d] to be %v, got %v", tci, x, i, tc.expected[x][i], *(newEndpoints[x][i])) + } + } + } + } + } +} diff --git a/pkg/service-controller/service_controller.go b/pkg/service-controller/service_controller.go index a364034..bfd5db8 100644 --- a/pkg/service-controller/service_controller.go +++ b/pkg/service-controller/service_controller.go @@ -332,7 +332,7 @@ func (s *ServiceController) createLoadBalancer(service *v1.Service) (*v1.LoadBal // Only support one network and network's name is same with namespace. networkName := util.BuildNetworkName(service.Namespace, service.Namespace) - network, err := s.osClient.GetNetwork(networkName) + network, err := s.osClient.GetNetworkByName(networkName) if err != nil { glog.Errorf("Get network by name %q failed: %v", networkName, err) return nil, err