Skip to content

Commit 4110e51

Browse files
nullabeAntoine Belluardremyleone
authored
feat(container): add health_check block (#2878)
* feat(container/serverless): add health_check block * Fix lint * Fix linter --------- Co-authored-by: Antoine Belluard <[email protected]> Co-authored-by: Rémy Léone <[email protected]>
1 parent 6cddf65 commit 4110e51

File tree

8 files changed

+3243
-2
lines changed

8 files changed

+3243
-2
lines changed

docs/data-sources/container.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,13 @@ In addition to all arguments above, the following attributes are exported:
9191

9292
- `deploy` - Boolean indicating whether the container is on a production environment.
9393

94-
- `sandbox` - (Optional) Execution environment of the container.
94+
- `sandbox` - Execution environment of the container.
95+
96+
- `heath_check` - Health check configuration block of the container.
97+
- `http` - HTTP health check configuration.
98+
- `path` - Path to use for the HTTP health check.
99+
- `failure_threshold` - Number of consecutive health check failures before considering the container unhealthy.
100+
- `interval`- Period between health checks (in seconds).
95101

96102
- `status` - The container status.
97103

docs/resources/container.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ The following arguments are supported:
8484

8585
- `sandbox` - (Optional) Execution environment of the container.
8686

87+
- `heath_check` - (Optional) Health check configuration block of the container.
88+
- `http` - HTTP health check configuration.
89+
- `path` - Path to use for the HTTP health check.
90+
- `failure_threshold` - Number of consecutive health check failures before considering the container unhealthy.
91+
- `interval`- Period between health checks (in seconds).
92+
8793
- `port` - (Optional) The port to expose the container.
8894

8995
- `deploy` - (Optional) Boolean indicating whether the container is in a production environment.
@@ -152,4 +158,34 @@ The `memory_limit` (in MB) must correspond with the right amount of vCPU. Refer
152158
| 4096 | 2240 |
153159

154160
~>**Important:** Make sure to select the right resources, as you will be billed based on compute usage over time and the number of Containers executions.
155-
Refer to the [Serverless Containers pricing](https://www.scaleway.com/en/docs/faq/serverless-containers/#prices) for more information.
161+
Refer to the [Serverless Containers pricing](https://www.scaleway.com/en/docs/faq/serverless-containers/#prices) for more information.
162+
163+
## Health check configuration
164+
165+
Custom health checks can be configured on the container.
166+
167+
It's possible to specify the HTTP path that the probe will listen to and the number of failures before considering the container as unhealthy.
168+
During a deployment, if a newly created container fails to pass the health check, the deployment is aborted.
169+
As a result, lowering this value can help to reduce the time it takes to detect a failed deployment.
170+
The period between health checks is also configurable.
171+
172+
Example:
173+
174+
```terraform
175+
resource scaleway_container main {
176+
name = "my-container-02"
177+
namespace_id = scaleway_container_namespace.main.id
178+
179+
health_check {
180+
http {
181+
path = "/ping"
182+
}
183+
failure_threshold = 40
184+
interval = "3s"
185+
}
186+
}
187+
```
188+
189+
~>**Important:** Another probe type can be set to TCP with the API, but currently the SDK has not been updated with this parameter.
190+
This is why the only probe that can be used here is the HTTP probe.
191+
Refer to the [API Reference](https://www.scaleway.com/en/developers/api/serverless-containers/#path-containers-create-a-new-container) for more information.

internal/services/container/container.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
99
container "github.com/scaleway/scaleway-sdk-go/api/container/v1beta1"
1010
"github.com/scaleway/scaleway-sdk-go/scw"
11+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/dsf"
1112
"github.com/scaleway/terraform-provider-scaleway/v2/internal/httperrors"
1213
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality"
1314
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality/regional"
@@ -170,6 +171,44 @@ func ResourceContainer() *schema.Resource {
170171
Description: "Execution environment of the container.",
171172
ValidateDiagFunc: verify.ValidateEnum[container.ContainerSandbox](),
172173
},
174+
"health_check": {
175+
Type: schema.TypeSet,
176+
Optional: true,
177+
Computed: true,
178+
Description: "Health check configuration of the container.",
179+
Elem: &schema.Resource{
180+
Schema: map[string]*schema.Schema{
181+
// TCP has not been implemented yet in the API SDK, that's why the parameter is not in the schema.
182+
// See container.ContainerHealthCheckSpecTCPProbe.
183+
"http": {
184+
Type: schema.TypeSet,
185+
Description: "HTTP health check configuration.",
186+
Required: true,
187+
Elem: &schema.Resource{
188+
Schema: map[string]*schema.Schema{
189+
"path": {
190+
Type: schema.TypeString,
191+
Description: "Path to use for the HTTP health check.",
192+
Required: true,
193+
},
194+
},
195+
},
196+
},
197+
"failure_threshold": {
198+
Type: schema.TypeInt,
199+
Description: "Number of consecutive health check failures before considering the container unhealthy.",
200+
Required: true,
201+
},
202+
"interval": {
203+
Type: schema.TypeString,
204+
Description: "Period between health checks.",
205+
DiffSuppressFunc: dsf.Duration,
206+
ValidateDiagFunc: verify.IsDuration(),
207+
Required: true,
208+
},
209+
},
210+
},
211+
},
173212
// computed
174213
"status": {
175214
Type: schema.TypeString,
@@ -283,6 +322,7 @@ func ResourceContainerRead(ctx context.Context, d *schema.ResourceData, m interf
283322
_ = d.Set("deploy", scw.BoolPtr(*types.ExpandBoolPtr(d.Get("deploy"))))
284323
_ = d.Set("http_option", co.HTTPOption)
285324
_ = d.Set("sandbox", co.Sandbox)
325+
_ = d.Set("health_check", flattenHealthCheck(co.HealthCheck))
286326
_ = d.Set("region", co.Region.String())
287327

288328
return nil
@@ -378,6 +418,17 @@ func ResourceContainerUpdate(ctx context.Context, d *schema.ResourceData, m inte
378418
req.Sandbox = container.ContainerSandbox(d.Get("sandbox").(string))
379419
}
380420

421+
if d.HasChanges("health_check") {
422+
healthCheck := d.Get("health_check")
423+
424+
healthCheckReq, errExpandHealthCheck := expandHealthCheck(healthCheck)
425+
if errExpandHealthCheck != nil {
426+
return diag.FromErr(errExpandHealthCheck)
427+
}
428+
429+
req.HealthCheck = healthCheckReq
430+
}
431+
381432
imageHasChanged := d.HasChanges("registry_sha256")
382433
if imageHasChanged {
383434
req.Redeploy = &imageHasChanged

internal/services/container/container_data_source_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,41 @@ func TestAccDataSourceContainer_Basic(t *testing.T) {
4949
},
5050
})
5151
}
52+
53+
func TestAccDataSourceContainer_HealthCheck(t *testing.T) {
54+
tt := acctest.NewTestTools(t)
55+
defer tt.Cleanup()
56+
57+
resource.ParallelTest(t, resource.TestCase{
58+
PreCheck: func() { acctest.PreCheck(t) },
59+
ProviderFactories: tt.ProviderFactories,
60+
CheckDestroy: isNamespaceDestroyed(tt),
61+
Steps: []resource.TestStep{
62+
{
63+
Config: `
64+
resource scaleway_container_namespace main {}
65+
66+
resource scaleway_container main {
67+
namespace_id = scaleway_container_namespace.main.id
68+
deploy = false
69+
}
70+
71+
data scaleway_container main {
72+
namespace_id = scaleway_container_namespace.main.id
73+
container_id = scaleway_container.main.id
74+
}
75+
`,
76+
Check: resource.ComposeTestCheckFunc(
77+
isContainerPresent(tt, "scaleway_container.main"),
78+
// Check default option returned by the API when you don't specify the health_check block.
79+
resource.TestCheckResourceAttr("scaleway_container.main", "health_check.#", "1"),
80+
resource.TestCheckResourceAttr("scaleway_container.main", "health_check.0.failure_threshold", "30"),
81+
resource.TestCheckResourceAttr("scaleway_container.main", "health_check.0.interval", "10s"),
82+
resource.TestCheckResourceAttr("data.scaleway_container.main", "health_check.#", "1"),
83+
resource.TestCheckResourceAttr("data.scaleway_container.main", "health_check.0.failure_threshold", "30"),
84+
resource.TestCheckResourceAttr("data.scaleway_container.main", "health_check.0.interval", "10s"),
85+
),
86+
},
87+
},
88+
})
89+
}

internal/services/container/container_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,60 @@ func TestAccContainer_Sandbox(t *testing.T) {
400400
})
401401
}
402402

403+
func TestAccContainer_HealthCheck(t *testing.T) {
404+
tt := acctest.NewTestTools(t)
405+
defer tt.Cleanup()
406+
resource.ParallelTest(t, resource.TestCase{
407+
PreCheck: func() { acctest.PreCheck(t) },
408+
ProviderFactories: tt.ProviderFactories,
409+
CheckDestroy: isContainerDestroyed(tt),
410+
Steps: []resource.TestStep{
411+
{
412+
Config: `
413+
resource scaleway_container_namespace main {}
414+
415+
resource scaleway_container main {
416+
namespace_id = scaleway_container_namespace.main.id
417+
deploy = false
418+
}
419+
`,
420+
Check: resource.ComposeTestCheckFunc(
421+
isContainerPresent(tt, "scaleway_container.main"),
422+
// Check default option returned by the API when you don't specify the health_check block.
423+
resource.TestCheckResourceAttr("scaleway_container.main", "health_check.#", "1"),
424+
resource.TestCheckResourceAttr("scaleway_container.main", "health_check.0.failure_threshold", "30"),
425+
resource.TestCheckResourceAttr("scaleway_container.main", "health_check.0.interval", "10s"),
426+
),
427+
},
428+
{
429+
Config: `
430+
resource scaleway_container_namespace main {}
431+
432+
resource scaleway_container main {
433+
namespace_id = scaleway_container_namespace.main.id
434+
deploy = false
435+
436+
health_check {
437+
http {
438+
path = "/test"
439+
}
440+
failure_threshold = 40
441+
interval = "12s"
442+
}
443+
}
444+
`,
445+
Check: resource.ComposeTestCheckFunc(
446+
isContainerPresent(tt, "scaleway_container.main"),
447+
resource.TestCheckResourceAttr("scaleway_container.main", "health_check.#", "1"),
448+
resource.TestCheckResourceAttr("scaleway_container.main", "health_check.0.http.0.path", "/test"),
449+
resource.TestCheckResourceAttr("scaleway_container.main", "health_check.0.failure_threshold", "40"),
450+
resource.TestCheckResourceAttr("scaleway_container.main", "health_check.0.interval", "12s"),
451+
),
452+
},
453+
},
454+
})
455+
}
456+
403457
func isContainerPresent(tt *acctest.TestTools, n string) resource.TestCheckFunc {
404458
return func(state *terraform.State) error {
405459
rs, ok := state.RootModule().Resources[n]

internal/services/container/helpers_container.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,109 @@ func setCreateContainerRequest(d *schema.ResourceData, region scw.Region) (*cont
117117
req.Sandbox = container.ContainerSandbox(sandbox.(string))
118118
}
119119

120+
if healthCheck, ok := d.GetOk("health_check"); ok {
121+
healthCheckReq, errExpandHealthCheck := expandHealthCheck(healthCheck)
122+
if errExpandHealthCheck != nil {
123+
return nil, errExpandHealthCheck
124+
}
125+
126+
req.HealthCheck = healthCheckReq
127+
}
128+
120129
return req, nil
121130
}
122131

132+
func expandHealthCheck(healthCheckSchema interface{}) (*container.ContainerHealthCheckSpec, error) {
133+
healthCheck, ok := healthCheckSchema.(*schema.Set)
134+
if !ok {
135+
return &container.ContainerHealthCheckSpec{}, nil
136+
}
137+
138+
for _, option := range healthCheck.List() {
139+
rawOption, isRawOption := option.(map[string]interface{})
140+
if !isRawOption {
141+
continue
142+
}
143+
144+
healthCheckSpec := &container.ContainerHealthCheckSpec{}
145+
if http, ok := rawOption["http"].(*schema.Set); ok {
146+
healthCheckSpec.HTTP = expendHealthCheckHTTP(http)
147+
}
148+
149+
// Failure threshold is a required field and will be checked by TF.
150+
healthCheckSpec.FailureThreshold = uint32(rawOption["failure_threshold"].(int))
151+
152+
if interval, ok := rawOption["interval"]; ok {
153+
duration, err := types.ExpandDuration(interval)
154+
if err != nil {
155+
return nil, err
156+
}
157+
158+
healthCheckSpec.Interval = scw.NewDurationFromTimeDuration(*duration)
159+
}
160+
161+
return healthCheckSpec, nil
162+
}
163+
164+
return &container.ContainerHealthCheckSpec{}, nil
165+
}
166+
167+
func expendHealthCheckHTTP(healthCheckHTTPSchema interface{}) *container.ContainerHealthCheckSpecHTTPProbe {
168+
healthCheckHTTP, ok := healthCheckHTTPSchema.(*schema.Set)
169+
if !ok {
170+
return &container.ContainerHealthCheckSpecHTTPProbe{}
171+
}
172+
173+
for _, option := range healthCheckHTTP.List() {
174+
rawOption, isRawOption := option.(map[string]interface{})
175+
if !isRawOption {
176+
continue
177+
}
178+
179+
httpProbe := &container.ContainerHealthCheckSpecHTTPProbe{}
180+
if path, ok := rawOption["path"].(string); ok {
181+
httpProbe.Path = path
182+
}
183+
184+
return httpProbe
185+
}
186+
187+
return &container.ContainerHealthCheckSpecHTTPProbe{}
188+
}
189+
190+
func flattenHealthCheck(healthCheck *container.ContainerHealthCheckSpec) interface{} {
191+
if healthCheck == nil {
192+
return nil
193+
}
194+
195+
var interval *time.Duration
196+
if healthCheck.Interval != nil {
197+
interval = healthCheck.Interval.ToTimeDuration()
198+
}
199+
200+
flattenedHealthCheck := []map[string]interface{}(nil)
201+
flattenedHealthCheck = append(flattenedHealthCheck, map[string]interface{}{
202+
"http": flattenHealthCheckHTTP(healthCheck.HTTP),
203+
"failure_threshold": types.FlattenUint32Ptr(&healthCheck.FailureThreshold),
204+
"interval": types.FlattenDuration(interval),
205+
})
206+
207+
return flattenedHealthCheck
208+
}
209+
210+
func flattenHealthCheckHTTP(healthCheckHTTP *container.ContainerHealthCheckSpecHTTPProbe) interface{} {
211+
if healthCheckHTTP == nil {
212+
return nil
213+
}
214+
215+
flattenedHealthCheckHTTP := []map[string]interface{}(nil)
216+
flattenedHealthCheckHTTP = append(flattenedHealthCheckHTTP, map[string]interface{}{
217+
"path": types.FlattenStringPtr(&healthCheckHTTP.Path),
218+
})
219+
220+
return flattenedHealthCheckHTTP
221+
}
222+
123223
func expandContainerSecrets(secretsRawMap interface{}) []*container.Secret {
124224
secretsMap := secretsRawMap.(map[string]interface{})
125225
secrets := make([]*container.Secret, 0, len(secretsMap))

0 commit comments

Comments
 (0)