diff --git a/pkg/lbagent/gobetween/config.go b/pkg/lbagent/gobetween/config.go index 4352d61afe..62545da196 100644 --- a/pkg/lbagent/gobetween/config.go +++ b/pkg/lbagent/gobetween/config.go @@ -301,18 +301,21 @@ type HealthcheckConfig struct { *PingHealthcheckConfig *ExecHealthcheckConfig - *UdpHealthcheckConfig + *ProbeHealthcheckConfig } type PingHealthcheckConfig struct{} +type ProbeHealthcheckConfig struct { + ProbeProtocol string `toml:"probe_protocol" json:"probe_protocol"` + ProbeStrategy string `toml:"probe_strategy" json:"probe_strategy"` + ProbeSend string `toml:"probe_send" json:"probe_send"` + ProbeRecv string `toml:"probe_recv" json:"probe_recv"` + ProbeRecvLen int `toml:"probe_recv_len" json:"probe_recv_len"` +} + type ExecHealthcheckConfig struct { ExecCommand string `toml:"exec_command" json:"exec_command,omitempty"` ExecExpectedPositiveOutput string `toml:"exec_expected_positive_output" json:"exec_expected_positive_output"` ExecExpectedNegativeOutput string `toml:"exec_expected_negative_output" json:"exec_expected_negative_output"` } - -type UdpHealthcheckConfig struct { - Receive string - Send string -} diff --git a/pkg/lbagent/models/gobetween.go b/pkg/lbagent/models/gobetween.go index c4a0b1b76c..71b1a93430 100644 --- a/pkg/lbagent/models/gobetween.go +++ b/pkg/lbagent/models/gobetween.go @@ -94,16 +94,23 @@ func (b *LoadbalancerCorpus) GenGobetweenConfigs(dir string, opts *GenGobetweenC // healthcheck var serverHealthcheck *gobetween.HealthcheckConfig if listener.HealthCheck == "on" && listener.HealthCheckType == "udp" { - serverHealthcheck = &gobetween.HealthcheckConfig{ - Kind: "pingudp", - Interval: fmt.Sprintf("%ds", listener.HealthCheckInterval), - Timeout: fmt.Sprintf("%ds", listener.HealthCheckTimeout), - Passes: listener.HealthCheckRise, - Fails: listener.HealthCheckFall, - UdpHealthcheckConfig: &gobetween.UdpHealthcheckConfig{ - Send: listener.HealthCheckReq, - Receive: listener.HealthCheckExp, - }, + if listener.HealthCheckReq == "" || listener.HealthCheckExp == "" { + log.Warningf("listener %s(%s): skip UDP healthcheck with empty request or expected response", + listener.Name, listener.Id) + } else { + serverHealthcheck = &gobetween.HealthcheckConfig{ + Kind: "probe", + Interval: fmt.Sprintf("%ds", listener.HealthCheckInterval), + Timeout: fmt.Sprintf("%ds", listener.HealthCheckTimeout), + Passes: listener.HealthCheckRise, + Fails: listener.HealthCheckFall, + ProbeHealthcheckConfig: &gobetween.ProbeHealthcheckConfig{ + ProbeProtocol: "udp", + ProbeStrategy: "starts_with", + ProbeSend: listener.HealthCheckReq, + ProbeRecv: listener.HealthCheckExp, + }, + } } } diff --git a/pkg/lbagent/models/gobetween_test.go b/pkg/lbagent/models/gobetween_test.go new file mode 100644 index 0000000000..49e57b696d --- /dev/null +++ b/pkg/lbagent/models/gobetween_test.go @@ -0,0 +1,165 @@ +// Copyright 2019 Yunion +// +// 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 models + +import ( + "encoding/json" + "testing" + + compute_models "yunion.io/x/onecloud/pkg/compute/models" +) + +func TestGenGobetweenConfigs_UDPHealthcheckUsesProbeSchema(t *testing.T) { + server := genUDPHealthcheckTestServer(t, "PING", "PONG", "on") + healthcheck := server["healthcheck"].(map[string]interface{}) + if got := healthcheck["kind"]; got != "probe" { + t.Fatalf("healthcheck.kind = %v, want probe", got) + } + if got := healthcheck["probe_protocol"]; got != "udp" { + t.Fatalf("healthcheck.probe_protocol = %v, want udp", got) + } + if got := healthcheck["probe_strategy"]; got != "starts_with" { + t.Fatalf("healthcheck.probe_strategy = %v, want starts_with", got) + } + if got := healthcheck["probe_send"]; got != "PING" { + t.Fatalf("healthcheck.probe_send = %v, want PING", got) + } + if got := healthcheck["probe_recv"]; got != "PONG" { + t.Fatalf("healthcheck.probe_recv = %v, want PONG", got) + } + if got := healthcheck["interval"]; got != "10s" { + t.Fatalf("healthcheck.interval = %v, want 10s", got) + } + if got := healthcheck["timeout"]; got != "5s" { + t.Fatalf("healthcheck.timeout = %v, want 5s", got) + } + if got := healthcheck["passes"]; got != float64(3) { + t.Fatalf("healthcheck.passes = %v, want 3", got) + } + if got := healthcheck["fails"]; got != float64(2) { + t.Fatalf("healthcheck.fails = %v, want 2", got) + } + if _, ok := healthcheck["Receive"]; ok { + t.Fatal("healthcheck must not contain Receive") + } + if _, ok := healthcheck["Send"]; ok { + t.Fatal("healthcheck must not contain Send") + } +} + +func TestGenGobetweenConfigs_UDPHealthcheckSkipsIncompleteProbe(t *testing.T) { + tests := []struct { + name string + request string + expected string + }{ + {name: "empty request", request: "", expected: "PONG"}, + {name: "empty expected response", request: "PING", expected: ""}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + server := genUDPHealthcheckTestServer(t, test.request, test.expected, "on") + if healthcheck, ok := server["healthcheck"]; ok && healthcheck != nil { + t.Fatalf("incomplete UDP healthcheck must not be generated: %v", healthcheck) + } + }) + } +} + +func TestGenGobetweenConfigs_UDPHealthcheckOffOmitsConfig(t *testing.T) { + server := genUDPHealthcheckTestServer(t, "PING", "PONG", "off") + if healthcheck, ok := server["healthcheck"]; ok && healthcheck != nil { + t.Fatalf("disabled UDP healthcheck must not be generated: %v", healthcheck) + } +} + +func genUDPHealthcheckTestServer(t *testing.T, request, expected, healthCheck string) map[string]interface{} { + t.Helper() + corpus, lb := newUDPHealthcheckTestCorpus(request, expected) + lb.Listeners["listener-id"].HealthCheck = healthCheck + opts := &GenGobetweenConfigOptions{ + LoadbalancersEnabled: []*Loadbalancer{lb}, + } + + if err := corpus.GenGobetweenConfigs(t.TempDir(), opts); err != nil { + t.Fatal(err) + } + + data, err := json.Marshal(opts.Config) + if err != nil { + t.Fatal(err) + } + var config map[string]interface{} + if err := json.Unmarshal(data, &config); err != nil { + t.Fatal(err) + } + + return config["servers"].(map[string]interface{})["listener-id"].(map[string]interface{}) +} + +func newUDPHealthcheckTestCorpus(request, expected string) (*LoadbalancerCorpus, *Loadbalancer) { + lb := &Loadbalancer{ + SLoadbalancer: &compute_models.SLoadbalancer{}, + Listeners: LoadbalancerListeners{}, + BackendGroups: LoadbalancerBackendGroups{}, + } + lb.Id = "loadbalancer-id" + lb.Address = "127.0.0.1" + + listener := &LoadbalancerListener{ + SLoadbalancerListener: &compute_models.SLoadbalancerListener{}, + } + listener.Id = "listener-id" + listener.Status = "enabled" + listener.ListenerType = "udp" + listener.ListenerPort = 9000 + listener.BackendGroupId = "backend-group-id" + listener.Scheduler = "rr" + listener.HealthCheck = "on" + listener.HealthCheckType = "udp" + listener.HealthCheckInterval = 10 + listener.HealthCheckTimeout = 5 + listener.HealthCheckRise = 3 + listener.HealthCheckFall = 2 + listener.HealthCheckReq = request + listener.HealthCheckExp = expected + lb.Listeners[listener.Id] = listener + listener.loadbalancer = lb + + backendGroup := &LoadbalancerBackendGroup{ + SLoadbalancerBackendGroup: &compute_models.SLoadbalancerBackendGroup{}, + Backends: LoadbalancerBackends{}, + } + backendGroup.Id = listener.BackendGroupId + backendGroup.LoadbalancerId = lb.Id + backendGroup.loadbalancer = lb + lb.BackendGroups[backendGroup.Id] = backendGroup + + backend := &LoadbalancerBackend{ + SLoadbalancerBackend: &compute_models.SLoadbalancerBackend{}, + } + backend.Id = "backend-id" + backend.BackendGroupId = backendGroup.Id + backend.Address = "127.0.0.1" + backend.Port = 9001 + backend.Weight = 1 + backend.backendGroup = backendGroup + backendGroup.Backends[backend.Id] = backend + + corpus := NewEmptyLoadbalancerCorpus() + corpus.Loadbalancers[lb.Id] = lb + return corpus, lb +}