Skip to content

Commit 9a75315

Browse files
committed
integrate health checks for a lb service
1 parent 07c3c93 commit 9a75315

File tree

5 files changed

+179
-61
lines changed

5 files changed

+179
-61
lines changed

cmd/networkLoadBalancerCreate.go

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import (
2222

2323
"github.com/spf13/cast"
2424
"github.com/spf13/cobra"
25+
"gopkg.in/yaml.v2"
2526

27+
"github.com/liquidweb/liquidweb-cli/instance"
2628
"github.com/liquidweb/liquidweb-cli/types/api"
2729
"github.com/liquidweb/liquidweb-cli/types/cmd"
2830
"github.com/liquidweb/liquidweb-cli/validate"
@@ -40,7 +42,9 @@ var networkLoadBalancerCreateCmd = &cobra.Command{
4042
A Load Balancer allows you to distribute traffic to multiple endpoints.
4143
4244
%s
43-
`, networkLoadBalancerServicesHealthChecksHelp),
45+
46+
%s
47+
`, networkLoadBalancerServicesHealthChecksHelp, networkLoadBalancerServicesHealthCheckFileHelp),
4448
Run: func(cmd *cobra.Command, args []string) {
4549
nameFlag, _ := cmd.Flags().GetString("name")
4650
strategyFlag, _ := cmd.Flags().GetString("strategy")
@@ -52,6 +56,7 @@ A Load Balancer allows you to distribute traffic to multiple endpoints.
5256
enableSslIncludesFlag, _ := cmd.Flags().GetBool("enable-ssl-includes")
5357
disableSslIncludesFlag, _ := cmd.Flags().GetBool("disable-ssl-includes")
5458
regionFlag, _ := cmd.Flags().GetInt("region")
59+
healthCheckFileFlag, _ := cmd.Flags().GetString("health-check-file")
5560

5661
if enableSslTerminationFlag && disableSslTerminationFlag {
5762
lwCliInst.Die(fmt.Errorf("can't both enable and disable ssl termination"))
@@ -69,6 +74,9 @@ A Load Balancer allows you to distribute traffic to multiple endpoints.
6974
lwCliInst.Die(fmt.Errorf("when using --ssl-certificate or --ssl-private-key --enable-ssl-termination must be passed"))
7075
}
7176
}
77+
if len(healthChecksMapCreate) > 0 && healthCheckFileFlag != "" {
78+
lwCliInst.Die(fmt.Errorf("cannot pass conflicting flags --health-check and --health-check-file"))
79+
}
7280

7381
validateFields := map[interface{}]interface{}{
7482
strategyFlag: "LoadBalancerStrategy",
@@ -150,9 +158,39 @@ A Load Balancer allows you to distribute traffic to multiple endpoints.
150158
// slice of maps with keys src_port, dest_port, with a value of its network port number.
151159
var servicesToBalance []map[string]interface{}
152160
// a service is permitted to have one health check
153-
healthChecks, err := cmdTypes.LoadBalancerHealthCheck{HealthCheck: healthChecksMapCreate}.Transform()
154-
if err != nil {
155-
lwCliInst.Die(err)
161+
162+
var healthChecks map[string]map[string]interface{}
163+
164+
// health check, command line flags.
165+
if len(healthChecksMapCreate) > 0 {
166+
healthChecksFromCmdLine, err := cmdTypes.LoadBalancerHealthCheckCmdLine{HealthCheck: healthChecksMapCreate}.Transform()
167+
if err != nil {
168+
lwCliInst.Die(err)
169+
}
170+
healthChecks = healthChecksFromCmdLine
171+
} else if healthCheckFileFlag != "" {
172+
// health check, yaml file
173+
contents, err := ioutil.ReadFile(healthCheckFileFlag)
174+
if err != nil {
175+
lwCliInst.Die(fmt.Errorf("error reading given --health-check-file [%s]: %s", healthCheckFileFlag, err))
176+
}
177+
if err = yaml.Unmarshal(contents, &healthChecks); err != nil {
178+
lwCliInst.Die(fmt.Errorf("error yaml decoding [%s] (see help for an example of the file); %s", healthCheckFileFlag, err))
179+
}
180+
}
181+
182+
// validate
183+
for _, healthCheck := range healthChecks {
184+
var obj apiTypes.NetworkLoadBalancerDetailsServiceHealthCheck
185+
186+
if err := instance.CastFieldTypes(healthCheck, &obj); err != nil {
187+
lwCliInst.Die(fmt.Errorf(
188+
"failed casting --health-check-file [%s] to expected structure (see help for an example of the file): %s",
189+
healthCheckFileFlag, err))
190+
}
191+
if err := obj.Validate(); err != nil {
192+
lwCliInst.Die(err)
193+
}
156194
}
157195

158196
for _, pair := range networkLoadBalancerCreateServicesCmd {
@@ -216,7 +254,9 @@ func init() {
216254
[]string{}, "source/destination port pairs (such as 80:80) separated by ',' to balance via the Load Balancer")
217255

218256
networkLoadBalancerCreateCmd.Flags().StringToStringVar(&healthChecksMapCreate, "health-check", nil,
219-
"Health check defintions for the service matching src_port")
257+
"Health check defintions for the service matching src_port. Should not be combined with --health-check.")
258+
networkLoadBalancerCreateCmd.Flags().String("health-check-file", "",
259+
"A file containing valid yaml describing the LoadBalancer health checks to add for the service(s). Should not be combined with --health-check.")
220260

221261
networkLoadBalancerCreateCmd.MarkFlagRequired("name")
222262
networkLoadBalancerCreateCmd.MarkFlagRequired("services")

cmd/networkLoadBalancerUpdate.go

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import (
2222

2323
"github.com/spf13/cast"
2424
"github.com/spf13/cobra"
25+
"gopkg.in/yaml.v2"
2526

27+
"github.com/liquidweb/liquidweb-cli/instance"
2628
"github.com/liquidweb/liquidweb-cli/types/api"
2729
"github.com/liquidweb/liquidweb-cli/types/cmd"
2830
"github.com/liquidweb/liquidweb-cli/validate"
@@ -50,10 +52,33 @@ such as '80:80,443:443'.
5052
5153
For example, to set these values for the service with source port 443, the flag could look like this:
5254
53-
--health-check 443_failure_threshold=12,443_http_body_match=hello,443_http_path=/status,443_http_response_codes=200:201:202,443_http_use_tls=true,443_interval=10,443_protocol=tcp,443_timeout=99
55+
--health-check 443_failure_threshold=12,443_http_body_match=hello,443_http_path=/status,443_http_response_codes=200:201:202,443_http_use_tls=true,443_interval=10,443_protocol=http,443_timeout=99
5456
5557
Notice the leading '443_' before the parameter name. To create a health check for service 80 as well, follow the same pattern, but
5658
replacing '443_' with '80_'.`
59+
var networkLoadBalancerServicesHealthCheckFileHelp = `--health-check-file value should be the path to a yaml file containing the health check(s) to apply for each service(s). Here is
60+
an example of how that file should look:
61+
62+
443:
63+
protocol: http
64+
timeout: 5
65+
interval: 10
66+
http_use_tls: true
67+
http_response_codes: 200-202,404
68+
http_path: /status-443
69+
http_body_match:
70+
failure_threshold: 3
71+
80:
72+
protocol: http
73+
timeout: 10
74+
interval: 20
75+
http_use_tls: false
76+
http_response_codes: 200-202,404
77+
http_path: /status-80
78+
http_body_match:
79+
failure_threshold: 3
80+
81+
It is an error to provide both --health-check and --health-check-file flags.`
5782

5883
var networkLoadBalancerUpdateCmd = &cobra.Command{
5984
Use: "update",
@@ -64,11 +89,15 @@ A Load Balancer allows you to distribute traffic to multiple endpoints.
6489
6590
%s
6691
92+
%s
93+
6794
To remove a health check from a service, simply call update for the service(s) omitting their --health-check entries. For example,
6895
this would remove any set health checks for services 443:443,80:80 (as well as remove any other services entirely):
6996
7097
network load-balancer update --uniq_id ABC123 --services 443:443,80:80
71-
`, networkLoadBalancerServicesHealthChecksHelp),
98+
99+
Similarly to remove a health check when using --health-check-file, simply remove the health check from the file.
100+
`, networkLoadBalancerServicesHealthChecksHelp, networkLoadBalancerServicesHealthCheckFileHelp),
72101
Run: func(cmd *cobra.Command, args []string) {
73102
uniqIdFlag, _ := cmd.Flags().GetString("uniq_id")
74103
nameFlag, _ := cmd.Flags().GetString("name")
@@ -80,6 +109,7 @@ network load-balancer update --uniq_id ABC123 --services 443:443,80:80
80109
sslIntermediateCertFlag, _ := cmd.Flags().GetString("ssl-intermediate-certificate")
81110
enableSslIncludesFlag, _ := cmd.Flags().GetBool("enable-ssl-includes")
82111
disableSslIncludesFlag, _ := cmd.Flags().GetBool("disable-ssl-includes")
112+
healthCheckFileFlag, _ := cmd.Flags().GetString("health-check-file")
83113

84114
if enableSslTerminationFlag && disableSslTerminationFlag {
85115
lwCliInst.Die(fmt.Errorf("can't both enable and disable ssl termination"))
@@ -97,6 +127,9 @@ network load-balancer update --uniq_id ABC123 --services 443:443,80:80
97127
lwCliInst.Die(fmt.Errorf("when using --ssl-certificate or --ssl-private-key --enable-ssl-termination must be passed"))
98128
}
99129
}
130+
if len(healthChecksMapUpdate) > 0 && healthCheckFileFlag != "" {
131+
lwCliInst.Die(fmt.Errorf("cannot pass conflicting flags --health-check and --health-check-file"))
132+
}
100133

101134
validateFields := map[interface{}]interface{}{
102135
uniqIdFlag: "UniqId",
@@ -178,12 +211,42 @@ network load-balancer update --uniq_id ABC123 --services 443:443,80:80
178211
// services
179212
if len(networkLoadBalancerUpdateServicesCmd) > 0 {
180213
var servicesToBalance []map[string]interface{}
181-
// a service is permitted to have one health check
182-
healthChecks, err := cmdTypes.LoadBalancerHealthCheck{HealthCheck: healthChecksMapUpdate}.Transform()
183-
if err != nil {
184-
lwCliInst.Die(err)
214+
// a service is permitted to have one health check.
215+
216+
var healthChecks map[string]map[string]interface{}
217+
218+
// health check, command line flags.
219+
if len(healthChecksMapUpdate) > 0 {
220+
healthChecksFromCmdLine, err := cmdTypes.LoadBalancerHealthCheckCmdLine{HealthCheck: healthChecksMapUpdate}.Transform()
221+
if err != nil {
222+
lwCliInst.Die(err)
223+
}
224+
healthChecks = healthChecksFromCmdLine
225+
} else if healthCheckFileFlag != "" {
226+
// health check, yaml file
227+
contents, err := ioutil.ReadFile(healthCheckFileFlag)
228+
if err != nil {
229+
lwCliInst.Die(fmt.Errorf("error reading given --health-check-file [%s]: %s", healthCheckFileFlag, err))
230+
}
231+
if err = yaml.Unmarshal(contents, &healthChecks); err != nil {
232+
lwCliInst.Die(fmt.Errorf("error yaml decoding [%s] (see help for an example of the file); %s", healthCheckFileFlag, err))
233+
}
234+
}
235+
236+
// validate
237+
for _, healthCheck := range healthChecks {
238+
var obj apiTypes.NetworkLoadBalancerDetailsServiceHealthCheck
239+
if err := instance.CastFieldTypes(healthCheck, &obj); err != nil {
240+
lwCliInst.Die(fmt.Errorf(
241+
"failed casting --health-check-file [%s] to expected structure (see help for an example of the file): %s",
242+
healthCheckFileFlag, err))
243+
}
244+
if err := obj.Validate(); err != nil {
245+
lwCliInst.Die(err)
246+
}
185247
}
186248

249+
// build services api argument
187250
for _, pair := range networkLoadBalancerUpdateServicesCmd {
188251
err := validate.Validate(map[interface{}]interface{}{pair: "NetworkPortPair"})
189252
if err != nil {
@@ -206,7 +269,6 @@ network load-balancer update --uniq_id ABC123 --services 443:443,80:80
206269

207270
servicesToBalance = append(servicesToBalance, serviceToBalance)
208271
}
209-
210272
apiArgs["services"] = servicesToBalance
211273
}
212274

@@ -250,7 +312,10 @@ func init() {
250312
[]string{}, "source/destination port pairs (such as 80:80) separated by ',' to balance via the Load Balancer")
251313

252314
networkLoadBalancerUpdateCmd.Flags().StringToStringVar(&healthChecksMapUpdate, "health-check", nil,
253-
"Health check defintions for the service matching source port")
315+
"Health check defintions for the service matching source port. Should not be combined with --health-check.")
316+
317+
networkLoadBalancerUpdateCmd.Flags().String("health-check-file", "",
318+
"A file containing valid yaml describing the LoadBalancer health checks to add for the service(s). Should not be combined with --health-check.")
254319

255320
networkLoadBalancerUpdateCmd.MarkFlagRequired("uniq_id")
256321
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ require (
1717
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 // indirect
1818
golang.org/x/text v0.3.2 // indirect
1919
gopkg.in/ini.v1 v1.51.1 // indirect
20-
gopkg.in/yaml.v2 v2.2.8 // indirect
20+
gopkg.in/yaml.v2 v2.2.8
2121
)

types/api/network.go

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ limitations under the License.
1616
package apiTypes
1717

1818
import (
19+
"errors"
1920
"fmt"
2021
"strings"
22+
23+
"github.com/liquidweb/liquidweb-cli/validate"
2124
)
2225

2326
type NetworkIpPoolListEntry struct {
@@ -129,14 +132,61 @@ type NetworkLoadBalancerDetailsService struct {
129132
}
130133

131134
type NetworkLoadBalancerDetailsServiceHealthCheck struct {
132-
FailureThreshold int64 `json:"failure_threshold" mapstructure:"failure_threshold"`
133-
HttpBodyMatch string `json:"http_body_match" mapstructure:"http_body_match"`
134-
HttpPath string `json:"http_path" mapstructure:"http_path"`
135-
HttpResponseCodes string `json:"http_response_codes" mapstructure:"http_response_codes"`
136-
HttpUseTls bool `json:"http_use_tls" mapstructure:"http_use_tls"`
137-
Interval int64 `json:"interval" mapstructure:"interval"`
138-
Protocol string `json:"protocol" mapstructure:"protocol"`
139-
Timeout int64 `json:"timeout" mapstructure:"timeout"`
135+
FailureThreshold int64 `json:"failure_threshold" mapstructure:"failure_threshold" yaml:"failure_threshold"`
136+
HttpBodyMatch string `json:"http_body_match" mapstructure:"http_body_match" yaml:"http_body_match"`
137+
HttpPath string `json:"http_path" mapstructure:"http_path" yaml:"http_path"`
138+
HttpResponseCodes string `json:"http_response_codes" mapstructure:"http_response_codes" yaml:"http_response_codes"`
139+
HttpUseTls bool `json:"http_use_tls" mapstructure:"http_use_tls" yaml:"http_use_tls"`
140+
Interval int64 `json:"interval" mapstructure:"interval" yaml:"interval"`
141+
Protocol string `json:"protocol" mapstructure:"protocol" yaml:"protocol"`
142+
Timeout int64 `json:"timeout" mapstructure:"timeout" yaml:"timeout"`
143+
}
144+
145+
func (x NetworkLoadBalancerDetailsServiceHealthCheck) Validate() error {
146+
// protocol is required
147+
if x.Protocol == "" {
148+
return errors.New("protocol is required and was not given")
149+
}
150+
151+
// place defaults for http_path, http_use_tls, http_response_codes if protocol == "http" if unset.
152+
if x.Protocol != "http" {
153+
// when protocol isn't http, these shouldn't be set.
154+
if x.HttpPath != "" {
155+
return errors.New("http_path cannot be set when protocol isn't http")
156+
}
157+
if x.HttpResponseCodes != "" {
158+
return errors.New("http_response_codes cannot be set when protocol isn't http")
159+
}
160+
if x.HttpUseTls {
161+
return errors.New("http_use_tls cannot be set when protocol isn't http")
162+
}
163+
if x.HttpBodyMatch != "" {
164+
return errors.New("http_body_match cannot be set when protocol isn't http")
165+
}
166+
}
167+
168+
validateFields := map[interface{}]interface{}{
169+
x.Protocol: "LoadBalancerHealthCheckProtocol",
170+
}
171+
172+
if x.HttpResponseCodes != "" {
173+
validateFields[x.HttpResponseCodes] = "LoadBalancerHttpCodeRange"
174+
}
175+
if x.Timeout != 0 {
176+
validateFields[x.Timeout] = "PositiveInt64"
177+
}
178+
if x.Interval != 0 {
179+
validateFields[x.Interval] = "PositiveInt64"
180+
}
181+
if x.FailureThreshold != 0 {
182+
validateFields[x.FailureThreshold] = "PositiveInt64"
183+
}
184+
185+
if validateErr := validate.Validate(validateFields); validateErr != nil {
186+
return fmt.Errorf("healthCheck validation failed: %s", validateErr)
187+
}
188+
189+
return nil
140190
}
141191

142192
func (x NetworkLoadBalancerDetails) String() string {

types/cmd/cmdTypes.go

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,9 @@ package cmdTypes
1818
import (
1919
"fmt"
2020
"regexp"
21-
"strconv"
2221
"strings"
2322

2423
"github.com/spf13/cast"
25-
26-
"github.com/liquidweb/liquidweb-cli/validate"
2724
)
2825

2926
type AuthContext struct {
@@ -36,11 +33,11 @@ type AuthContext struct {
3633
Timeout int `json:"timeout" mapstructure:"timeout"`
3734
}
3835

39-
type LoadBalancerHealthCheck struct {
36+
type LoadBalancerHealthCheckCmdLine struct {
4037
HealthCheck map[string]string `json:"health_check" mapstructure:"health_check"`
4138
}
4239

43-
func (x LoadBalancerHealthCheck) Transform() (bySrcPort map[string]map[string]interface{}, err error) {
40+
func (x LoadBalancerHealthCheckCmdLine) Transform() (bySrcPort map[string]map[string]interface{}, err error) {
4441
// this is a bit of a hack. I couldn't find any supported way in cobra/viper/pflag to
4542
// get flags to go into a slice of maps. So its reading from flags into a single map,
4643
// and then doing this logic here to transform that into one suitable to aid later
@@ -154,40 +151,6 @@ func (x LoadBalancerHealthCheck) Transform() (bySrcPort map[string]map[string]in
154151
return
155152
}
156153
}
157-
158-
validateFields := map[interface{}]interface{}{
159-
healthCheck["protocol"]: "LoadBalancerHealthCheckProtocol",
160-
}
161-
162-
if val, exists := healthCheck["http_response_codes"]; exists {
163-
validateFields[val] = "LoadBalancerHttpCodeRange"
164-
}
165-
if val, exists := healthCheck["timeout"]; exists {
166-
if _, convErr := strconv.Atoi(cast.ToString(val)); convErr != nil {
167-
err = fmt.Errorf("timeout value [%+v] doesn't look numeric", val)
168-
return
169-
}
170-
validateFields[cast.ToInt(val)] = "PositiveInt"
171-
}
172-
if val, exists := healthCheck["interval"]; exists {
173-
if _, convErr := strconv.Atoi(cast.ToString(val)); convErr != nil {
174-
err = fmt.Errorf("interval value [%+v] doesn't look numeric", val)
175-
return
176-
}
177-
validateFields[cast.ToInt(val)] = "PositiveInt"
178-
}
179-
if val, exists := healthCheck["failure_threshold"]; exists {
180-
if _, convErr := strconv.Atoi(cast.ToString(val)); convErr != nil {
181-
err = fmt.Errorf("failure_threshold value [%+v] doesn't look numeric", val)
182-
return
183-
}
184-
validateFields[cast.ToInt(val)] = "PositiveInt"
185-
}
186-
187-
if validateErr := validate.Validate(validateFields); validateErr != nil {
188-
err = fmt.Errorf("healthCheck validation failed for service with source port [%+v]: %s", sourcePort, validateErr)
189-
return
190-
}
191154
}
192155

193156
return

0 commit comments

Comments
 (0)