Skip to content

Commit

Permalink
feat(manifest): add HTTP Health Check hidden fields (#1592)
Browse files Browse the repository at this point in the history
This enables the customization of four Load Balanced Web Service health check variables: HealthyThreshold, UnhealthyThreshold, Interval, and Timeout are now hidden fields in the manifest.

Resolves #1195.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
  • Loading branch information
huanjani committed Nov 3, 2020
1 parent 6570097 commit 95344bc
Show file tree
Hide file tree
Showing 9 changed files with 379 additions and 38 deletions.
9 changes: 4 additions & 5 deletions internal/pkg/deploy/cloudformation/stack/lb_web_svc.go
Expand Up @@ -25,7 +25,6 @@ const (
LBWebServiceHTTPSParamKey = "HTTPSEnabled"
LBWebServiceContainerPortParamKey = "ContainerPort"
LBWebServiceRulePathParamKey = "RulePath"
LBWebServiceHealthCheckPathParamKey = "HealthCheckPath"
LBWebServiceTargetContainerParamKey = "TargetContainer"
LBWebServiceTargetPortParamKey = "TargetPort"
LBWebServiceStickinessParamKey = "Stickiness"
Expand Down Expand Up @@ -115,6 +114,7 @@ func (s *LoadBalancedWebService) Template() (string, error) {
Sidecars: sidecars,
LogConfig: s.manifest.LogConfigOpts(),
Autoscaling: autoscaling,
HTTPHealthCheck: s.manifest.HTTPHealthCheckOpts(),
RulePriorityLambda: rulePriorityLambda.String(),
DesiredCountLambda: desiredCountLambda.String(),
})
Expand All @@ -130,6 +130,9 @@ func (s *LoadBalancedWebService) loadBalancerTarget() (targetContainer *string,
// Route load balancer traffic to main container by default.
targetContainer = aws.String(containerName)
targetPort = aws.String(containerPort)
if s.manifest.TargetContainer == nil && s.manifest.TargetContainerCamelCase != nil {
s.manifest.TargetContainer = s.manifest.TargetContainerCamelCase
}
mftTargetContainer := s.manifest.TargetContainer
if mftTargetContainer != nil {
sidecar, ok := s.manifest.Sidecars[*mftTargetContainer]
Expand Down Expand Up @@ -162,10 +165,6 @@ func (s *LoadBalancedWebService) Parameters() ([]*cloudformation.Parameter, erro
ParameterKey: aws.String(LBWebServiceRulePathParamKey),
ParameterValue: s.manifest.Path,
},
{
ParameterKey: aws.String(LBWebServiceHealthCheckPathParamKey),
ParameterValue: s.manifest.HealthCheckPath,
},
{
ParameterKey: aws.String(LBWebServiceHTTPSParamKey),
ParameterValue: aws.String(strconv.FormatBool(s.httpsEnabled)),
Expand Down
23 changes: 16 additions & 7 deletions internal/pkg/deploy/cloudformation/stack/lb_web_svc_test.go
Expand Up @@ -95,9 +95,8 @@ func TestLoadBalancedWebService_StackName(t *testing.T) {
func TestLoadBalancedWebService_Template(t *testing.T) {
testCases := map[string]struct {
mockDependencies func(t *testing.T, ctrl *gomock.Controller, c *LoadBalancedWebService)

wantedTemplate string
wantedError error
wantedTemplate string
wantedError error
}{
"unavailable rule priority lambda template": {
mockDependencies: func(t *testing.T, ctrl *gomock.Controller, c *LoadBalancedWebService) {
Expand Down Expand Up @@ -154,6 +153,13 @@ func TestLoadBalancedWebService_Template(t *testing.T) {
m.EXPECT().Read(lbWebSvcRulePriorityGeneratorPath).Return(&template.Content{Buffer: bytes.NewBufferString("lambda")}, nil)
m.EXPECT().Read(desiredCountGeneratorPath).Return(&template.Content{Buffer: bytes.NewBufferString("something")}, nil)
m.EXPECT().ParseLoadBalancedWebService(template.WorkloadOpts{
HTTPHealthCheck: &template.HTTPHealthCheckOpts{
HealthCheckPath: aws.String("/"),
HealthyThreshold: aws.Int64(2),
UnhealthyThreshold: aws.Int64(2),
Interval: aws.Int64(10),
Timeout: aws.Int64(5),
},
RulePriorityLambda: "lambda",
DesiredCountLambda: "something",
}).Return(&template.Content{Buffer: bytes.NewBufferString("template")}, nil)
Expand All @@ -177,6 +183,13 @@ func TestLoadBalancedWebService_Template(t *testing.T) {
SecretOutputs: []string{"MySecretArn"},
PolicyOutputs: []string{"AdditionalResourcesPolicyArn"},
},
HTTPHealthCheck: &template.HTTPHealthCheckOpts{
HealthCheckPath: aws.String("/"),
HealthyThreshold: aws.Int64(2),
UnhealthyThreshold: aws.Int64(2),
Interval: aws.Int64(10),
Timeout: aws.Int64(5),
},
RulePriorityLambda: "lambda",
DesiredCountLambda: "something",
}).Return(&template.Content{Buffer: bytes.NewBufferString("template")}, nil)
Expand Down Expand Up @@ -330,10 +343,6 @@ func TestLoadBalancedWebService_Parameters(t *testing.T) {
ParameterKey: aws.String(LBWebServiceRulePathParamKey),
ParameterValue: aws.String("frontend"),
},
{
ParameterKey: aws.String(LBWebServiceHealthCheckPathParamKey),
ParameterValue: aws.String("/"),
},
{
ParameterKey: aws.String(WorkloadTaskCPUParamKey),
ParameterValue: aws.String("256"),
Expand Down
105 changes: 99 additions & 6 deletions internal/pkg/manifest/lb_web_svc.go
Expand Up @@ -4,18 +4,33 @@
package manifest

import (
"errors"
"path/filepath"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/copilot-cli/internal/pkg/template"
"github.com/imdario/mergo"
"gopkg.in/yaml.v3"
)

const (
lbWebSvcManifestPath = "workloads/services/lb-web/manifest.yml"
)

// Default values for HTTPHealthCheck for a load balanced web service.
const (
// LogRetentionInDays is the default log retention time in days.
LogRetentionInDays = 30
LogRetentionInDays = 30
defaultHealthCheckPath = "/"
defaultHealthyThreshold = int64(2)
defaultUnhealthyThreshold = int64(2)
defaultIntervalinS = int64(10)
defaultTimeoutinS = int64(5)
)

var (
errUnmarshalHealthCheckArgs = errors.New("can't unmarshal healthcheck field into string or compose-style map")
)

// LoadBalancedWebService holds the configuration to build a container image with an exposed port that receives
Expand Down Expand Up @@ -46,13 +61,61 @@ func (lc *LoadBalancedWebServiceConfig) LogConfigOpts() *template.LogConfigOpts
return lc.logConfigOpts()
}

func (lc *LoadBalancedWebServiceConfig) HTTPHealthCheckOpts() *template.HTTPHealthCheckOpts {
opts := template.HTTPHealthCheckOpts{
HealthCheckPath: aws.String(defaultHealthCheckPath),
HealthyThreshold: aws.Int64(defaultHealthyThreshold),
UnhealthyThreshold: aws.Int64(defaultUnhealthyThreshold),
Interval: aws.Int64(defaultIntervalinS),
Timeout: aws.Int64(defaultTimeoutinS),
}
if lc.RoutingRule.HealthCheck.HealthCheckArgs.Path != nil {
opts.HealthCheckPath = lc.RoutingRule.HealthCheck.HealthCheckArgs.Path
}
if lc.RoutingRule.HealthCheck.HealthCheckPath != nil {
opts.HealthCheckPath = lc.HealthCheck.HealthCheckPath
}
if lc.RoutingRule.HealthCheck.HealthCheckArgs.HealthyThreshold != nil {
opts.HealthyThreshold = lc.RoutingRule.HealthCheck.HealthCheckArgs.HealthyThreshold
}
if lc.RoutingRule.HealthCheck.HealthCheckArgs.UnhealthyThreshold != nil {
opts.UnhealthyThreshold = lc.RoutingRule.HealthCheck.HealthCheckArgs.UnhealthyThreshold
}
if lc.RoutingRule.HealthCheck.HealthCheckArgs.Interval != nil {
opts.Interval = aws.Int64(int64(lc.RoutingRule.HealthCheck.HealthCheckArgs.Interval.Seconds()))
}
if lc.RoutingRule.HealthCheck.HealthCheckArgs.Timeout != nil {
opts.Timeout = aws.Int64(int64(lc.RoutingRule.HealthCheck.HealthCheckArgs.Timeout.Seconds()))
}
return &opts
}

// HTTPHealthCheckArgs holds the configuration to determine if the load balanced web service is healthy.
// These options are specifiable under the "healthcheck" field.
// See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-targetgroup.html.
type HTTPHealthCheckArgs struct {
Path *string `yaml:"path"`
HealthyThreshold *int64 `yaml:"healthy_threshold"`
UnhealthyThreshold *int64 `yaml:"unhealthy_threshold"`
Timeout *time.Duration `yaml:"timeout"`
Interval *time.Duration `yaml:"interval"`
}

// HealthCheckArgsOrString is a custom type which supports unmarshaling yaml which
// can either be of type string or type HealthCheckArgs. q
type HealthCheckArgsOrString struct {
HealthCheckPath *string
HealthCheckArgs HTTPHealthCheckArgs
}

// RoutingRule holds the path to route requests to the service.
type RoutingRule struct {
Path *string `yaml:"path"`
HealthCheckPath *string `yaml:"healthcheck"`
Stickiness *bool `yaml:"stickiness"`
Path *string `yaml:"path"`
HealthCheck HealthCheckArgsOrString `yaml:"healthcheck"`
Stickiness *bool `yaml:"stickiness"`
// TargetContainer is the container load balancer routes traffic to.
TargetContainer *string `yaml:"targetContainer"`
TargetContainer *string `yaml:"target_container"`
TargetContainerCamelCase *string `yaml:"targetContainer"` // "targetContainerCamelCase" for backwards compatibility
}

// LoadBalancedWebServiceProps contains properties for creating a new load balanced fargate service manifest.
Expand Down Expand Up @@ -85,7 +148,9 @@ func newDefaultLoadBalancedWebService() *LoadBalancedWebService {
LoadBalancedWebServiceConfig: LoadBalancedWebServiceConfig{
ImageConfig: ServiceImageWithPort{},
RoutingRule: RoutingRule{
HealthCheckPath: aws.String("/"),
HealthCheck: HealthCheckArgsOrString{
HealthCheckPath: aws.String("/"),
},
},
TaskConfig: TaskConfig{
CPU: aws.Int(256),
Expand All @@ -98,6 +163,34 @@ func newDefaultLoadBalancedWebService() *LoadBalancedWebService {
}
}

func (h *HTTPHealthCheckArgs) isEmpty() bool {
return h.Path == nil && h.HealthyThreshold == nil && h.UnhealthyThreshold == nil && h.Interval == nil && h.Timeout == nil
}

// UnmarshalYAML overrides the default YAML unmarshaling logic for the HealthCheckArgsOrString
// struct, allowing it to perform more complex unmarshaling behavior.
// This method implements the yaml.Unmarshaler (v2) interface.
func (h *HealthCheckArgsOrString) UnmarshalYAML(unmarshal func(interface{}) error) error {
if err := unmarshal(&h.HealthCheckArgs); err != nil {
switch err.(type) {
case *yaml.TypeError:
break
default:
return err
}
}

if !h.HealthCheckArgs.isEmpty() {
// Unmarshaled successfully to h.HealthCheckArgs, return.
return nil
}

if err := unmarshal(&h.HealthCheckPath); err != nil {
return errUnmarshalHealthCheckArgs
}
return nil
}

// MarshalBinary serializes the manifest object into a binary YAML document.
// Implements the encoding.BinaryMarshaler interface.
func (s *LoadBalancedWebService) MarshalBinary() ([]byte, error) {
Expand Down

0 comments on commit 95344bc

Please sign in to comment.