diff --git a/internal/cmd/loadbalancer/add_service.go b/internal/cmd/loadbalancer/add_service.go index 885dd681..587103dd 100644 --- a/internal/cmd/loadbalancer/add_service.go +++ b/internal/cmd/loadbalancer/add_service.go @@ -2,6 +2,7 @@ package loadbalancer import ( "fmt" + "time" "github.com/spf13/cobra" @@ -34,6 +35,18 @@ var AddServiceCmd = base.Cmd{ cmd.Flags().Int64Slice("http-certificates", []int64{}, "ID of Certificates which are attached to this Load Balancer") cmd.Flags().Bool("http-redirect-http", false, "Redirect all traffic on port 80 to port 443") + cmd.Flags().String("health-check-protocol", "", "The protocol the health check is performed over") + cmd.Flags().Int("health-check-port", 0, "The port the health check is performed over") + cmd.Flags().Duration("health-check-interval", 15*time.Second, "The interval the health check is performed") + cmd.Flags().Duration("health-check-timeout", 10*time.Second, "The timeout after a health check is marked as failed") + cmd.Flags().Int("health-check-retries", 3, "Number of retries after a health check is marked as failed") + + cmd.Flags().String("health-check-http-domain", "", "The domain we request when performing a http health check") + cmd.Flags().String("health-check-http-path", "", "The path we request when performing a http health check") + cmd.Flags().StringSlice("health-check-http-status-codes", []string{}, "List of status codes we expect to determine a target as healthy") + cmd.Flags().String("health-check-http-response", "", "The response we expect to determine a target as healthy") + cmd.Flags().Bool("health-check-http-tls", false, "Determine if the health check should verify if the target answers with a valid TLS certificate") + return cmd }, Run: func(s state.State, cmd *cobra.Command, args []string) error { @@ -116,6 +129,81 @@ var AddServiceCmd = base.Cmd{ opts.HTTP.Certificates = append(opts.HTTP.Certificates, &hcloud.Certificate{ID: certificateID}) } } + + // Health check + healthCheckProtocol, _ := cmd.Flags().GetString("health-check-protocol") + healthCheckPort, _ := cmd.Flags().GetInt("health-check-port") + healthCheckInterval, _ := cmd.Flags().GetDuration("health-check-interval") + healthCheckTimeout, _ := cmd.Flags().GetDuration("health-check-timeout") + healthCheckRetries, _ := cmd.Flags().GetInt("health-check-retries") + + addHealthCheck := false + for _, f := range []string{"protocol", "port", "interval", "timeout", "retries"} { + if cmd.Flags().Changed("health-check-" + f) { + addHealthCheck = true + break + } + } + + if addHealthCheck { + opts.HealthCheck = &hcloud.LoadBalancerAddServiceOptsHealthCheck{} + if healthCheckProtocol == "" { + return fmt.Errorf("required flag health-check-protocol not set") + } + switch proto := hcloud.LoadBalancerServiceProtocol(healthCheckProtocol); proto { + case hcloud.LoadBalancerServiceProtocolHTTP, hcloud.LoadBalancerServiceProtocolHTTPS, hcloud.LoadBalancerServiceProtocolTCP: + opts.HealthCheck.Protocol = proto + break + default: + return fmt.Errorf("invalid health check protocol: %s", healthCheckProtocol) + } + + if healthCheckPort == 0 { + return fmt.Errorf("required flag health-check-port not set") + } + if healthCheckPort > 65535 { + return fmt.Errorf("invalid health check port: %d", healthCheckPort) + } + opts.HealthCheck.Port = &healthCheckPort + + if cmd.Flags().Changed("health-check-interval") { + opts.HealthCheck.Interval = &healthCheckInterval + } + if cmd.Flags().Changed("health-check-timeout") { + opts.HealthCheck.Timeout = &healthCheckTimeout + } + if cmd.Flags().Changed("health-check-retries") { + opts.HealthCheck.Retries = &healthCheckRetries + } + + if opts.HealthCheck.Protocol == hcloud.LoadBalancerServiceProtocolHTTP || + opts.HealthCheck.Protocol == hcloud.LoadBalancerServiceProtocolHTTPS { + + opts.HealthCheck.HTTP = &hcloud.LoadBalancerAddServiceOptsHealthCheckHTTP{} + healthCheckHTTPDomain, _ := cmd.Flags().GetString("health-check-http-domain") + healthCheckHTTPPath, _ := cmd.Flags().GetString("health-check-http-path") + healthCheckHTTPResponse, _ := cmd.Flags().GetString("health-check-http-response") + healthCheckHTTPStatusCodes, _ := cmd.Flags().GetStringSlice("health-check-http-status-codes") + healthCheckHTTPTLS, _ := cmd.Flags().GetBool("health-check-http-tls") + + if cmd.Flags().Changed("health-check-http-domain") { + opts.HealthCheck.HTTP.Domain = &healthCheckHTTPDomain + } + if cmd.Flags().Changed("health-check-http-path") { + opts.HealthCheck.HTTP.Path = &healthCheckHTTPPath + } + if cmd.Flags().Changed("health-check-http-response") { + opts.HealthCheck.HTTP.Response = &healthCheckHTTPResponse + } + if cmd.Flags().Changed("health-check-http-status-codes") { + opts.HealthCheck.HTTP.StatusCodes = healthCheckHTTPStatusCodes + } + if cmd.Flags().Changed("health-check-http-tls") { + opts.HealthCheck.HTTP.TLS = &healthCheckHTTPTLS + } + } + } + action, _, err := s.Client().LoadBalancer().AddService(s, loadBalancer, opts) if err != nil { return err diff --git a/internal/cmd/loadbalancer/add_service_test.go b/internal/cmd/loadbalancer/add_service_test.go index 79c960bb..b4c97c36 100644 --- a/internal/cmd/loadbalancer/add_service_test.go +++ b/internal/cmd/loadbalancer/add_service_test.go @@ -2,6 +2,7 @@ package loadbalancer_test import ( "testing" + "time" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" @@ -45,3 +46,75 @@ func TestAddService(t *testing.T) { assert.Empty(t, errOut) assert.Equal(t, expOut, out) } + +func TestAddServiceWithHealthCheck(t *testing.T) { + fx := testutil.NewFixture(t) + defer fx.Finish() + + cmd := loadbalancer.AddServiceCmd.CobraCommand(fx.State()) + fx.ExpectEnsureToken() + + fx.Client.LoadBalancerClient.EXPECT(). + Get(gomock.Any(), "123"). + Return(&hcloud.LoadBalancer{ID: 123}, nil, nil) + fx.Client.LoadBalancerClient.EXPECT(). + AddService(gomock.Any(), &hcloud.LoadBalancer{ID: 123}, hcloud.LoadBalancerAddServiceOpts{ + Protocol: hcloud.LoadBalancerServiceProtocolHTTP, + ListenPort: hcloud.Ptr(80), + DestinationPort: hcloud.Ptr(8080), + HTTP: &hcloud.LoadBalancerAddServiceOptsHTTP{ + StickySessions: hcloud.Ptr(true), + RedirectHTTP: hcloud.Ptr(true), + CookieName: hcloud.Ptr("test"), + Certificates: []*hcloud.Certificate{{ID: 1}}, + CookieLifetime: hcloud.Ptr(10 * time.Minute), + }, + Proxyprotocol: hcloud.Ptr(false), + HealthCheck: &hcloud.LoadBalancerAddServiceOptsHealthCheck{ + Protocol: hcloud.LoadBalancerServiceProtocolHTTP, + Port: hcloud.Ptr(80), + Interval: hcloud.Ptr(10 * time.Second), + Timeout: hcloud.Ptr(5 * time.Second), + Retries: hcloud.Ptr(2), + HTTP: &hcloud.LoadBalancerAddServiceOptsHealthCheckHTTP{ + Domain: hcloud.Ptr("example.com"), + Path: hcloud.Ptr("/health"), + StatusCodes: []string{"200"}, + Response: hcloud.Ptr("OK"), + TLS: hcloud.Ptr(true), + }, + }, + }). + Return(&hcloud.Action{ID: 123}, nil, nil) + fx.ActionWaiter.EXPECT(). + ActionProgress(gomock.Any(), gomock.Any(), &hcloud.Action{ID: 123}). + Return(nil) + + out, errOut, err := fx.Run(cmd, []string{ + "123", + "--protocol", "http", + "--listen-port", "80", + "--destination-port", "8080", + "--http-redirect-http=true", + "--http-sticky-sessions=true", + "--http-cookie-name", "test", + "--http-cookie-lifetime", "10m", + "--http-certificates", "1", + "--health-check-protocol", "http", + "--health-check-port", "80", + "--health-check-interval", "10s", + "--health-check-timeout", "5s", + "--health-check-retries", "2", + "--health-check-http-domain", "example.com", + "--health-check-http-path", "/health", + "--health-check-http-status-codes", "200", + "--health-check-http-response", "OK", + "--health-check-http-tls=true", + }) + + expOut := "Service was added to Load Balancer 123\n" + + assert.NoError(t, err) + assert.Empty(t, errOut) + assert.Equal(t, expOut, out) +}