diff --git a/kubernetes/resource_kubernetes_deployment_v1_test.go b/kubernetes/resource_kubernetes_deployment_v1_test.go index 85872dec7e..3de9be9396 100644 --- a/kubernetes/resource_kubernetes_deployment_v1_test.go +++ b/kubernetes/resource_kubernetes_deployment_v1_test.go @@ -462,6 +462,38 @@ func TestAccKubernetesDeploymentV1_with_container_liveness_probe_using_tcp(t *te }) } +func TestAccKubernetesDeploymentV1_with_container_liveness_probe_using_termination_grace_period_seconds(t *testing.T) { + var conf appsv1.Deployment + + deploymentName := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + resourceName := "kubernetes_deployment_v1.test" + imageName := agnhostImage + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckKubernetesDeploymentV1Destroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesDeploymentV1ConfigWithLivenessProbeUsingTerminationGracePeriodSeconds(deploymentName, imageName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesDeploymentV1Exists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "spec.0.template.0.spec.0.container.0.args.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.template.0.spec.0.container.0.liveness_probe.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.template.0.spec.0.container.0.liveness_probe.0.http_get.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.template.0.spec.0.container.0.liveness_probe.0.http_get.0.path", "/healthz"), + resource.TestCheckResourceAttr(resourceName, "spec.0.template.0.spec.0.container.0.liveness_probe.0.http_get.0.port", "8080"), + resource.TestCheckResourceAttr(resourceName, "spec.0.template.0.spec.0.container.0.liveness_probe.0.http_get.0.http_header.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.template.0.spec.0.container.0.liveness_probe.0.http_get.0.http_header.0.name", "X-Custom-Header"), + resource.TestCheckResourceAttr(resourceName, "spec.0.template.0.spec.0.container.0.liveness_probe.0.http_get.0.http_header.0.value", "Awesome"), + resource.TestCheckResourceAttr(resourceName, "spec.0.template.0.spec.0.container.0.liveness_probe.0.initial_delay_seconds", "3"), + resource.TestCheckResourceAttr(resourceName, "spec.0.template.0.spec.0.container.0.liveness_probe.0.termination_grace_period_seconds", "10"), + ), + }, + }, + }) +} + func TestAccKubernetesDeploymentV1_with_container_lifecycle(t *testing.T) { var conf appsv1.Deployment @@ -2012,6 +2044,59 @@ func testAccKubernetesDeploymentV1ConfigWithLivenessProbeUsingHTTPGet(deployment `, deploymentName, imageName) } +func testAccKubernetesDeploymentV1ConfigWithLivenessProbeUsingTerminationGracePeriodSeconds(deploymentName, imageName string) string { + return fmt.Sprintf(`resource "kubernetes_deployment_v1" "test" { + metadata { + name = "%s" + + labels = { + Test = "TfAcceptanceTest" + } + } + + spec { + selector { + match_labels = { + Test = "TfAcceptanceTest" + } + } + + template { + metadata { + labels = { + Test = "TfAcceptanceTest" + } + } + + spec { + container { + image = "%s" + name = "containername" + args = ["liveness"] + + liveness_probe { + http_get { + path = "/healthz" + port = 8080 + + http_header { + name = "X-Custom-Header" + value = "Awesome" + } + } + initial_delay_seconds = 3 + period_seconds = 1 + termination_grace_period_seconds = 10 + } + } + termination_grace_period_seconds = 1 + } + } + } +} +`, deploymentName, imageName) +} + func testAccKubernetesDeploymentV1ConfigWithLivenessProbeUsingTCP(deploymentName, imageName string) string { return fmt.Sprintf(`resource "kubernetes_deployment_v1" "test" { metadata { diff --git a/kubernetes/resource_kubernetes_pod_v1_test.go b/kubernetes/resource_kubernetes_pod_v1_test.go index d1970deb96..0cbf6dafed 100644 --- a/kubernetes/resource_kubernetes_pod_v1_test.go +++ b/kubernetes/resource_kubernetes_pod_v1_test.go @@ -571,6 +571,47 @@ func TestAccKubernetesPodV1_with_container_liveness_probe_using_http_get(t *test }) } +func TestAccKubernetesPodV1_with_container_liveness_probe_using_termination_grace_period_seconds(t *testing.T) { + var conf api.Pod + + podName := acctest.RandomWithPrefix("tf-acc-test") + imageName := agnhostImage + resourceName := "kubernetes_pod_v1.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + skipIfClusterVersionLessThan(t, "1.25.0") + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckKubernetesPodV1Destroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesPodV1ConfigWithLivenessProbeUsingTerminationGracePeriod(podName, imageName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesPodV1Exists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "spec.0.container.0.args.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.container.0.liveness_probe.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.container.0.liveness_probe.0.http_get.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.container.0.liveness_probe.0.http_get.0.path", "/healthz"), + resource.TestCheckResourceAttr(resourceName, "spec.0.container.0.liveness_probe.0.http_get.0.port", "8080"), + resource.TestCheckResourceAttr(resourceName, "spec.0.container.0.liveness_probe.0.http_get.0.http_header.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.container.0.liveness_probe.0.http_get.0.http_header.0.name", "X-Custom-Header"), + resource.TestCheckResourceAttr(resourceName, "spec.0.container.0.liveness_probe.0.http_get.0.http_header.0.value", "Awesome"), + resource.TestCheckResourceAttr(resourceName, "spec.0.container.0.liveness_probe.0.initial_delay_seconds", "3"), + resource.TestCheckResourceAttr(resourceName, "spec.0.container.0.liveness_probe.0.termination_grace_period_seconds", "10"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"metadata.0.resource_version"}, + }, + }, + }) +} + func TestAccKubernetesPodV1_with_container_liveness_probe_using_tcp(t *testing.T) { var conf api.Pod @@ -1188,6 +1229,43 @@ func TestAccKubernetesPodV1_config_container_startup_probe(t *testing.T) { }) } +func TestAccKubernetesPodV1_config_container_startup_probe_termination_grace_period_seconds(t *testing.T) { + var confPod api.Pod + + podName := acctest.RandomWithPrefix("tf-acc-test") + imageName := busyboxImage + resourceName := "kubernetes_pod_v1.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + skipIfClusterVersionLessThan(t, "1.25.0") + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckKubernetesPodV1Destroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesPodV1ContainerStartupProbe_termination_grace_period_seconds(podName, imageName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesPodV1Exists(resourceName, &confPod), + resource.TestCheckResourceAttr(resourceName, "metadata.0.generation", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.container.0.startup_probe.0.http_get.0.path", "/index.html"), + resource.TestCheckResourceAttr(resourceName, "spec.0.container.0.startup_probe.0.http_get.0.port", "80"), + resource.TestCheckResourceAttr(resourceName, "spec.0.container.0.startup_probe.0.initial_delay_seconds", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.container.0.startup_probe.0.timeout_seconds", "2"), + resource.TestCheckResourceAttr(resourceName, "spec.0.container.0.startup_probe.0.termination_grace_period_seconds", "10"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"metadata.0.resource_version"}, + }, + }, + }) +} + func TestAccKubernetesPodV1_termination_message_policy_default(t *testing.T) { var confPod api.Pod @@ -2117,6 +2195,42 @@ func testAccKubernetesPodV1ConfigWithLivenessProbeUsingHTTPGet(podName, imageNam `, podName, imageName) } +func testAccKubernetesPodV1ConfigWithLivenessProbeUsingTerminationGracePeriod(podName, imageName string) string { + return fmt.Sprintf(`resource "kubernetes_pod_v1" "test" { + metadata { + labels = { + app = "pod_label" + } + + name = "%s" + } + + spec { + container { + image = "%s" + name = "containername" + args = ["liveness"] + + liveness_probe { + http_get { + path = "/healthz" + port = 8080 + + http_header { + name = "X-Custom-Header" + value = "Awesome" + } + } + initial_delay_seconds = 3 + period_seconds = 1 + termination_grace_period_seconds = 10 + } + } + } +} +`, podName, imageName) +} + func testAccKubernetesPodV1ConfigWithLivenessProbeUsingTCP(podName, imageName string) string { return fmt.Sprintf(`resource "kubernetes_pod_v1" "test" { metadata { @@ -2946,6 +3060,33 @@ func testAccKubernetesPodV1ContainerStartupProbe(podName, imageName string) stri `, podName, imageName) } +func testAccKubernetesPodV1ContainerStartupProbe_termination_grace_period_seconds(podName, imageName string) string { + return fmt.Sprintf(`resource "kubernetes_pod_v1" "test" { + metadata { + name = "%s" + } + + spec { + container { + image = "%s" + name = "containername" + + startup_probe { + http_get { + path = "/index.html" + port = 80 + } + + initial_delay_seconds = 1 + timeout_seconds = 2 + termination_grace_period_seconds = 10 + } + } + } +} +`, podName, imageName) +} + func testAccKubernetesTerminationMessagePolicyDefault(podName, imageName string) string { return fmt.Sprintf(`resource "kubernetes_pod_v1" "test" { metadata { diff --git a/kubernetes/schema_container.go b/kubernetes/schema_container.go index 499ea0236a..d3d12ed062 100644 --- a/kubernetes/schema_container.go +++ b/kubernetes/schema_container.go @@ -528,7 +528,7 @@ func containerFields(isUpdatable bool) map[string]*schema.Schema { MaxItems: 1, ForceNew: !isUpdatable, Description: "Periodic probe of container liveness. Container will be restarted if the probe fails. Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes", - Elem: probeSchema(), + Elem: probeSchema(LivenessProbe), }, "name": { Type: schema.TypeString, @@ -589,7 +589,7 @@ func containerFields(isUpdatable bool) map[string]*schema.Schema { MaxItems: 1, ForceNew: !isUpdatable, Description: "Periodic probe of container service readiness. Container will be removed from service endpoints if the probe fails. Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes", - Elem: probeSchema(), + Elem: probeSchema(ReadinessProbe), }, "resources": { Type: schema.TypeList, @@ -616,7 +616,7 @@ func containerFields(isUpdatable bool) map[string]*schema.Schema { MaxItems: 1, ForceNew: !isUpdatable, Description: "StartupProbe indicates that the Pod has successfully initialized. If specified, no other probes are executed until this completes successfully. If this probe fails, the Pod will be restarted, just as if the livenessProbe failed. This can be used to provide different probe parameters at the beginning of a Pod's lifecycle, when it might take a long time to load data or warm a cache, than during steady-state operation. This cannot be updated. This is an alpha feature enabled by the StartupProbe feature flag. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", - Elem: probeSchema(), + Elem: probeSchema(StartupProbe), }, "stdin": { Type: schema.TypeBool, @@ -685,7 +685,18 @@ func containerFields(isUpdatable bool) map[string]*schema.Schema { return s } -func probeSchema() *schema.Resource { +type ProbeType int + +const ( + LivenessProbe ProbeType = iota + ReadinessProbe + StartupProbe +) + +// probeSchema generates the schema for a Liveness/Readiness/Startup probe +// +// probeType specifies the type of probe to generate the schema for (liveness, readiness, startup) +func probeSchema(probeType ProbeType) *schema.Resource { h := lifecycleHandlerFields() h["grpc"] = &schema.Schema{ Type: schema.TypeList, @@ -741,10 +752,22 @@ func probeSchema() *schema.Resource { ValidateFunc: validatePositiveInteger, Description: "Number of seconds after which the probe times out. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes", } + + // Conditionally add a `termination_grace_period_seconds` to this schema if the probe type is Liveness or Startup + // `Probe-level terminationGracePeriodSeconds cannot be set for readiness probes. It will be rejected by the API server.` + // https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#probe-level-terminationgraceperiodseconds + if probeType == LivenessProbe || probeType == StartupProbe { + h["termination_grace_period_seconds"] = &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validateIntGreaterThan(0), + Description: "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's `terminationGracePeriodSeconds` will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer greater than zero. `spec.terminationGracePeriodSeconds` is used if unset.", + } + } + return &schema.Resource{ Schema: h, } - } func securityContextSchema(isUpdatable bool) *schema.Resource { diff --git a/kubernetes/structures_container.go b/kubernetes/structures_container.go index e116825050..69fbfab531 100644 --- a/kubernetes/structures_container.go +++ b/kubernetes/structures_container.go @@ -177,6 +177,9 @@ func flattenProbe(in *v1.Probe) []interface{} { if in.GRPC != nil { att["grpc"] = flattenGRPC(in.GRPC) } + if in.TerminationGracePeriodSeconds != nil { + att["termination_grace_period_seconds"] = *in.TerminationGracePeriodSeconds + } return []interface{}{att} } @@ -765,6 +768,11 @@ func expandProbe(l []interface{}) *v1.Probe { obj.TimeoutSeconds = int32(v) } + // Set the `TerminationGracePeriodSeconds` field only if it is not set to the default (0) + if v, ok := in["termination_grace_period_seconds"].(int); ok && v != 0 { + obj.TerminationGracePeriodSeconds = ptr.To(int64(v)) + } + return &obj }