package f5
import (
"encoding/json"
"flag"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os/exec"
"reflect"
"strings"
"testing"
"github.com/gorilla/mux"
"github.com/openshift/origin/pkg/cmd/util"
kapi "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/watch"
routeapi "github.com/openshift/origin/pkg/route/api"
)
type (
// mockF5State stores the state necessary to mock the functionality of an F5
// BIG-IP host that the F5 router uses.
mockF5State struct {
// policies is the set of policies that exist in the mock F5 host.
policies map[string]map[string]policyRule
// vserverPolicies represents the associations between vservers and policies
// in the mock F5 host.
vserverPolicies map[string]map[string]bool
// certs represents the set of certificates that have been installed into
// the mock F5 host.
certs map[string]bool
// keys represents the set of certificates that have been installed into
// the mock F5 host.
keys map[string]bool
// serverSslProfiles represents the set of server-ssl profiles that exist in
// the mock F5 host.
serverSslProfiles map[string]bool
// clientSslProfiles represents the set of client-ssl profiles that exist in
// the mock F5 host.
clientSslProfiles map[string]bool
// vserverProfiles represents the associations between vservers and
// client-ssl and server-ssl profiles in the mock F5 host.
//
// Note that although the F5 management console displays client and server
// profiles separately, the F5 iControl REST interface puts these
// associations under a single REST endpoint.
vserverProfiles map[string]map[string]bool
// datagroups represents the iRules data-groups in the F5 host. For our
// purposes, we assume that every data-group maps strings to strings.
datagroups map[string]datagroup
// iRules represents the iRules that exist in the F5 host.
iRules map[string]iRule
// vserverIRules represents the associations between vservers and iRules in
// the mock F5 host.
vserverIRules map[string][]string
// partitionPaths represents the partitions that exist in
// the mock F5 host.
partitionPaths map[string]string
// pools represents the pools that exist on the mock F5 host.
pools map[string]pool
}
mockF5 struct {
// state is the internal state of the mock F5 BIG-IP host.
state mockF5State
// server is the mock F5 BIG-IP host, which accepts HTTPS connections and
// behaves as an actual F5 BIG-IP host for testing purposes.
server *httptest.Server
}
// A policyCondition describes a single condition for a policy rule to match.
policyCondition struct {
HttpHost bool `json:"httpHost,omitempty"`
HttpUri bool `json:"httpUri,omitempty"`
PathSegment bool `json:"pathSegment,omitempty"`
Index int `json:"index"`
Host bool `json:"host,omitempty"`
Values []string `json:"values"`
}
// A policyRule has a name and comprises a list of conditions and a list of
// actions.
policyRule struct {
conditions []policyCondition
}
// A datagroup is an associative array. For our purposes, a datagroup maps
// strings to strings.
datagroup map[string]string
// An iRule comprises a string of TCL code.
iRule string
// A pool comprises a set of strings of the form addr:port.
pool map[string]bool
)
const (
httpVserverName = "ose-vserver"
httpsVserverName = "https-ose-vserver"
insecureRoutesPolicyName = "openshift_insecure_routes"
secureRoutesPolicyName = "openshift_secure_routes"
passthroughIRuleName = "openshift_passthrough_irule"
passthroughIRuleDatagroupName = "ssl_passthrough_servername_dg"
)
func mockExecCommand(command string, args ...string) *exec.Cmd {
// TODO: Parse ssh and scp commands in order to keep track of what files are
// being uploaded so that we can perform more validations in HTTP handlers in
// the mock F5 host that use files uploaded via SSH.
return exec.Command("true")
}
type Route struct {
Name string
Method string
Pattern string
HandlerFunc func(mockF5State) http.HandlerFunc
}
var f5Routes = []Route{
{"getPolicy", "GET", "/mgmt/tm/ltm/policy/{policyName}", getPolicyHandler},
{"postPolicy", "POST", "/mgmt/tm/ltm/policy", postPolicyHandler},
{"postRule", "POST", "/mgmt/tm/ltm/policy/{policyName}/rules", postRuleHandler},
{"getPolicies", "GET", "/mgmt/tm/ltm/virtual/{vserverName}/policies", getPoliciesHandler},
{"associatePolicyWithVserver", "POST", "/mgmt/tm/ltm/virtual/{vserverName}/policies", associatePolicyWithVserverHandler},
{"getDatagroup", "GET", "/mgmt/tm/ltm/data-group/internal/{datagroupName}", getDatagroupHandler},
{"patchDatagroup", "PATCH", "/mgmt/tm/ltm/data-group/internal/{datagroupName}", patchDatagroupHandler},
{"postDatagroup", "POST", "/mgmt/tm/ltm/data-group/internal", postDatagroupHandler},
{"getIRule", "GET", "/mgmt/tm/ltm/rule/{iRuleName}", getIRuleHandler},
{"postIRule", "POST", "/mgmt/tm/ltm/rule", postIRuleHandler},
{"getVserver", "GET", "/mgmt/tm/ltm/virtual/{vserverName}", getVserverHandler},
{"patchVserver", "PATCH", "/mgmt/tm/ltm/virtual/{vserverName}", patchVserverHandler},
{"getPartition", "GET", "/mgmt/tm/sys/folder/{partitionPath}", getPartitionPath},
{"postPartition", "POST", "/mgmt/tm/sys/folder", postPartitionPathHandler},
{"postPool", "POST", "/mgmt/tm/ltm/pool", postPoolHandler},
{"deletePool", "DELETE", "/mgmt/tm/ltm/pool/{poolName}", deletePoolHandler},
{"getPoolMembers", "GET", "/mgmt/tm/ltm/pool/{poolName}/members", getPoolMembersHandler},
{"postPoolMember", "POST", "/mgmt/tm/ltm/pool/{poolName}/members", postPoolMemberHandler},
{"deletePoolMember", "DELETE", "/mgmt/tm/ltm/pool/{poolName}/members/{memberName}", deletePoolMemberHandler},
{"getRules", "GET", "/mgmt/tm/ltm/policy/{policyName}/rules", getRulesHandler},
{"postCondition", "POST", "/mgmt/tm/ltm/policy/{policyName}/rules/{ruleName}/conditions", postConditionHandler},
{"postAction", "POST", "/mgmt/tm/ltm/policy/{policyName}/rules/{ruleName}/actions", postActionHandler},
{"deleteRule", "DELETE", "/mgmt/tm/ltm/policy/{policyName}/rules/{ruleName}", deleteRuleHandler},
{"postSslCert", "POST", "/mgmt/tm/sys/crypto/cert", postSslCertHandler},
{"postSslKey", "POST", "/mgmt/tm/sys/crypto/key", postSslKeyHandler},
{"postClientSslProfile", "POST", "/mgmt/tm/ltm/profile/client-ssl", postClientSslProfileHandler},
{"deleteClientSslProfile", "DELETE", "/mgmt/tm/ltm/profile/client-ssl/{profileName}", deleteClientSslProfileHandler},
{"postServerSslProfile", "POST", "/mgmt/tm/ltm/profile/server-ssl", postServerSslProfileHandler},
{"deleteServerSslProfile", "DELETE", "/mgmt/tm/ltm/profile/server-ssl/{profileName}", deleteServerSslProfileHandler},
{"associateProfileWithVserver", "POST", "/mgmt/tm/ltm/virtual/{vserverName}/profiles", associateProfileWithVserver},
{"deleteSslVserverProfile", "DELETE", "/mgmt/tm/ltm/virtual/{vserverName}/profiles/{profileName}", deleteSslVserverProfileHandler},
{"deleteSslKey", "DELETE", "/mgmt/tm/sys/file/ssl-key/{keyName}", deleteSslKeyHandler},
{"deleteSslCert", "DELETE", "/mgmt/tm/sys/file/ssl-cert/{certName}", deleteSslCertHandler},
}
func newF5Routes(mockF5State mockF5State) *mux.Router {
mockF5 := mux.NewRouter().StrictSlash(true)
for _, route := range f5Routes {
mockF5.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(route.HandlerFunc(mockF5State))
}
return mockF5
}
// newTestRouterWithState creates a new F5 plugin with a mock F5 BIG-IP server
// initialized from the given mock F5 state and returns pointers to the plugin
// and mock server. Note that these pointers will be nil if an error is
// returned.
func newTestRouterWithState(state mockF5State, partitionPath string) (*F5Plugin, *mockF5, error) {
routerLogLevel := util.Env("TEST_ROUTER_LOGLEVEL", "")
if routerLogLevel != "" {
flag.Set("v", routerLogLevel)
}
execCommand = mockExecCommand
server := httptest.NewTLSServer(newF5Routes(state))
url, err := url.Parse(server.URL)
if err != nil {
return nil, nil,
fmt.Errorf("Failed to parse URL of mock F5 host; URL: %s, error: %v",
url, err)
}
f5PluginTestCfg := F5PluginConfig{
Host: url.Host,
Username: "admin",
Password: "password",
HttpVserver: httpVserverName,
HttpsVserver: httpsVserverName,
PrivateKey: "/dev/null",
Insecure: true,
PartitionPath: partitionPath,
}
router, err := NewF5Plugin(f5PluginTestCfg)
if err != nil {
return nil, nil, fmt.Errorf("Failed to create new F5 router: %v", err)
}
mockF5 := &mockF5{state: state, server: server}
return router, mockF5, nil
}
// newTestRouter creates a new F5 plugin with a mock F5 BIG-IP server and
// returns pointers to the plugin and mock server. Note that these pointers
// will be nil if an error is returned.
func newTestRouter(partitionPath string) (*F5Plugin, *mockF5, error) {
pathKey := strings.Replace(partitionPath, "/", "~", -1)
state := mockF5State{
policies: map[string]map[string]policyRule{},
vserverPolicies: map[string]map[string]bool{
httpVserverName: {},
httpsVserverName: {},
},
certs: map[string]bool{},
keys: map[string]bool{},
serverSslProfiles: map[string]bool{},
clientSslProfiles: map[string]bool{},
vserverProfiles: map[string]map[string]bool{
httpsVserverName: {},
},
datagroups: map[string]datagroup{},
iRules: map[string]iRule{},
vserverIRules: map[string][]string{},
// Add the default /Common partition path.
partitionPaths: map[string]string{pathKey: partitionPath},
pools: map[string]pool{},
}
return newTestRouterWithState(state, partitionPath)
}
func (f5 *mockF5) close() {
f5.server.Close()
}
func validatePolicyName(response http.ResponseWriter, request *http.Request,
f5state mockF5State, policyName string) bool {
_, ok := f5state.policies[policyName]
if !ok {
response.WriteHeader(http.StatusNotFound)
fmt.Fprintf(response,
`{"code":404,"errorStack":[],"message":"01020036:3: The requested Policy (/Common/%s) was not found."}`,
policyName)
return false
}
return true
}
func getPolicyHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
policyName := vars["policyName"]
if !validatePolicyName(response, request, f5state, policyName) {
return
}
fmt.Fprintf(response,
`{"controls":["forwarding"],"fullPath":"%s","generation":1,"kind":"tm:ltm:policy:policystate","name":"%s","requires":["http"],"rulesReference":{"isSubcollection":true,"link":"https://localhost/mgmt/tm/ltm/policy/~Common~%s/rules?ver=11.6.0"},"selfLink":"https://localhost/mgmt/tm/ltm/policy/%s?ver=11.6.0","strategy":"/Common/best-match"}`,
policyName, policyName, policyName, policyName)
}
}
func OK(response http.ResponseWriter) {
fmt.Fprint(response, `{"code":200,"message":"OK"}`)
}
func postPolicyHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
payload := struct {
Name string `json:"name"`
}{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(&payload)
policyName := payload.Name
f5state.policies[policyName] = map[string]policyRule{}
OK(response)
}
}
func postRuleHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
policyName := vars["policyName"]
if !validatePolicyName(response, request, f5state, policyName) {
return
}
payload := struct {
Name string `json:"name"`
}{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(&payload)
ruleName := payload.Name
newRule := policyRule{[]policyCondition{}}
f5state.policies[policyName][ruleName] = newRule
OK(response)
}
}
func validateVserverName(response http.ResponseWriter, request *http.Request,
f5state mockF5State, vserverName string) bool {
if !recogniseVserver(vserverName) {
response.WriteHeader(http.StatusNotFound)
fmt.Fprintf(response,
`{"code":404,"errorStack":[],"message":"01020036:3: The requested Virtual Server (/Common/%s) was not found."}`,
vserverName)
return false
}
return true
}
func getPoliciesHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
vserverName := vars["vserverName"]
if !validateVserverName(response, request, f5state, vserverName) {
return
}
fmt.Fprint(response, `{"items":[{"controls":["classification"],"fullPath":"/Common/_sys_CEC_SSL_client_policy","generation":1,"hints":["no-write","no-delete","no-exclusion"],"kind":"tm:ltm:policy:policystate","name":"_sys_CEC_SSL_client_policy","partition":"Common","requires":["ssl-persistence"],"rulesReference":{"isSubcollection":true,"link":"https://localhost/mgmt/tm/ltm/policy/~Common~_sys_CEC_SSL_client_policy/rules?ver=11.6.0"},"selfLink":"https://localhost/mgmt/tm/ltm/policy/~Common~_sys_CEC_SSL_client_policy?ver=11.6.0","strategy":"/Common/first-match"},{"controls":["classification"],"fullPath":"/Common/_sys_CEC_SSL_server_policy","generation":1,"hints":["no-write","no-delete","no-exclusion"],"kind":"tm:ltm:policy:policystate","name":"_sys_CEC_SSL_server_policy","partition":"Common","requires":["ssl-persistence"],"rulesReference":{"isSubcollection":true,"link":"https://localhost/mgmt/tm/ltm/policy/~Common~_sys_CEC_SSL_server_policy/rules?ver=11.6.0"},"selfLink":"https://localhost/mgmt/tm/ltm/policy/~Common~_sys_CEC_SSL_server_policy?ver=11.6.0","strategy":"/Common/first-match"},{"controls":["classification"],"fullPath":"/Common/_sys_CEC_video_policy","generation":1,"hints":["no-write","no-delete","no-exclusion"],"kind":"tm:ltm:policy:policystate","name":"_sys_CEC_video_policy","partition":"Common","requires":["http"],"rulesReference":{"isSubcollection":true,"link":"https://localhost/mgmt/tm/ltm/policy/~Common~_sys_CEC_video_policy/rules?ver=11.6.0"},"selfLink":"https://localhost/mgmt/tm/ltm/policy/~Common~_sys_CEC_video_policy?ver=11.6.0","strategy":"/Common/first-match"}`)
for policyName := range f5state.vserverPolicies[vserverName] {
fmt.Fprintf(response,
`,{"controls":["forwarding"],"fullPath":"/Common/%s","generation":1,"kind":"tm:ltm:policy:policystate","name":"%s","partition":"Common","requires":["http"],"rulesReference":{"isSubcollection":true,"link":"https://localhost/mgmt/tm/ltm/policy/~Common~%s/rules?ver=11.6.0"},"selfLink":"https://localhost/mgmt/tm/ltm/policy/~Common~%s?ver=11.6.0","strategy":"/Common/best-match"}`,
policyName, policyName, policyName, policyName)
}
fmt.Fprintf(response, `],"kind":"tm:ltm:policy:policycollectionstate","selfLink":"https://localhost/mgmt/tm/ltm/policy?ver=11.6.0"}`)
}
}
func recogniseVserver(vserverName string) bool {
return vserverName == httpVserverName || vserverName == httpsVserverName
}
func associatePolicyWithVserverHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
vserverName := vars["vserverName"]
payload := struct {
Name string `json:"name"`
}{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(&payload)
policyName := payload.Name
validVserver := recogniseVserver(vserverName)
_, validPolicy := f5state.policies[policyName]
if !validVserver || !validPolicy {
response.WriteHeader(http.StatusNotFound)
}
if !validVserver && !validPolicy {
fmt.Fprintf(response,
`{"code":400,"errorStack":[],"message":"01070712:3: Values (%s) specified for virtual server policy (/Common/%s %s): foreign key index (policy_FK) do not point at an item that exists in the database."}`,
policyName, vserverName, policyName)
return
}
if !validVserver {
fmt.Fprintf(response,
`{"code":400,"errorStack":[],"message":"01070712:3: Values (/Common/%s) specified for virtual server policy (/Common/%s /Common/%s): foreign key index (vs_FK) do not point at an item that exists in the database."}`,
vserverName, vserverName, policyName)
return
}
if !validPolicy {
fmt.Fprintf(response,
`{"code":404,"errorStack":[],"message":"01020036:3: The requested policy (%s) was not found."}`,
policyName)
return
}
f5state.vserverPolicies[vserverName][policyName] = true
OK(response)
}
}
func validateDatagroupName(response http.ResponseWriter, request *http.Request,
f5state mockF5State, datagroupName string) bool {
_, ok := f5state.datagroups[datagroupName]
if !ok {
response.WriteHeader(http.StatusNotFound)
fmt.Fprintf(response,
`{"code":404,"errorStack":[],"message":"01020036:3: The requested value list (/Common/%s) was not found."}`,
datagroupName)
return false
}
return true
}
func getDatagroupHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
datagroupName := vars["datagroupName"]
if !validateDatagroupName(response, request, f5state, datagroupName) {
return
}
datagroup := f5state.datagroups[datagroupName]
fmt.Fprintf(response,
`{"fullPath":"%s","generation":1556,"kind":"tm:ltm:data-group:internal:internalstate","name":"%s","records":[`,
datagroupName, datagroupName)
first := true
for key, value := range datagroup {
if first {
first = false
} else {
fmt.Fprintf(response, ",")
}
fmt.Fprintf(response, `{"data":"%s","name":"%s"}`, value, key)
}
fmt.Fprintf(response,
`],"selfLink":"https://localhost/mgmt/tm/ltm/data-group/internal/%s?ver=11.6.0","type":"string"}`,
datagroupName)
}
}
func patchDatagroupHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
datagroupName := vars["datagroupName"]
if !validateDatagroupName(response, request, f5state, datagroupName) {
return
}
type record struct {
Key string `json:"name"`
Value string `json:"data"`
}
payload := struct {
Records []record `json:"records"`
}{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(&payload)
dg := datagroup{}
for _, record := range payload.Records {
dg[record.Key] = record.Value
}
f5state.datagroups[datagroupName] = dg
OK(response)
}
}
func postDatagroupHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
payload := struct {
Name string `json:"name"`
}{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(&payload)
datagroupName := payload.Name
_, datagroupAlreadyExists := f5state.datagroups[datagroupName]
if datagroupAlreadyExists {
response.WriteHeader(http.StatusConflict)
fmt.Fprintf(response,
`{"code":409,"errorStack":[],"message":"01020066:3: The requested value list (/Common/%s) already exists in partition Common."}`,
datagroupName)
return
}
f5state.datagroups[datagroupName] = map[string]string{}
OK(response)
}
}
func getIRuleHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
iRuleName := vars["iRuleName"]
iRuleCode, ok := f5state.iRules[iRuleName]
if !ok {
response.WriteHeader(http.StatusNotFound)
fmt.Fprintf(response,
`{"code":404,"errorStack":[],"message":"01020036:3: The requested iRule (/Common/%s) was not found."}`,
iRuleName)
return
}
fmt.Fprintf(response,
`{"apiAnonymous":"%s","fullPath":"%s","generation":386,"kind":"tm:ltm:rule:rulestate","name":"%s","selfLink":"https://localhost/mgmt/tm/ltm/rule/%s?ver=11.6.0"}`,
iRuleCode, iRuleName, iRuleName, iRuleName)
}
}
func postIRuleHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
payload := struct {
Name string `json:"name"`
Code string `json:"apiAnonymous"`
}{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(&payload)
iRuleName := payload.Name
iRuleCode := payload.Code
_, iRuleAlreadyExists := f5state.iRules[iRuleName]
if iRuleAlreadyExists {
response.WriteHeader(http.StatusConflict)
fmt.Fprintf(response,
`{"code":409,"errorStack":[],"message":"01020066:3: The requested iRule (/Common/%s) already exists in partition Common."}`,
iRuleName)
return
}
// F5 iControl REST does not know how to parse \u escapes.
if badCharIdx := strings.Index(string(iRuleCode), `\u`); badCharIdx != -1 {
response.WriteHeader(http.StatusBadRequest)
truncateAt := badCharIdx
if truncateAt > 86 {
truncateAt = 86
}
fmt.Fprintf(response,
`{"code":400,"message":"can't parse TCL script beginning with\n%.*s\n","errorStack":[]}`,
truncateAt, iRuleCode)
return
}
f5state.iRules[iRuleName] = iRule(iRuleCode)
OK(response)
}
}
func getVserverHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
vserverName := vars["vserverName"]
if !validateVserverName(response, request, f5state, vserverName) {
return
}
description := "OpenShift Enterprise Virtual Server for HTTPS connections"
destination := "10.1.1.1:443"
if vserverName == httpVserverName {
description = "OpenShift Enterprise Virtual Server for HTTP connections"
destination = "10.1.1.2:80"
}
fmt.Fprintf(response,
`{"addressStatus":"yes","autoLasthop":"default","cmpEnabled":"yes","connectionLimit":0,"description":"%s","destination":"/Common/%s","enabled":true,"fullPath":"%s","generation":387,"gtmScore":0,"ipProtocol":"tcp","kind":"tm:ltm:virtual:virtualstate","mask":"255.255.255.255","mirror":"disabled","mobileAppTunnel":"disabled","name":"%s","nat64":"disabled","policiesReference":{"isSubcollection":true,"link":"https://localhost/mgmt/tm/ltm/virtual/~Common~%s/policies?ver=11.6.0"},"profilesReference":{"isSubcollection":true,"link":"https://localhost/mgmt/tm/ltm/virtual/~Common~%s/profiles?ver=11.6.0"},"rateLimit":"disabled","rateLimitDstMask":0,"rateLimitMode":"object","rateLimitSrcMask":0,"rules":[`,
description, destination, vserverName,
vserverName, vserverName, vserverName)
first := true
for _, ruleName := range f5state.vserverIRules[vserverName] {
if first {
first = false
} else {
fmt.Fprintf(response, ",")
}
fmt.Fprintf(response, `"/Common/%s"`, ruleName)
}
fmt.Fprintf(response, `],"selfLink":"https://localhost/mgmt/tm/ltm/virtual/%s?ver=11.6.0","source":"0.0.0.0/0","sourceAddressTranslation":{"type":"none"},"sourcePort":"preserve","synCookieStatus":"not-activated","translateAddress":"enabled","translatePort":"enabled","vlansDisabled":true,"vsIndex":11}`, vserverName)
}
}
func patchVserverHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
vserverName := vars["vserverName"]
if !validateVserverName(response, request, f5state, vserverName) {
return
}
payload := struct {
Rules []string `json:"rules"`
}{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(&payload)
iRules := []string(payload.Rules)
f5state.vserverIRules[vserverName] = iRules
OK(response)
}
}
func validatePartitionPath(response http.ResponseWriter, request *http.Request,
f5state mockF5State, partitionPath string) bool {
_, ok := f5state.partitionPaths[partitionPath]
if !ok {
response.WriteHeader(http.StatusNotFound)
fmt.Fprintf(response,
`{"code":404,"errorStack":[],"message":"01020036:3: The requested folder (%s) was not found."}`,
partitionPath)
return false
}
return true
}
func getPartitionPath(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
partitionPath := vars["partitionPath"]
if !validatePartitionPath(response, request, f5state, partitionPath) {
return
}
fullPath := f5state.partitionPaths[partitionPath]
parts := strings.Split(fullPath, "/")
partitionName := parts[0]
if len(parts) > 1 {
partitionName = parts[1]
}
fmt.Fprintf(response,
`{"deviceGroup":"%s/ose-sync-failover","fullPath":"%s","generation":580,"hidden":"false","inheritedDevicegroup":"true","inheritedTrafficGroup":"true","kind":"tm:sys:folder:folderstate","name":"%s","noRefCheck":"false","selfLink":"https://localhost/mgmt/tm/sys/folder/%s?ver=11.6.0","subPath":"/"}`,
fullPath, fullPath, partitionName, partitionPath)
}
}
func postPartitionPathHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
payload := struct {
Name string `json:"name"`
}{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(&payload)
partitionPath := payload.Name
// Convert / form to ~ form and add it to the map. This
// makes the GETs simpler: check/get the key in the map.
pathKey := strings.Replace(partitionPath, "/", "~", -1)
f5state.partitionPaths[pathKey] = partitionPath
OK(response)
}
}
func postPoolHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
payload := struct {
Name string `json:"name"`
}{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(&payload)
poolName := payload.Name
_, poolAlreadyExists := f5state.pools[poolName]
if poolAlreadyExists {
response.WriteHeader(http.StatusConflict)
fmt.Fprintf(response,
`{"code":409,"errorStack":[],"message":"01020066:3: The requested Pool (/Common/%s) already exists in partition Common."}`,
poolName)
return
}
f5state.pools[poolName] = pool{}
OK(response)
}
}
func validatePoolName(response http.ResponseWriter, request *http.Request,
f5state mockF5State, poolName string) bool {
_, ok := f5state.pools[poolName]
if !ok {
response.WriteHeader(http.StatusNotFound)
fmt.Fprintf(response,
`{"code":404,"errorStack":[],"message":"01020036:3:The requested Pool (/Common/%s) was not found."}`,
poolName)
return false
}
return true
}
func deletePoolHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
poolName := vars["poolName"]
if !validatePoolName(response, request, f5state, poolName) {
return
}
// TODO: Validate that no rule references the pool.
delete(f5state.pools, poolName)
OK(response)
}
}
func getPoolMembersHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
poolName := vars["poolName"]
if !validatePoolName(response, request, f5state, poolName) {
return
}
fmt.Fprint(response, `{"items":[`)
first := true
for member := range f5state.pools[poolName] {
if first {
first = false
} else {
fmt.Fprintf(response, ",")
}
addr := strings.Split(member, ":")[0]
fmt.Fprintf(response,
`{"address":"%s","connectionLimit":0,"dynamicRatio":1,"ephemeral":"false","fqdn":{"autopopulate":"disabled"},"fullPath":"/Common/%s","generation":1190,"inheritProfile":"enabled","kind":"tm:ltm:pool:members:membersstate","logging":"disabled","monitor":"default","name":"%s","partition":"Common","priorityGroup":0,"rateLimit":"disabled","ratio":1,"selfLink":"https://localhost/mgmt/tm/ltm/pool/%s/members/~Common~%s?ver=11.6.0","session":"monitor-enabled","state":"up"}`,
addr, member, member, member, member)
}
fmt.Fprintf(response,
`],"kind":"tm:ltm:pool:members:memberscollectionstate","selfLink":"https://localhost/mgmt/tm/ltm/pool/%s/members?ver=11.6.0"}`,
poolName)
}
}
func postPoolMemberHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
poolName := vars["poolName"]
if !validatePoolName(response, request, f5state, poolName) {
return
}
payload := struct {
Member string `json:"name"`
}{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(&payload)
memberName := payload.Member
_, memberAlreadyExists := f5state.pools[poolName][memberName]
if memberAlreadyExists {
response.WriteHeader(http.StatusConflict)
fmt.Fprintf(response,
`{"code":409,"message":"01020066:3: The requested Pool Member (/Common/%s /Common/%s) already exists in partition Common.","errorStack":[]}`,
poolName, strings.Replace(memberName, ":", " ", 1))
return
}
f5state.pools[poolName][memberName] = true
OK(response)
}
}
func deletePoolMemberHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
poolName := vars["poolName"]
memberName := vars["memberName"]
if !validatePoolName(response, request, f5state, poolName) {
return
}
_, foundMember := f5state.pools[poolName][memberName]
if !foundMember {
fmt.Fprintf(response,
`{"code":404,"message":"01020036:3: The requested Pool Member (/Common/%s /Common/%s) was not found.","errorStack":[]}`,
poolName, strings.Replace(memberName, ":", " ", 1))
}
delete(f5state.pools[poolName], memberName)
OK(response)
}
}
func getRulesHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
policyName := vars["policyName"]
if !validatePolicyName(response, request, f5state, policyName) {
return
}
fmt.Fprint(response, `{"items": [`)
first := true
for ruleName := range f5state.policies[policyName] {
if first {
first = false
} else {
fmt.Fprintf(response, ",")
}
fmt.Fprintf(response,
`{"actionsReference":{"isSubcollection":true,"link":"https://localhost/mgmt/tm/ltm/policy/%s/rules/%s/actions?ver=11.6.0"},"conditionsReference":{"isSubcollection":true,"link":"https://localhost/mgmt/tm/ltm/policy/%s/rules/%s/conditions?ver=11.6.0"},"fullPath":"%s","generation":1218,"kind":"tm:ltm:policy:rules:rulesstate","name":"%s","ordinal":0,"selfLink":"https://localhost/mgmt/tm/ltm/policy/%s/rules/%s?ver=11.6.0"}`,
policyName, ruleName, policyName, ruleName,
ruleName, ruleName, policyName, ruleName)
}
fmt.Fprintf(response,
`],"kind":"tm:ltm:policy:rules:rulescollectionstate","selfLink":"https://localhost/mgmt/tm/ltm/policy/%s/rules?ver=11.6.0"}`,
policyName)
}
}
func validateRuleName(response http.ResponseWriter, request *http.Request,
f5state mockF5State, policyName, ruleName string) bool {
for rule := range f5state.policies[policyName] {
if rule == ruleName {
return true
}
}
response.WriteHeader(http.StatusNotFound)
fmt.Fprintf(response,
`{"code":404,"errorStack":[],"message":"01020036:3: The requested policy rule (/Common/%s %s) was not found."}`,
policyName, ruleName)
return false
}
func postConditionHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
policyName := vars["policyName"]
ruleName := vars["ruleName"]
if !validatePolicyName(response, request, f5state, policyName) {
return
}
foundRule := validateRuleName(response, request, f5state,
policyName, ruleName)
if !foundRule {
return
}
payload := policyCondition{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(&payload)
// TODO: Validate more fields in the payload: equals, request, maybe others.
conditions := f5state.policies[policyName][ruleName].conditions
conditions = append(conditions, payload)
f5state.policies[policyName][ruleName] = policyRule{conditions}
OK(response)
}
}
func postActionHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
policyName := vars["policyName"]
ruleName := vars["ruleName"]
if !validatePolicyName(response, request, f5state, policyName) {
return
}
foundRule := validateRuleName(response, request, f5state,
policyName, ruleName)
if !foundRule {
return
}
// TODO: Validate payload.
OK(response)
}
}
func deleteRuleHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
policyName := vars["policyName"]
ruleName := vars["ruleName"]
if !validatePolicyName(response, request, f5state, policyName) {
return
}
foundRule := validateRuleName(response, request, f5state,
policyName, ruleName)
if !foundRule {
return
}
delete(f5state.policies[policyName], ruleName)
OK(response)
}
}
func postSslCertHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
payload := struct {
Name string `json:"name"`
}{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(&payload)
certName := payload.Name
// TODO: Validate filename (which would require more elaborate mocking of
// the ssh and scp commands in mockExecCommand).
// F5 adds the extension to the filename specified in the payload, and this
// extension must be included in subsequent REST calls that reference the
// file.
f5state.certs[fmt.Sprintf("%s.crt", certName)] = true
OK(response)
}
}
func postSslKeyHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
payload := struct {
Name string `json:"name"`
}{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(&payload)
keyName := payload.Name
// TODO: Validate filename (which would require more elaborate mocking of
// the ssh and scp commands in mockExecCommand).
f5state.keys[fmt.Sprintf("%s.key", keyName)] = true
OK(response)
}
}
func validateClientKey(response http.ResponseWriter, request *http.Request,
f5state mockF5State, keyName string) bool {
_, ok := f5state.keys[keyName]
if !ok {
response.WriteHeader(http.StatusNotFound)
fmt.Fprintf(response,
`{"code":400,"message":"010717e3:3: Client SSL profile must have RSA certificate/key pair.","errorStack":[]}`)
return false
}
return true
}
func validateCert(response http.ResponseWriter, request *http.Request,
f5state mockF5State, certName string) bool {
_, ok := f5state.certs[certName]
if !ok {
response.WriteHeader(http.StatusNotFound)
fmt.Fprintf(response,
`{"code":400,"message":"0107134a:3: File object by name (%s) is missing.","errorStack":[]}`,
certName)
return false
}
return true
}
func postClientSslProfileHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
payload := struct {
CertificateName string `json:"cert"`
KeyName string `json:"key"`
Name string `json:"name"`
}{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(&payload)
keyName := payload.KeyName
certificateName := payload.CertificateName
clientSslProfileName := payload.Name
// Complain about name collision first because F5 does.
_, clientSslProfileAlreadyExists := f5state.clientSslProfiles[clientSslProfileName]
if clientSslProfileAlreadyExists {
response.WriteHeader(http.StatusConflict)
fmt.Fprintf(response,
`{"code":409,"message":"01020066:3: The requested ClientSSL Profile (/Common/%s) already exists in partition Common.","errorStack":[]`,
clientSslProfileName)
return
}
// Check key before certificate because if both are missing, F5 returns the
// same error message as if only the key were missing.
if !validateClientKey(response, request, f5state, keyName) {
return
}
if !validateCert(response, request, f5state, certificateName) {
return
}
// The name for a client-ssl profile cannot collide with the name of any
// server-ssl profile either, but F5 complains about name collisions with
// server-ssl profiles only if the above checks pass.
_, serverSslProfileAlreadyExists := f5state.serverSslProfiles[clientSslProfileName]
if serverSslProfileAlreadyExists {
response.WriteHeader(http.StatusConflict)
fmt.Fprintf(response,
`{"code":400,"message":"01070293:3: The profile name (/Common/%s) is already assigned to another profile.","errorStack":[]}`,
clientSslProfileName)
return
}
f5state.clientSslProfiles[clientSslProfileName] = true
OK(response)
}
}
func deleteClientSslProfileHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
clientSslProfileName := vars["profileName"]
_, clientSslProfileFound := f5state.clientSslProfiles[clientSslProfileName]
if !clientSslProfileFound {
response.WriteHeader(http.StatusNotFound)
fmt.Fprintf(response,
`{"code":404,"message":"01020036:3: The requested ClientSSL Profile (/Common/%s) was not found.","errorStack":[]}`,
clientSslProfileName)
return
}
delete(f5state.clientSslProfiles, clientSslProfileName)
OK(response)
}
}
func validateServerKey(response http.ResponseWriter, request *http.Request,
f5state mockF5State, keyName string) bool {
_, ok := f5state.keys[keyName]
if !ok {
response.WriteHeader(http.StatusNotFound)
fmt.Fprintf(response,
`{"code":400,"message":"0107134a:3: File object by name (%s) is missing.","errorStack":[]}`,
keyName)
return false
}
return true
}
func postServerSslProfileHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
payload := struct {
CertificateName string `json:"chain"`
Name string `json:"name"`
}{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(&payload)
certificateName := payload.CertificateName
serverSslProfileName := payload.Name
// Complain about name collision first because F5 does.
_, serverSslProfileAlreadyExists := f5state.serverSslProfiles[serverSslProfileName]
if serverSslProfileAlreadyExists {
response.WriteHeader(http.StatusConflict)
fmt.Fprintf(response,
`{"code":409,"message":"01020066:3: The requested ServerSSL Profile (/Common/%s) already exists in partition Common.","errorStack":[]}`,
serverSslProfileName)
return
}
_, clientSslProfileAlreadyExists := f5state.clientSslProfiles[serverSslProfileName]
if clientSslProfileAlreadyExists {
response.WriteHeader(http.StatusConflict)
fmt.Fprintf(response,
`{"code":400,"message":"01070293:3: The profile name (/Common/%s) is already assigned to another profile.","errorStack":[]}`,
serverSslProfileName)
return
}
if !validateCert(response, request, f5state, certificateName) {
return
}
f5state.serverSslProfiles[serverSslProfileName] = true
OK(response)
}
}
func deleteServerSslProfileHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
serverSslProfileName := vars["profileName"]
_, serverSslProfileFound := f5state.serverSslProfiles[serverSslProfileName]
if !serverSslProfileFound {
response.WriteHeader(http.StatusNotFound)
fmt.Fprintf(response,
`{"code":404,"message":"01020036:3: The requested ServerSSL Profile (/Common/%s) was not found.","errorStack":[]}`,
serverSslProfileName)
return
}
delete(f5state.serverSslProfiles, serverSslProfileName)
OK(response)
}
}
func associateProfileWithVserver(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
vserverName := vars["vserverName"]
if !validateVserverName(response, request, f5state, vserverName) {
return
}
payload := struct {
Name string `json:"name"`
}{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(&payload)
profileName := payload.Name
f5state.vserverProfiles[vserverName][profileName] = true
OK(response)
}
}
func deleteSslVserverProfileHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
vserverName := vars["vserverName"]
profileName := vars["profileName"]
if !validateVserverName(response, request, f5state, vserverName) {
return
}
delete(f5state.vserverProfiles[vserverName], profileName)
OK(response)
}
}
func deleteSslKeyHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
keyName := vars["keyName"]
_, keyFound := f5state.keys[keyName]
if !keyFound {
response.WriteHeader(http.StatusNotFound)
fmt.Fprintf(response,
`{"code":404,"message":"01020036:3: The requested Certificate Key File (/Common/%s) was not found.","errorStack":[]}`,
keyName)
return
}
// TODO: Validate that the key is not in use (which will require keeping
// track of more state).
//{"code":400,"message":"01071349:3: File object by name (/Common/%s) is in use.","errorStack":[]}
delete(f5state.keys, keyName)
OK(response)
}
}
func deleteSslCertHandler(f5state mockF5State) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
certName := vars["certName"]
_, certFound := f5state.certs[certName]
if !certFound {
response.WriteHeader(http.StatusNotFound)
fmt.Fprintf(response,
`{"code":404,"message":"01020036:3: The requested Certificate File (/Common/%s) was not found.","errorStack":[]}`,
certName)
return
}
// TODO: Validate that the key is not in use (which will require keeping
// track of more state).
//{"code":400,"message":"01071349:3: File object by name (/Common/openshift_route_default_route-reencrypt-https-cert.crt) is in use.","errorStack":[]}
delete(f5state.certs, certName)
OK(response)
}
}
// TestInitializeF5Plugin initializes the F5 plug-in with a mock unconfigured F5
// BIG-IP host and validates the configuration of the F5 BIG-IP host after the
// plug-in has performed its initialization.
func TestInitializeF5Plugin(t *testing.T) {
router, mockF5, err := newTestRouter(F5DefaultPartitionPath)
if err != nil {
t.Fatalf("Failed to initialize test router: %v", err)
}
defer mockF5.close()
// The policy for secure routes and the policy for insecure routes should
// exist.
expectedPolicies := []string{insecureRoutesPolicyName, secureRoutesPolicyName}
for _, policyName := range expectedPolicies {
_, ok := mockF5.state.policies[policyName]
if !ok {
t.Errorf("%s policy was not created; policies map: %v",
policyName, mockF5.state.policies)
}
}
// The HTTPS vserver should have the policy for secure routes associated.
foundSecureRoutesPolicy := false
for policyName := range mockF5.state.vserverPolicies[httpsVserverName] {
if policyName == secureRoutesPolicyName {
foundSecureRoutesPolicy = true
} else {
t.Errorf("Encountered unexpected policy associated to vserver %s: %s",
httpsVserverName, policyName)
}
}
if !foundSecureRoutesPolicy {
t.Errorf("%s policy was not associated with vserver %s.",
secureRoutesPolicyName, httpsVserverName)
}
// The HTTP vserver should have the policy for insecure routes associated.
foundInsecureRoutesPolicy := false
for policyName := range mockF5.state.vserverPolicies[httpVserverName] {
if policyName == insecureRoutesPolicyName {
foundInsecureRoutesPolicy = true
} else {
t.Errorf("Encountered unexpected policy associated to vserver %s: %s",
httpVserverName, policyName)
}
}
if !foundInsecureRoutesPolicy {
t.Errorf("%s policy was not associated with vserver %s.",
insecureRoutesPolicyName, httpVserverName)
}
// The datagroup for passthrough routes should exist.
foundPassthroughIRuleDatagroup := false
for datagroupName := range mockF5.state.datagroups {
if datagroupName == passthroughIRuleDatagroupName {
foundPassthroughIRuleDatagroup = true
}
}
if !foundPassthroughIRuleDatagroup {
t.Errorf("%s datagroup was not created.", passthroughIRuleDatagroupName)
}
// The passthrough iRule should exist and should reference the datagroup for
// passthrough routes.
foundPassthroughIRule := false
for iRuleName, iRuleCode := range mockF5.state.iRules {
if iRuleName == passthroughIRuleName {
foundPassthroughIRule = true
if !strings.Contains(string(iRuleCode), passthroughIRuleDatagroupName) {
t.Errorf("iRule for passthrough routes exists, but its body does not"+
" reference the datagroup for passthrough routes.\n"+
"iRule name: %s\nDatagroup name: %s\niRule code: %s",
iRuleName, passthroughIRuleDatagroupName, iRuleCode)
}
} else {
t.Errorf("Encountered unexpected iRule: %s", iRuleName)
}
}
if !foundPassthroughIRule {
t.Errorf("%s iRule was not created.", passthroughIRuleName)
}
// The HTTPS vserver should have the passthrough iRule associated.
foundPassthroughIRuleUnderVserver := false
for _, iRuleName := range mockF5.state.vserverIRules[httpsVserverName] {
if iRuleName == passthroughIRuleName {
foundPassthroughIRuleUnderVserver = true
} else {
t.Errorf("Encountered unexpected iRule associated with vserver %s: %s",
httpsVserverName, iRuleName)
}
}
if !foundPassthroughIRuleUnderVserver {
t.Errorf("%s iRule was not associated with vserver %s.",
passthroughIRuleName, httpsVserverName)
}
// The HTTP vserver should have no iRules associated.
if len(mockF5.state.vserverIRules[httpVserverName]) != 0 {
t.Errorf("Vserver %s has iRules associated: %v",
httpVserverName, mockF5.state.vserverIRules[httpVserverName])
}
// Initialization should be idempotent.
// Warning: This is off-label use of DeepCopy!
savedMockF5State, err := kapi.Scheme.DeepCopy(mockF5.state)
if err != nil {
t.Errorf("Failed to deepcopy mock F5 state for idempotency check: %v", err)
}
router.F5Client.Initialize()
if !reflect.DeepEqual(savedMockF5State, mockF5.state) {
t.Errorf("Initialize method should be idempotent but it is not.\n"+
"State after first initialization: %v\n"+
"State after second initialization: %v\n",
savedMockF5State, mockF5.state)
}
}
// TestF5PartitionPath creates an F5 router instance with a specific partition.
func TestF5RouterPartition(t *testing.T) {
testCases := []struct {
// name of the test
name string
// partition path.
partition string
}{
{
name: "Default/Common partition",
partition: "/Common",
},
{
name: "Custom partition",
partition: "/OSPartA",
},
{
name: "Sub partition",
partition: "/OSPartA/ShardOne",
},
{
name: "Layered sub partition",
partition: "/OSPartA/region1/zone4/Shard-7",
},
}
for _, tc := range testCases {
_, mockF5, err := newTestRouter(tc.partition)
if err != nil {
t.Fatalf("Test case %q failed to initialize test router: %v", tc.name, err)
}
name := strings.Replace(tc.partition, "/", "~", -1)
_, ok := mockF5.state.partitionPaths[name]
if !ok {
t.Fatalf("Test case %q missing partition key %s", tc.name, name)
}
mockF5.close()
}
}
// TestHandleEndpoints tests endpoint watch events and validates that the state
// of the F5 client object is as expected after each event.
func TestHandleEndpoints(t *testing.T) {
router, mockF5, err := newTestRouter(F5DefaultPartitionPath)
if err != nil {
t.Fatalf("Failed to initialize test router: %v", err)
}
defer mockF5.close()
testCases := []struct {
// name is a human readable name for the test case.
name string
// type is the type to be passed to the HandleEndpoints method.
eventType watch.EventType
// endpoints is the set of endpoints to be passed to the HandleEndpoints
// method.
endpoints *kapi.Endpoints
// validate checks the state of the F5Plugin object and returns a Boolean
// indicating whether the state is as expected.
validate func() error
}{
{
name: "Endpoint add",
eventType: watch.Added,
endpoints: &kapi.Endpoints{
ObjectMeta: kapi.ObjectMeta{
Namespace: "foo",
Name: "test",
},
Subsets: []kapi.EndpointSubset{{
Addresses: []kapi.EndpointAddress{{IP: "1.1.1.1"}},
Ports: []kapi.EndpointPort{{Port: 345}},
}}, // Specifying no port implies port 80.
},
validate: func() error {
poolName := "openshift_foo_test"
poolExists, err := router.F5Client.PoolExists(poolName)
if err != nil {
return fmt.Errorf("PoolExists(%s) failed: %v", poolName, err)
}
if poolExists != true {
return fmt.Errorf("PoolExists(%s) returned %v instead of true",
poolName, poolExists)
}
memberName := "1.1.1.1:345"
poolHasMember, err := router.F5Client.PoolHasMember(poolName, memberName)
if err != nil {
return fmt.Errorf("PoolHasMember(%s, %s) failed: %v",
poolName, memberName, err)
}
if poolHasMember != true {
return fmt.Errorf("PoolHasMember(%s, %s) returned %v instead of true",
poolName, memberName, poolHasMember)
}
return nil
},
},
{
name: "Endpoint modify",
eventType: watch.Modified,
endpoints: &kapi.Endpoints{
ObjectMeta: kapi.ObjectMeta{
Namespace: "foo",
Name: "test",
},
Subsets: []kapi.EndpointSubset{{
Addresses: []kapi.EndpointAddress{{IP: "2.2.2.2"}},
Ports: []kapi.EndpointPort{{Port: 8080}},
}},
},
validate: func() error {
poolName := "openshift_foo_test"
poolExists, err := router.F5Client.PoolExists(poolName)
if err != nil {
return fmt.Errorf("PoolExists(%s) failed: %v", poolName, err)
}
if poolExists != true {
return fmt.Errorf("PoolExists(%s) returned %v instead of true",
poolName, poolExists)
}
memberName := "2.2.2.2:8080"
poolHasMember, err := router.F5Client.PoolHasMember(poolName, memberName)
if err != nil {
return fmt.Errorf("PoolHasMember(%s, %s) failed: %v",
poolName, memberName, err)
}
if poolHasMember != true {
return fmt.Errorf("PoolHasMember(%s, %s) returned %v instead of true",
poolName, memberName, poolHasMember)
}
return nil
},
},
{
name: "Endpoint delete",
eventType: watch.Modified,
endpoints: &kapi.Endpoints{
ObjectMeta: kapi.ObjectMeta{
Namespace: "foo",
Name: "test",
},
Subsets: []kapi.EndpointSubset{},
},
validate: func() error {
poolName := "openshift_foo_test"
poolExists, err := router.F5Client.PoolExists(poolName)
if err != nil {
return fmt.Errorf("PoolExists(%s) failed: %v", poolName, err)
}
if poolExists != false {
return fmt.Errorf("PoolExists(%s) returned %v instead of false",
poolName, poolExists)
}
return nil
},
},
}
for _, tc := range testCases {
router.HandleEndpoints(tc.eventType, tc.endpoints)
err := tc.validate()
if err != nil {
t.Errorf("Test case %s failed: %v", tc.name, err)
}
}
}
// TestHandleRoute test route watch events and validates that the state of the
// F5 client object is as expected after each event.
func TestHandleRoute(t *testing.T) {
router, mockF5, err := newTestRouter(F5DefaultPartitionPath)
if err != nil {
t.Fatalf("Failed to initialize test router: %v", err)
}
defer mockF5.close()
type testCase struct {
// name is a human readable name for the test case.
name string
// type is the type to be passed to the HandleRoute method.
eventType watch.EventType
// route specifies the route object to be passed to the
// HandleRoute method.
route *routeapi.Route
// validate checks the state of the F5Plugin
// object and returns a Boolean
// indicating whether the state is as
// expected.
validate func(tc testCase) error
}
testCases := []testCase{
{
name: "Unsecure route add",
eventType: watch.Added,
route: &routeapi.Route{
ObjectMeta: kapi.ObjectMeta{
Namespace: "foo",
Name: "unsecuretest",
},
Spec: routeapi.RouteSpec{
Host: "www.example.com",
To: routeapi.RouteTargetReference{
Name: "TestService",
},
},
},
validate: func(tc testCase) error {
rulename := routeName(*tc.route)
rule, ok := mockF5.state.policies[insecureRoutesPolicyName][rulename]
if !ok {
return fmt.Errorf("Policy %s should have rule %s,"+
" but no rule was found: %v",
insecureRoutesPolicyName, rulename,
mockF5.state.policies[insecureRoutesPolicyName])
}
if len(rule.conditions) != 1 {
return fmt.Errorf("Insecure route should have rule with 1 condition,"+
" but rule has %d conditions: %v",
len(rule.conditions), rule.conditions)
}
condition := rule.conditions[0]
if !(condition.HttpHost && condition.Host && !condition.HttpUri &&
!condition.PathSegment) {
return fmt.Errorf("Insecure route should have rule that matches on"+
" hostname but found this instead: %v", rule.conditions)
}
if len(condition.Values) != 1 {
return fmt.Errorf("Insecure route rule condition should have 1 value"+
" but has %d values: %v", len(condition.Values), condition)
}
if condition.Values[0] != tc.route.Spec.Host {
return fmt.Errorf("Insecure route rule condition should match on"+
" hostname %s, but it has a different value: %v",
tc.route.Spec.Host, condition)
}
return nil
},
},
{
name: "Unsecure route modify",
eventType: watch.Modified,
route: &routeapi.Route{
ObjectMeta: kapi.ObjectMeta{
Namespace: "foo",
Name: "unsecuretest",
},
Spec: routeapi.RouteSpec{
Host: "www.example2.com",
To: routeapi.RouteTargetReference{
Name: "TestService",
},
Path: "/foo/bar",
},
},
validate: func(tc testCase) error {
rulename := routeName(*tc.route)
rule, ok := mockF5.state.policies[insecureRoutesPolicyName][rulename]
if !ok {
return fmt.Errorf("Policy %s should have rule %s,"+
" but no rule was found: %v",
insecureRoutesPolicyName, rulename,
mockF5.state.policies[insecureRoutesPolicyName])
}
if len(rule.conditions) != 3 {
return fmt.Errorf("Insecure route with pathname should have rule"+
" with 3 conditions, but rule has %d conditions: %v",
len(rule.conditions), rule.conditions)
}
pathSegments := strings.Split(tc.route.Spec.Path, "/")
for _, condition := range rule.conditions {
if !(condition.PathSegment && condition.HttpUri) {
continue
}
expectedValue := pathSegments[condition.Index]
foundValue := condition.Values[0]
if foundValue != expectedValue {
return fmt.Errorf("Rule condition with index %d for insecure route"+
" with pathname %s should have value \"%s\" but has value"+
" \"%s\": %v",
condition.Index, tc.route.Spec.Path, expectedValue, foundValue, rule)
}
}
return nil
},
},
{
name: "Unsecure route delete",
eventType: watch.Deleted,
route: &routeapi.Route{
ObjectMeta: kapi.ObjectMeta{
Namespace: "foo",
Name: "unsecuretest",
},
Spec: routeapi.RouteSpec{
Host: "www.example2.com",
To: routeapi.RouteTargetReference{
Name: "TestService",
},
},
},
validate: func(tc testCase) error {
rulename := routeName(*tc.route)
_, found := mockF5.state.policies[secureRoutesPolicyName][rulename]
if found {
return fmt.Errorf("Rule %s should have been deleted from policy %s"+
" when the corresponding route was deleted, but it remains yet: %v",
rulename, secureRoutesPolicyName,
mockF5.state.policies[secureRoutesPolicyName])
}
return nil
},
},
{
name: "Edge route add",
eventType: watch.Added,
route: &routeapi.Route{
ObjectMeta: kapi.ObjectMeta{
Namespace: "foo",
Name: "edgetest",
},
Spec: routeapi.RouteSpec{
Host: "www.example.com",
To: routeapi.RouteTargetReference{
Name: "TestService",
},
TLS: &routeapi.TLSConfig{
Termination: routeapi.TLSTerminationEdge,
Certificate: "abc",
Key: "def",
},
},
},
validate: func(tc testCase) error {
rulename := routeName(*tc.route)
_, found := mockF5.state.policies[secureRoutesPolicyName][rulename]
if !found {
return fmt.Errorf("Policy %s should have rule %s,"+
" but no such rule was found: %v",
secureRoutesPolicyName, rulename,
mockF5.state.policies[secureRoutesPolicyName])
}
certfname := fmt.Sprintf("%s-https-cert.crt", rulename)
_, found = mockF5.state.certs[certfname]
if !found {
return fmt.Errorf("Certificate file %s should have been created but"+
" does not exist: %v",
certfname, mockF5.state.certs)
}
keyfname := fmt.Sprintf("%s-https-key.key", rulename)
_, found = mockF5.state.keys[keyfname]
if !found {
return fmt.Errorf("Key file %s should have been created but"+
" does not exist: %v",
keyfname, mockF5.state.keys)
}
clientSslProfileName := fmt.Sprintf("%s-client-ssl-profile", rulename)
_, found = mockF5.state.clientSslProfiles[clientSslProfileName]
if !found {
return fmt.Errorf("client-ssl profile %s should have been created"+
" but does not exist: %v",
clientSslProfileName, mockF5.state.clientSslProfiles)
}
_, found = mockF5.state.vserverProfiles[httpsVserverName][clientSslProfileName]
if !found {
return fmt.Errorf("client-ssl profile %s should have been"+
" associated with the vserver but was not: %v",
clientSslProfileName,
mockF5.state.vserverProfiles[httpsVserverName])
}
return nil
},
},
{
name: "Edge route delete",
eventType: watch.Deleted,
route: &routeapi.Route{
ObjectMeta: kapi.ObjectMeta{
Namespace: "foo",
Name: "edgetest",
},
Spec: routeapi.RouteSpec{
Host: "www.example.com",
To: routeapi.RouteTargetReference{
Name: "TestService",
},
TLS: &routeapi.TLSConfig{
Termination: routeapi.TLSTerminationEdge,
Certificate: "abc",
Key: "def",
},
},
},
validate: func(tc testCase) error {
rulename := routeName(*tc.route)
_, found := mockF5.state.policies[secureRoutesPolicyName][rulename]
if found {
return fmt.Errorf("Rule %s should have been deleted from policy %s"+
" when the corresponding route was deleted, but it remains yet: %v",
rulename, secureRoutesPolicyName,
mockF5.state.policies[secureRoutesPolicyName])
}
certfname := fmt.Sprintf("%s-https-cert.crt", rulename)
_, found = mockF5.state.certs[certfname]
if found {
return fmt.Errorf("Certificate file %s should have been deleted with"+
" the route but remains yet: %v",
certfname, mockF5.state.certs)
}
keyfname := fmt.Sprintf("%s-https-key.key", rulename)
_, found = mockF5.state.keys[keyfname]
if found {
return fmt.Errorf("Key file %s should have been deleted with the"+
" route but remains yet: %v",
keyfname, mockF5.state.keys)
}
clientSslProfileName := fmt.Sprintf("%s-client-ssl-profile", rulename)
_, found = mockF5.state.vserverProfiles[httpsVserverName][clientSslProfileName]
if found {
return fmt.Errorf("client-ssl profile %s should have been deleted"+
" from the vserver when the route was deleted but remains yet: %v",
clientSslProfileName,
mockF5.state.vserverProfiles[httpsVserverName])
}
_, found = mockF5.state.clientSslProfiles[clientSslProfileName]
if found {
return fmt.Errorf("client-ssl profile %s should have been deleted"+
" with the route but remains yet: %v",
clientSslProfileName, mockF5.state.clientSslProfiles)
}
return nil
},
},
{
name: "Passthrough route add",
eventType: watch.Added,
route: &routeapi.Route{
ObjectMeta: kapi.ObjectMeta{
Namespace: "foo",
Name: "passthroughtest",
},
Spec: routeapi.RouteSpec{
Host: "www.example3.com",
To: routeapi.RouteTargetReference{
Name: "TestService",
},
TLS: &routeapi.TLSConfig{
Termination: routeapi.TLSTerminationPassthrough,
},
},
},
validate: func(tc testCase) error {
_, found := mockF5.state.datagroups[passthroughIRuleDatagroupName][tc.route.Spec.Host]
if !found {
return fmt.Errorf("Datagroup entry for %s should have been created"+
" in the %s datagroup for the passthrough route but cannot be"+
" found: %v",
tc.route.Spec.Host, passthroughIRuleDatagroupName,
mockF5.state.datagroups[passthroughIRuleDatagroupName])
}
return nil
},
},
{
name: "Add route with same hostname as passthrough route",
eventType: watch.Added,
route: &routeapi.Route{
ObjectMeta: kapi.ObjectMeta{
Namespace: "foo",
Name: "conflictingroutetest",
},
Spec: routeapi.RouteSpec{
Host: "www.example3.com",
To: routeapi.RouteTargetReference{
Name: "TestService",
},
TLS: &routeapi.TLSConfig{
Termination: routeapi.TLSTerminationEdge,
Certificate: "abc",
Key: "def",
},
},
},
validate: func(tc testCase) error {
_, found := mockF5.state.datagroups[passthroughIRuleDatagroupName][tc.route.Spec.Host]
if !found {
return fmt.Errorf("Datagroup entry for %s should still exist"+
" in the %s datagroup after a secure route with the same hostname"+
" was created, but the datagroup entry cannot be found: %v",
tc.route.Spec.Host, passthroughIRuleDatagroupName,
mockF5.state.datagroups[passthroughIRuleDatagroupName])
}
return nil
},
},
{
name: "Modify route with same hostname as passthrough route",
eventType: watch.Modified,
route: &routeapi.Route{
ObjectMeta: kapi.ObjectMeta{
Namespace: "foo",
Name: "conflictingroutetest",
},
Spec: routeapi.RouteSpec{
Host: "www.example3.com",
To: routeapi.RouteTargetReference{
Name: "TestService",
},
},
},
validate: func(tc testCase) error {
_, found := mockF5.state.datagroups[passthroughIRuleDatagroupName][tc.route.Spec.Host]
if !found {
return fmt.Errorf("Datagroup entry for %s should still exist"+
" in the %s datagroup after a secure route with the same hostname"+
" was updated, but the datagroup entry cannot be found: %v",
tc.route.Spec.Host, passthroughIRuleDatagroupName,
mockF5.state.datagroups[passthroughIRuleDatagroupName])
}
return nil
},
},
{
name: "Delete route with same hostname as passthrough route",
eventType: watch.Deleted,
route: &routeapi.Route{
ObjectMeta: kapi.ObjectMeta{
Namespace: "foo",
Name: "conflictingroutetest",
},
Spec: routeapi.RouteSpec{
Host: "www.example3.com",
To: routeapi.RouteTargetReference{
Name: "TestService",
},
},
},
validate: func(tc testCase) error {
_, found := mockF5.state.datagroups[passthroughIRuleDatagroupName][tc.route.Spec.Host]
if !found {
return fmt.Errorf("Datagroup entry for %s should still exist"+
" in the %s datagroup after a secure route with the same hostname"+
" was deleted, but the datagroup entry cannot be found: %v",
tc.route.Spec.Host, passthroughIRuleDatagroupName,
mockF5.state.datagroups[passthroughIRuleDatagroupName])
}
return nil
},
},
{
name: "Passthrough route delete",
eventType: watch.Deleted,
route: &routeapi.Route{
ObjectMeta: kapi.ObjectMeta{
Namespace: "foo",
Name: "passthroughtest",
},
Spec: routeapi.RouteSpec{
Host: "www.example3.com",
To: routeapi.RouteTargetReference{
Name: "TestService",
},
TLS: &routeapi.TLSConfig{
Termination: routeapi.TLSTerminationPassthrough,
},
},
},
validate: func(tc testCase) error {
_, found := mockF5.state.datagroups[passthroughIRuleDatagroupName][tc.route.Spec.Host]
if found {
return fmt.Errorf("Datagroup entry for %s should have been deleted"+
" from the %s datagroup for the passthrough route but remains"+
" yet: %v",
tc.route.Spec.Host, passthroughIRuleDatagroupName,
mockF5.state.datagroups[passthroughIRuleDatagroupName])
}
return nil
},
},
{
name: "Reencrypted route add",
eventType: watch.Added,
route: &routeapi.Route{
ObjectMeta: kapi.ObjectMeta{
Namespace: "foo",
Name: "reencryptedtest",
},
Spec: routeapi.RouteSpec{
Host: "www.example4.com",
To: routeapi.RouteTargetReference{
Name: "TestService",
},
TLS: &routeapi.TLSConfig{
Termination: routeapi.TLSTerminationReencrypt,
Certificate: "abc",
Key: "def",
CACertificate: "ghi",
DestinationCACertificate: "jkl",
},
},
},
validate: func(tc testCase) error {
rulename := routeName(*tc.route)
_, found := mockF5.state.policies[secureRoutesPolicyName][rulename]
if !found {
return fmt.Errorf("Policy %s should have rule %s for secure route,"+
" but no rule was found: %v",
secureRoutesPolicyName, rulename,
mockF5.state.policies[secureRoutesPolicyName])
}
certcafname := fmt.Sprintf("%s-https-chain.crt", rulename)
_, found = mockF5.state.certs[certcafname]
if !found {
return fmt.Errorf("Certificate chain file %s should have been"+
" created but does not exist: %v",
certcafname, mockF5.state.certs)
}
keyfname := fmt.Sprintf("%s-https-key.key", rulename)
_, found = mockF5.state.keys[keyfname]
if !found {
return fmt.Errorf("Key file %s should have been created but"+
" does not exist: %v",
keyfname, mockF5.state.keys)
}
clientSslProfileName := fmt.Sprintf("%s-client-ssl-profile", rulename)
_, found = mockF5.state.clientSslProfiles[clientSslProfileName]
if !found {
return fmt.Errorf("client-ssl profile %s should have been created"+
" but does not exist: %v",
clientSslProfileName, mockF5.state.clientSslProfiles)
}
_, found = mockF5.state.vserverProfiles[httpsVserverName][clientSslProfileName]
if !found {
return fmt.Errorf("client-ssl profile %s should have been"+
" associated with the vserver but was not: %v",
clientSslProfileName,
mockF5.state.vserverProfiles[httpsVserverName])
}
serverSslProfileName := fmt.Sprintf("%s-server-ssl-profile", rulename)
_, found = mockF5.state.serverSslProfiles[serverSslProfileName]
if !found {
return fmt.Errorf("server-ssl profile %s should have been created"+
" but does not exist: %v",
serverSslProfileName, mockF5.state.serverSslProfiles)
}
_, found = mockF5.state.vserverProfiles[httpsVserverName][serverSslProfileName]
if !found {
return fmt.Errorf("server-ssl profile %s should have been"+
" associated with the vserver but was not: %v",
serverSslProfileName,
mockF5.state.vserverProfiles[httpsVserverName])
}
return nil
},
},
{
name: "Reencrypted route delete",
eventType: watch.Deleted,
route: &routeapi.Route{
ObjectMeta: kapi.ObjectMeta{
Namespace: "foo",
Name: "reencryptedtest",
},
Spec: routeapi.RouteSpec{
Host: "www.example4.com",
To: routeapi.RouteTargetReference{
Name: "TestService",
},
TLS: &routeapi.TLSConfig{
Termination: routeapi.TLSTerminationReencrypt,
Certificate: "abc",
Key: "def",
CACertificate: "ghi",
DestinationCACertificate: "jkl",
},
},
},
validate: func(tc testCase) error {
rulename := routeName(*tc.route)
_, found := mockF5.state.policies[secureRoutesPolicyName][rulename]
if found {
return fmt.Errorf("Rule %s should have been deleted from policy %s"+
" when the corresponding route was deleted, but it remains yet: %v",
rulename, secureRoutesPolicyName,
mockF5.state.policies[secureRoutesPolicyName])
}
certcafname := fmt.Sprintf("%s-https-chain.crt", rulename)
_, found = mockF5.state.certs[certcafname]
if found {
return fmt.Errorf("Certificate chain file %s should have been"+
" deleted with the route but remains yet: %v",
certcafname, mockF5.state.certs)
}
keyfname := fmt.Sprintf("%s-https-key.key", rulename)
_, found = mockF5.state.keys[keyfname]
if found {
return fmt.Errorf("Key file %s should have been deleted with the"+
" route but remains yet: %v",
keyfname, mockF5.state.keys)
}
clientSslProfileName := fmt.Sprintf("%s-client-ssl-profile", rulename)
_, found = mockF5.state.vserverProfiles[httpsVserverName][clientSslProfileName]
if found {
return fmt.Errorf("client-ssl profile %s should have been deleted"+
" from the vserver when the route was deleted but remains yet: %v",
clientSslProfileName,
mockF5.state.vserverProfiles[httpsVserverName])
}
serverSslProfileName := fmt.Sprintf("%s-server-ssl-profile", rulename)
_, found = mockF5.state.vserverProfiles[httpsVserverName][clientSslProfileName]
if found {
return fmt.Errorf("server-ssl profile %s should have been deleted"+
" from the vserver when the route was deleted but remains yet: %v",
serverSslProfileName,
mockF5.state.vserverProfiles[httpsVserverName])
}
_, found = mockF5.state.serverSslProfiles[serverSslProfileName]
if found {
return fmt.Errorf("server-ssl profile %s should have been deleted"+
" with the route but remains yet: %v",
serverSslProfileName, mockF5.state.serverSslProfiles)
}
return nil
},
},
}
for _, tc := range testCases {
router.HandleRoute(tc.eventType, tc.route)
err := tc.validate(tc)
if err != nil {
t.Errorf("Test case %s failed: %v", tc.name, err)
}
}
}
// TestHandleRouteModifications creates an F5 router instance, creates
// a service and a route, modifies the route in several ways, and verifies that
// the router correctly updates the route.
func TestHandleRouteModifications(t *testing.T) {
router, mockF5, err := newTestRouter(F5DefaultPartitionPath)
if err != nil {
t.Fatalf("Failed to initialize test router: %v", err)
}
defer mockF5.close()
testRoute := &routeapi.Route{
ObjectMeta: kapi.ObjectMeta{
Namespace: "foo",
Name: "mutatingroute",
},
Spec: routeapi.RouteSpec{
Host: "www.example.com",
To: routeapi.RouteTargetReference{
Name: "testendpoint",
},
},
}
err = router.HandleRoute(watch.Added, testRoute)
if err != nil {
t.Fatalf("HandleRoute failed on adding test route: %v", err)
}
// Verify that modifying the route into a secure route works.
testRoute.Spec.TLS = &routeapi.TLSConfig{
Termination: routeapi.TLSTerminationReencrypt,
Certificate: "abc",
Key: "def",
CACertificate: "ghi",
DestinationCACertificate: "jkl",
}
err = router.HandleRoute(watch.Modified, testRoute)
if err != nil {
t.Fatalf("HandleRoute failed on modifying test route: %v", err)
}
// Verify that updating the hostname of the route succeeds.
testRoute.Spec.Host = "www.example2.com"
err = router.HandleRoute(watch.Modified, testRoute)
if err != nil {
t.Fatalf("HandleRoute failed on modifying test route: %v", err)
}
// Verify that modifying the route into a passthrough route works.
testRoute.Spec.TLS = &routeapi.TLSConfig{
Termination: routeapi.TLSTerminationPassthrough,
}
err = router.HandleRoute(watch.Modified, testRoute)
if err != nil {
t.Fatalf("HandleRoute failed on modifying test route: %v", err)
}
// Verify that updating the hostname of the passthrough route succeeds.
testRoute.Spec.Host = "www.example3.com"
err = router.HandleRoute(watch.Modified, testRoute)
if err != nil {
t.Fatalf("HandleRoute failed on modifying test route: %v", err)
}
}
// TestF5RouterSuccessiveInstances creates an F5 router instance, creates
// a service and a route, creates a new F5 router instance, and verifies that
// the new instance behaves correctly picking up the state from the first
// instance.
func TestF5RouterSuccessiveInstances(t *testing.T) {
router, mockF5, err := newTestRouter(F5DefaultPartitionPath)
if err != nil {
t.Fatalf("Failed to initialize test router: %v", err)
}
testRoute := &routeapi.Route{
ObjectMeta: kapi.ObjectMeta{
Namespace: "xyzzy",
Name: "testroute",
},
Spec: routeapi.RouteSpec{
Host: "www.example.com",
To: routeapi.RouteTargetReference{
Name: "testendpoint",
},
TLS: &routeapi.TLSConfig{
Termination: routeapi.TLSTerminationReencrypt,
Certificate: "abc",
Key: "def",
CACertificate: "ghi",
DestinationCACertificate: "jkl",
},
},
}
testPassthroughRoute := &routeapi.Route{
ObjectMeta: kapi.ObjectMeta{
Namespace: "quux",
Name: "testpassthroughroute",
},
Spec: routeapi.RouteSpec{
Host: "www.example2.com",
To: routeapi.RouteTargetReference{
Name: "testhttpsendpoint",
},
TLS: &routeapi.TLSConfig{
Termination: routeapi.TLSTerminationPassthrough,
},
},
}
testHttpEndpoint := &kapi.Endpoints{
ObjectMeta: kapi.ObjectMeta{
Namespace: "xyzzy",
Name: "testhttpendpoint",
},
Subsets: []kapi.EndpointSubset{{
Addresses: []kapi.EndpointAddress{{IP: "10.1.1.1"}},
Ports: []kapi.EndpointPort{{Port: 8080}},
}},
}
testHttpsEndpoint := &kapi.Endpoints{
ObjectMeta: kapi.ObjectMeta{
Namespace: "quux",
Name: "testhttpsendpoint",
},
Subsets: []kapi.EndpointSubset{{
Addresses: []kapi.EndpointAddress{{IP: "10.1.2.1"}},
Ports: []kapi.EndpointPort{{Port: 8443}},
}},
}
// Add routes.
err = router.HandleRoute(watch.Added, testRoute)
if err != nil {
t.Fatalf("HandleRoute failed on adding test route: %v", err)
}
err = router.HandleRoute(watch.Added, testPassthroughRoute)
if err != nil {
t.Fatalf("HandleRoute failed on adding test passthrough route: %v", err)
}
err = router.HandleEndpoints(watch.Added, testHttpEndpoint)
if err != nil {
t.Fatalf("HandleEndpoints failed on adding test HTTP endpoint subset: %v",
err)
}
err = router.HandleEndpoints(watch.Added, testHttpsEndpoint)
if err != nil {
t.Fatalf("HandleEndpoints failed on adding test HTTPS endpoint subset: %v",
err)
}
// Initialize a new router, but retain the mock F5 host.
mockF5.close()
router, mockF5, err = newTestRouterWithState(mockF5.state, F5DefaultPartitionPath)
if err != nil {
t.Fatalf("Failed to initialize test router: %v", err)
}
defer mockF5.close()
// Have the new router delete the routes that the old router created.
err = router.HandleRoute(watch.Deleted, testRoute)
if err != nil {
t.Fatalf("HandleRoute failed on deleting test route: %v", err)
}
err = router.HandleRoute(watch.Deleted, testPassthroughRoute)
if err != nil {
t.Fatalf("HandleRoute failed on deleting test passthrough route: %v", err)
}
err = router.HandleEndpoints(watch.Deleted, testHttpEndpoint)
if err != nil {
t.Fatalf("HandleEndpoints failed on deleting test HTTP endpoint subset: %v",
err)
}
err = router.HandleEndpoints(watch.Deleted, testHttpsEndpoint)
if err != nil {
t.Fatalf("HandleEndpoints failed on deleting test HTTPS endpoint subset: %v",
err)
}
}