diff --git a/.changelog/2596.txt b/.changelog/2596.txt new file mode 100644 index 0000000000..50e5816094 --- /dev/null +++ b/.changelog/2596.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +Properly handle Kubernetes Jobs with ttl_seconds_after_finished = 0 to prevent unnecessary recreation. +``` \ No newline at end of file diff --git a/kubernetes/resource_kubernetes_job_v1.go b/kubernetes/resource_kubernetes_job_v1.go index 572635b340..cd2615518f 100644 --- a/kubernetes/resource_kubernetes_job_v1.go +++ b/kubernetes/resource_kubernetes_job_v1.go @@ -118,8 +118,17 @@ func resourceKubernetesJobV1Read(ctx context.Context, d *schema.ResourceData, me return diag.FromErr(err) } if !exists { - d.SetId("") - return diag.Diagnostics{} + // Check if ttl_seconds_after_finished is set + if ttl, ok := d.GetOk("spec.0.ttl_seconds_after_finished"); ok { + // ttl_seconds_after_finished is set, Job is deleted due to TTL + // We don't need to remove the resource from the state + log.Printf("[INFO] Job %s has been deleted by Kubernetes due to TTL (ttl_seconds_after_finished = %v), keeping resource in state", d.Id(), ttl) + return diag.Diagnostics{} + } else { + // ttl_seconds_after_finished is not set, remove the resource from the state + d.SetId("") + return diag.Diagnostics{} + } } conn, err := meta.(KubeClientsets).MainClientset() if err != nil { @@ -204,7 +213,6 @@ func resourceKubernetesJobV1Update(ctx context.Context, d *schema.ResourceData, } return resourceKubernetesJobV1Read(ctx, d, meta) } - func resourceKubernetesJobV1Delete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn, err := meta.(KubeClientsets).MainClientset() if err != nil { diff --git a/kubernetes/resource_kubernetes_job_v1_test.go b/kubernetes/resource_kubernetes_job_v1_test.go index 396777de9b..2e85679c23 100644 --- a/kubernetes/resource_kubernetes_job_v1_test.go +++ b/kubernetes/resource_kubernetes_job_v1_test.go @@ -237,6 +237,82 @@ func TestAccKubernetesJobV1_ttl_seconds_after_finished(t *testing.T) { }) } +func TestAccKubernetesJobV1_customizeDiff_ttlZero(t *testing.T) { + var conf batchv1.Job + name := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(10)) + imageName := busyboxImage + resourceName := "kubernetes_job_v1.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + skipIfClusterVersionLessThan(t, "1.21.0") + }, + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Create the Job + { + Config: testAccKubernetesJobV1Config_Diff(name, imageName, 0), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesJobV1Exists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "spec.0.ttl_seconds_after_finished", "0"), + ), + }, + // Step 2: Wait for the Job to complete and be deleted + { + PreConfig: func() { + time.Sleep(30 * time.Second) + }, + Config: testAccKubernetesJobV1Config_Diff(name, imageName, 0), + PlanOnly: true, + ExpectNonEmptyPlan: false, + }, + }, + }) +} + +func TestAccKubernetesJobV1_updateTTLFromZero(t *testing.T) { + var conf batchv1.Job + name := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(10)) + imageName := busyboxImage + resourceName := "kubernetes_job_v1.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + skipIfClusterVersionLessThan(t, "1.21.0") + }, + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Create the Job with ttl_seconds_after_finished = 0 + { + Config: testAccKubernetesJobV1Config_Diff(name, imageName, 0), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesJobV1Exists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "spec.0.ttl_seconds_after_finished", "0"), + ), + }, + // Step 2: Wait for the Job to complete and be deleted + { + PreConfig: func() { + time.Sleep(30 * time.Second) + }, + Config: testAccKubernetesJobV1Config_Diff(name, imageName, 0), + PlanOnly: true, + ExpectNonEmptyPlan: false, + }, + // Step 3: Update the Job to ttl_seconds_after_finished = 5 + { + Config: testAccKubernetesJobV1Config_Diff(name, imageName, 5), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesJobV1Exists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "spec.0.ttl_seconds_after_finished", "5"), + ), + }, + }, + }) +} + func testAccCheckJobV1Waited(minDuration time.Duration) func(*terraform.State) error { // NOTE this works because this function is called when setting up the test // and the function it returns is called after the resource has been created @@ -516,3 +592,28 @@ func testAccKubernetesJobV1Config_modified(name, imageName string) string { wait_for_completion = false }`, name, imageName) } + +func testAccKubernetesJobV1Config_Diff(name, imageName string, ttl int) string { + return fmt.Sprintf(` +resource "kubernetes_job_v1" "test" { + metadata { + name = "%s" + } + spec { + ttl_seconds_after_finished = %d + template { + metadata {} + spec { + container { + name = "wait-test" + image = "%s" + command = ["sleep", "20"] + } + restart_policy = "Never" + } + } + } + wait_for_completion = false +} +`, name, ttl, imageName) +} diff --git a/kubernetes/schema_job_spec.go b/kubernetes/schema_job_spec.go index 1e7157bab3..243d8aae13 100644 --- a/kubernetes/schema_job_spec.go +++ b/kubernetes/schema_job_spec.go @@ -235,7 +235,7 @@ func jobSpecFields(specUpdatable bool) map[string]*schema.Schema { "ttl_seconds_after_finished": { Type: schema.TypeString, Optional: true, - ForceNew: false, + ForceNew: true, ValidateFunc: func(value interface{}, key string) ([]string, []error) { v, err := strconv.Atoi(value.(string)) if err != nil {