diff options
Diffstat (limited to 'vendor/github.com/goamz/goamz/elb/elbtest')
-rw-r--r-- | vendor/github.com/goamz/goamz/elb/elbtest/server.go | 551 |
1 files changed, 551 insertions, 0 deletions
diff --git a/vendor/github.com/goamz/goamz/elb/elbtest/server.go b/vendor/github.com/goamz/goamz/elb/elbtest/server.go new file mode 100644 index 000000000..9b8f79d4e --- /dev/null +++ b/vendor/github.com/goamz/goamz/elb/elbtest/server.go @@ -0,0 +1,551 @@ +// Package elbtest implements a fake ELB provider with the capability of +// inducing errors on any given operation, and retrospectively determining what +// operations have been carried out. +package elbtest + +import ( + "encoding/xml" + "fmt" + "github.com/goamz/goamz/elb" + "net" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + "sync" +) + +// Server implements an ELB simulator for use in testing. +type Server struct { + url string + listener net.Listener + mutex sync.Mutex + reqId int + lbs map[string]*elb.LoadBalancerDescription + lbsReqs map[string]url.Values + instances []string + instanceStates map[string][]*elb.InstanceState + instCount int +} + +// Starts and returns a new server +func NewServer() (*Server, error) { + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + return nil, fmt.Errorf("cannot listen on localhost: %v", err) + } + srv := &Server{ + listener: l, + url: "http://" + l.Addr().String(), + lbs: make(map[string]*elb.LoadBalancerDescription), + instanceStates: make(map[string][]*elb.InstanceState), + } + go http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + srv.serveHTTP(w, req) + })) + return srv, nil +} + +// Quit closes down the server. +func (srv *Server) Quit() { + srv.listener.Close() +} + +// URL returns the URL of the server. +func (srv *Server) URL() string { + return srv.url +} + +type xmlErrors struct { + XMLName string `xml:"ErrorResponse"` + Error elb.Error +} + +func (srv *Server) error(w http.ResponseWriter, err *elb.Error) { + w.WriteHeader(err.StatusCode) + xmlErr := xmlErrors{Error: *err} + if e := xml.NewEncoder(w).Encode(xmlErr); e != nil { + panic(e) + } +} + +func (srv *Server) serveHTTP(w http.ResponseWriter, req *http.Request) { + req.ParseForm() + srv.mutex.Lock() + defer srv.mutex.Unlock() + f := actions[req.Form.Get("Action")] + if f == nil { + srv.error(w, &elb.Error{ + StatusCode: 400, + Code: "InvalidParameterValue", + Message: "Unrecognized Action", + }) + } + reqId := fmt.Sprintf("req%0X", srv.reqId) + srv.reqId++ + if resp, err := f(srv, w, req, reqId); err == nil { + if err := xml.NewEncoder(w).Encode(resp); err != nil { + panic(err) + } + } else { + switch err.(type) { + case *elb.Error: + srv.error(w, err.(*elb.Error)) + default: + panic(err) + } + } +} + +func (srv *Server) createLoadBalancer(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) { + composition := map[string]string{ + "AvailabilityZones.member.1": "Subnets.member.1", + } + if err := srv.validateComposition(req, composition); err != nil { + return nil, err + } + required := []string{ + "Listeners.member.1.InstancePort", + "Listeners.member.1.InstanceProtocol", + "Listeners.member.1.Protocol", + "Listeners.member.1.LoadBalancerPort", + "LoadBalancerName", + } + if err := srv.validate(req, required); err != nil { + return nil, err + } + path := req.FormValue("Path") + if path == "" { + path = "/" + } + lbName := req.FormValue("LoadBalancerName") + srv.lbs[lbName] = srv.makeLoadBalancerDescription(req.Form) + srv.lbs[lbName].DNSName = fmt.Sprintf("%s-some-aws-stuff.us-east-1.elb.amazonaws.com", lbName) + return elb.CreateLoadBalancerResp{ + DNSName: srv.lbs[lbName].DNSName, + }, nil +} + +func (srv *Server) deleteLoadBalancer(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) { + if err := srv.validate(req, []string{"LoadBalancerName"}); err != nil { + return nil, err + } + srv.RemoveLoadBalancer(req.FormValue("LoadBalancerName")) + return elb.SimpleResp{RequestId: reqId}, nil +} + +func (srv *Server) registerInstancesWithLoadBalancer(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) { + required := []string{"LoadBalancerName", "Instances.member.1.InstanceId"} + if err := srv.validate(req, required); err != nil { + return nil, err + } + lbName := req.FormValue("LoadBalancerName") + if err := srv.lbExists(lbName); err != nil { + return nil, err + } + instIds := []string{} + instances := []elb.Instance{} + i := 1 + instId := req.FormValue(fmt.Sprintf("Instances.member.%d.InstanceId", i)) + for instId != "" { + if err := srv.instanceExists(instId); err != nil { + return nil, err + } + instIds = append(instIds, instId) + instances = append(instances, elb.Instance{InstanceId: instId}) + i++ + instId = req.FormValue(fmt.Sprintf("Instances.member.%d.InstanceId", i)) + } + srv.instanceStates[lbName] = append(srv.instanceStates[lbName], srv.makeInstanceState(instId)) + srv.lbs[lbName].Instances = append(srv.lbs[lbName].Instances, instances...) + return elb.RegisterInstancesResp{InstanceIds: instIds}, nil +} + +func (srv *Server) deregisterInstancesFromLoadBalancer(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) { + required := []string{"LoadBalancerName"} + if err := srv.validate(req, required); err != nil { + return nil, err + } + lbName := req.FormValue("LoadBalancerName") + if err := srv.lbExists(lbName); err != nil { + return nil, err + } + i := 1 + lb := srv.lbs[lbName] + instId := req.FormValue(fmt.Sprintf("Instances.member.%d.InstanceId", i)) + for instId != "" { + if err := srv.instanceExists(instId); err != nil { + return nil, err + } + i++ + removeInstanceFromLB(lb, instId) + instId = req.FormValue(fmt.Sprintf("Instances.member.%d.InstanceId", i)) + } + srv.lbs[lbName] = lb + srv.removeInstanceStatesFromLoadBalancer(lbName, instId) + return elb.SimpleResp{RequestId: reqId}, nil +} + +func (srv *Server) describeLoadBalancers(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) { + i := 1 + lbName := req.FormValue(fmt.Sprintf("LoadBalancerNames.member.%d", i)) + for lbName != "" { + key := fmt.Sprintf("LoadBalancerNames.member.%d", i) + if req.FormValue(key) != "" { + if err := srv.lbExists(req.FormValue(key)); err != nil { + return nil, err + } + } + i++ + lbName = req.FormValue(fmt.Sprintf("LoadBalancerNames.member.%d", i)) + } + lbsDesc := make([]elb.LoadBalancerDescription, len(srv.lbs)) + i = 0 + for _, lb := range srv.lbs { + lbsDesc[i] = *lb + i++ + } + resp := elb.DescribeLoadBalancerResp{ + LoadBalancerDescriptions: lbsDesc, + } + return resp, nil +} + +// getParameters returns the value all parameters from a request that matches a +// prefix. +// +// For example, for the prefix "Subnets.member.", it will return a slice +// containing the value of keys "Subnets.member.1", "Subnets.member.2" ... +// "Subnets.member.N". The prefix must include the trailing dot. +func (srv *Server) getParameters(prefix string, values url.Values) []string { + i, key := 1, "" + var k = func(n int) string { + return fmt.Sprintf(prefix+"%d", n) + } + var result []string + for i, key = 2, k(i); values.Get(key) != ""; i, key = i+1, k(i) { + result = append(result, values.Get(key)) + } + return result +} + +func (srv *Server) makeInstanceState(id string) *elb.InstanceState { + return &elb.InstanceState{ + Description: "Instance is in pending state.", + InstanceId: id, + State: "OutOfService", + ReasonCode: "Instance", + } +} + +func removeInstanceFromLB(lb *elb.LoadBalancerDescription, id string) { + index := -1 + for i, instance := range lb.Instances { + if instance.InstanceId == id { + index = i + break + } + } + if index > -1 { + copy(lb.Instances[index:], lb.Instances[index+1:]) + lb.Instances = lb.Instances[:len(lb.Instances)-1] + } +} + +func (srv *Server) removeInstanceStatesFromLoadBalancer(lb, id string) { + for i, state := range srv.instanceStates[lb] { + if state.InstanceId == id { + a := srv.instanceStates[lb] + a[i], a = a[len(a)-1], a[:len(a)-1] + srv.instanceStates[lb] = a + return + } + } +} + +func (srv *Server) makeLoadBalancerDescription(value url.Values) *elb.LoadBalancerDescription { + lds := []elb.ListenerDescription{} + i := 1 + protocol := value.Get(fmt.Sprintf("Listeners.member.%d.Protocol", i)) + for protocol != "" { + key := fmt.Sprintf("Listeners.member.%d.", i) + lInstPort, _ := strconv.Atoi(value.Get(key + "InstancePort")) + lLBPort, _ := strconv.Atoi(value.Get(key + "LoadBalancerPort")) + lDescription := elb.ListenerDescription{ + Listener: elb.Listener{ + Protocol: strings.ToUpper(protocol), + InstanceProtocol: strings.ToUpper(value.Get(key + "InstanceProtocol")), + LoadBalancerPort: lLBPort, + InstancePort: lInstPort, + }, + } + i++ + protocol = value.Get(fmt.Sprintf("Listeners.member.%d.Protocol", i)) + lds = append(lds, lDescription) + } + sourceSecGroup := srv.makeSourceSecGroup(value) + lbDesc := elb.LoadBalancerDescription{ + AvailabilityZones: srv.getParameters("AvailabilityZones.member.", value), + Subnets: srv.getParameters("Subnets.member.", value), + SecurityGroups: srv.getParameters("SecurityGroups.member.", value), + HealthCheck: srv.makeHealthCheck(value), + ListenerDescriptions: lds, + Scheme: value.Get("Scheme"), + SourceSecurityGroup: sourceSecGroup, + LoadBalancerName: value.Get("LoadBalancerName"), + } + if lbDesc.Scheme == "" { + lbDesc.Scheme = "internet-facing" + } + return &lbDesc +} + +func (srv *Server) makeHealthCheck(value url.Values) elb.HealthCheck { + ht := 10 + timeout := 5 + ut := 2 + interval := 30 + target := "TCP:80" + if v := value.Get("HealthCheck.HealthyThreshold"); v != "" { + ht, _ = strconv.Atoi(v) + } + if v := value.Get("HealthCheck.Timeout"); v != "" { + timeout, _ = strconv.Atoi(v) + } + if v := value.Get("HealthCheck.UnhealthyThreshold"); v != "" { + ut, _ = strconv.Atoi(v) + } + if v := value.Get("HealthCheck.Interval"); v != "" { + interval, _ = strconv.Atoi(v) + } + if v := value.Get("HealthCheck.Target"); v != "" { + target = v + } + return elb.HealthCheck{ + HealthyThreshold: ht, + Interval: interval, + Target: target, + Timeout: timeout, + UnhealthyThreshold: ut, + } +} + +func (srv *Server) makeSourceSecGroup(value url.Values) elb.SourceSecurityGroup { + name := "amazon-elb-sg" + alias := "amazon-elb" + if v := value.Get("SourceSecurityGroup.GroupName"); v != "" { + name = v + } + if v := value.Get("SourceSecurityGroup.OwnerAlias"); v != "" { + alias = v + } + return elb.SourceSecurityGroup{ + GroupName: name, + OwnerAlias: alias, + } +} + +func (srv *Server) describeInstanceHealth(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) { + if err := srv.lbExists(req.FormValue("LoadBalancerName")); err != nil { + return nil, err + } + resp := elb.DescribeInstanceHealthResp{ + InstanceStates: []elb.InstanceState{}, + } + for _, state := range srv.instanceStates[req.FormValue("LoadBalancerName")] { + resp.InstanceStates = append(resp.InstanceStates, *state) + } + i := 1 + instanceId := req.FormValue("Instances.member.1.InstanceId") + for instanceId != "" { + if err := srv.instanceExists(instanceId); err != nil { + return nil, err + } + is := elb.InstanceState{ + Description: "Instance is in pending state.", + InstanceId: instanceId, + State: "OutOfService", + ReasonCode: "Instance", + } + resp.InstanceStates = append(resp.InstanceStates, is) + i++ + instanceId = req.FormValue(fmt.Sprintf("Instances.member.%d.InstanceId", i)) + } + return resp, nil +} + +func (srv *Server) configureHealthCheck(w http.ResponseWriter, req *http.Request, reqId string) (interface{}, error) { + required := []string{ + "LoadBalancerName", + "HealthCheck.HealthyThreshold", + "HealthCheck.Interval", + "HealthCheck.Target", + "HealthCheck.Timeout", + "HealthCheck.UnhealthyThreshold", + } + if err := srv.validate(req, required); err != nil { + return nil, err + } + target := req.FormValue("HealthCheck.Target") + r, err := regexp.Compile(`[\w]+:[\d]+\/+`) + if err != nil { + panic(err) + } + if m := r.FindStringSubmatch(target); m == nil { + return nil, &elb.Error{ + StatusCode: 400, + Code: "ValidationError", + Message: "HealthCheck HTTP Target must specify a port followed by a path that begins with a slash. e.g. HTTP:80/ping/this/path", + } + } + ht, _ := strconv.Atoi(req.FormValue("HealthCheck.HealthyThreshold")) + interval, _ := strconv.Atoi(req.FormValue("HealthCheck.Interval")) + timeout, _ := strconv.Atoi(req.FormValue("HealthCheck.Timeout")) + ut, _ := strconv.Atoi(req.FormValue("HealthCheck.UnhealthyThreshold")) + return elb.HealthCheckResp{ + HealthCheck: &elb.HealthCheck{ + HealthyThreshold: ht, + Interval: interval, + Target: target, + Timeout: timeout, + UnhealthyThreshold: ut, + }, + }, nil +} + +func (srv *Server) instanceExists(id string) error { + for _, instId := range srv.instances { + if instId == id { + return nil + } + } + return &elb.Error{ + StatusCode: 400, + Code: "InvalidInstance", + Message: fmt.Sprintf("InvalidInstance found in [%s]. Invalid id: \"%s\"", id, id), + } +} + +func (srv *Server) lbExists(name string) error { + if _, ok := srv.lbs[name]; !ok { + return &elb.Error{ + StatusCode: 400, + Code: "LoadBalancerNotFound", + Message: fmt.Sprintf("There is no ACTIVE Load Balancer named '%s'", name), + } + } + return nil +} + +func (srv *Server) validate(req *http.Request, required []string) error { + for _, field := range required { + if req.FormValue(field) == "" { + return &elb.Error{ + StatusCode: 400, + Code: "ValidationError", + Message: fmt.Sprintf("%s is required.", field), + } + } + } + return nil +} + +// Validates the composition of the fields. +// +// Some fields cannot be together in the same request, such as AvailabilityZones and Subnets. +// A sample map with the above requirement would be +// c := map[string]string{ +// "AvailabilityZones.member.1": "Subnets.member.1", +// } +// +// The server also requires that at least one of those fields are specified. +func (srv *Server) validateComposition(req *http.Request, composition map[string]string) error { + for k, v := range composition { + if req.FormValue(k) != "" && req.FormValue(v) != "" { + return &elb.Error{ + StatusCode: 400, + Code: "ValidationError", + Message: fmt.Sprintf("Only one of %s or %s may be specified", k, v), + } + } + if req.FormValue(k) == "" && req.FormValue(v) == "" { + return &elb.Error{ + StatusCode: 400, + Code: "ValidationError", + Message: fmt.Sprintf("Either %s or %s must be specified", k, v), + } + } + } + return nil +} + +// Creates a fake instance in the server +func (srv *Server) NewInstance() string { + srv.instCount++ + instId := fmt.Sprintf("i-%d", srv.instCount) + srv.instances = append(srv.instances, instId) + return instId +} + +// Removes a fake instance from the server +// +// If no instance is found it does nothing +func (srv *Server) RemoveInstance(instId string) { + for i, id := range srv.instances { + if id == instId { + srv.instances[i], srv.instances = srv.instances[len(srv.instances)-1], srv.instances[:len(srv.instances)-1] + } + } +} + +// Creates a fake load balancer in the fake server +func (srv *Server) NewLoadBalancer(name string) { + srv.lbs[name] = &elb.LoadBalancerDescription{ + LoadBalancerName: name, + DNSName: fmt.Sprintf("%s-some-aws-stuff.sa-east-1.amazonaws.com", name), + } +} + +// Removes a fake load balancer from the fake server +func (srv *Server) RemoveLoadBalancer(name string) { + delete(srv.lbs, name) +} + +// Register a fake instance with a fake Load Balancer +// +// If the Load Balancer does not exists it does nothing +func (srv *Server) RegisterInstance(instId, lbName string) { + lb, ok := srv.lbs[lbName] + if !ok { + fmt.Println("lb not found :/") + return + } + lb.Instances = append(lb.Instances, elb.Instance{InstanceId: instId}) + srv.instanceStates[lbName] = append(srv.instanceStates[lbName], srv.makeInstanceState(instId)) +} + +func (srv *Server) DeregisterInstance(instId, lbName string) { + removeInstanceFromLB(srv.lbs[lbName], instId) + srv.removeInstanceStatesFromLoadBalancer(lbName, instId) +} + +func (srv *Server) ChangeInstanceState(lb string, state elb.InstanceState) { + states := srv.instanceStates[lb] + for i, s := range states { + if s.InstanceId == state.InstanceId { + srv.instanceStates[lb][i] = &state + return + } + } +} + +var actions = map[string]func(*Server, http.ResponseWriter, *http.Request, string) (interface{}, error){ + "CreateLoadBalancer": (*Server).createLoadBalancer, + "DeleteLoadBalancer": (*Server).deleteLoadBalancer, + "RegisterInstancesWithLoadBalancer": (*Server).registerInstancesWithLoadBalancer, + "DeregisterInstancesFromLoadBalancer": (*Server).deregisterInstancesFromLoadBalancer, + "DescribeLoadBalancers": (*Server).describeLoadBalancers, + "DescribeInstanceHealth": (*Server).describeInstanceHealth, + "ConfigureHealthCheck": (*Server).configureHealthCheck, +} |