Skip to content

Feature/job v1 suspend field #2682

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/resources/job_v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Optional:
- `parallelism` (Number) Specifies the maximum desired number of pods the job should run at any given time. The actual number of pods running in steady state will be less than this number when ((.spec.completions - .status.successful) < .spec.parallelism), i.e. when the work left to do is less than max parallelism. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/
- `pod_failure_policy` (Block List, Max: 1) Specifies the maximum desired number of pods the job should run at any given time. The actual number of pods running in steady state will be less than this number when ((.spec.completions - .status.successful) < .spec.parallelism), i.e. when the work left to do is less than max parallelism. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/ (see [below for nested schema](#nestedblock--spec--pod_failure_policy))
- `selector` (Block List, Max: 1) A label query over volumes to consider for binding. (see [below for nested schema](#nestedblock--spec--selector))
- `suspend` (Boolean) Tells the controller to suspend subsequent executions, and terminate all active executions. Defaults to false. More info: https://kubernetes.io/docs/concepts/workloads/controllers/job/#suspending-a-job
- `ttl_seconds_after_finished` (String) ttlSecondsAfterFinished limits the lifetime of a Job that has finished execution (either Complete or Failed). If this field is set, ttlSecondsAfterFinished after the Job finishes, it is eligible to be automatically deleted. When the Job is being deleted, its lifecycle guarantees (e.g. finalizers) will be honored. If this field is unset, the Job won't be automatically deleted. If this field is set to zero, the Job becomes eligible to be deleted immediately after it finishes.

<a id="nestedblock--spec--template"></a>
Expand Down
14 changes: 14 additions & 0 deletions kubernetes/resource_kubernetes_job_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import (
"k8s.io/client-go/kubernetes"
)

const (
waitForCompletionSuspendError = `cannot set both wait_for_completion and spec.suspend to true`
)

func resourceKubernetesJobV1() *schema.Resource {
return &schema.Resource{
Description: "A Job creates one or more Pods and ensures that a specified number of them successfully terminate. As pods successfully complete, the Job tracks the successful completions. When a specified number of successful completions is reached, the task (ie, Job) is complete. Deleting a Job will clean up the Pods it created. A simple case is to create one Job object in order to reliably run one Pod to completion. The Job object will start a new Pod if the first Pod fails or is deleted (for example due to a node hardware failure or a node reboot. You can also use a Job to run multiple Pods in parallel. ",
Expand All @@ -45,6 +49,16 @@ func resourceKubernetesJobV1() *schema.Resource {
Delete: schema.DefaultTimeout(1 * time.Minute),
},
Schema: resourceKubernetesJobV1Schema(),
CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error {
// wait_for_completion and suspend cannot be both set to true
if !diff.HasChange("wait_for_completion") && !diff.HasChange("spec.0.suspend") {
return nil
}
if diff.Get("wait_for_completion").(bool) && diff.Get("spec.0.suspend").(bool) {
return fmt.Errorf(waitForCompletionSuspendError)
}
return nil
},
}
}

Expand Down
142 changes: 136 additions & 6 deletions kubernetes/resource_kubernetes_job_v1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package kubernetes
import (
"context"
"fmt"
"regexp"
"testing"
"time"

Expand Down Expand Up @@ -42,6 +43,7 @@ func TestAccKubernetesJobV1_wait_for_completion(t *testing.T) {
testAccCheckJobV1Waited(time.Duration(10)*time.Second),
testAccCheckKubernetesJobV1Exists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "wait_for_completion", "true"),
resource.TestCheckResourceAttr(resourceName, "spec.0.suspend", "false"),
),
},
},
Expand Down Expand Up @@ -90,6 +92,7 @@ func TestAccKubernetesJobV1_basic(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "spec.0.pod_failure_policy.0.rule.1.action", "Ignore"),
resource.TestCheckResourceAttr(resourceName, "spec.0.pod_failure_policy.0.rule.1.on_pod_condition.0.type", "DisruptionTarget"),
resource.TestCheckResourceAttr(resourceName, "spec.0.pod_failure_policy.0.rule.1.on_pod_condition.0.status", "False"),
resource.TestCheckResourceAttr(resourceName, "spec.0.suspend", "false"),
),
},
{
Expand All @@ -110,6 +113,7 @@ func TestAccKubernetesJobV1_basic(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "spec.0.manual_selector", "true"),
resource.TestCheckResourceAttr(resourceName, "spec.0.template.0.spec.0.container.0.name", "hello"),
resource.TestCheckResourceAttr(resourceName, "spec.0.template.0.spec.0.container.0.image", imageName),
resource.TestCheckResourceAttr(resourceName, "spec.0.suspend", "false"),
resource.TestCheckResourceAttr(resourceName, "wait_for_completion", "false"),
),
},
Expand Down Expand Up @@ -157,40 +161,49 @@ func TestAccKubernetesJobV1_update(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "spec.0.pod_failure_policy.0.rule.1.action", "Ignore"),
resource.TestCheckResourceAttr(resourceName, "spec.0.pod_failure_policy.0.rule.1.on_pod_condition.0.type", "DisruptionTarget"),
resource.TestCheckResourceAttr(resourceName, "spec.0.pod_failure_policy.0.rule.1.on_pod_condition.0.status", "False"),
resource.TestCheckResourceAttr(resourceName, "spec.0.suspend", "false"),
),
},
{
Config: testAccKubernetesJobV1Config_updateMutableFields(name, imageName, "121", "4", "false", "2"),
Config: testAccKubernetesJobV1Config_updateMutableFields(name, imageName, "121", "4", "false", "2", "false"),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesJobV1Exists(resourceName, &conf2),
resource.TestCheckResourceAttr(resourceName, "spec.0.active_deadline_seconds", "121"),
testAccCheckKubernetesJobV1ForceNew(&conf1, &conf2, false),
),
},
{
Config: testAccKubernetesJobV1Config_updateMutableFields(name, imageName, "121", "5", "false", "2"),
Config: testAccKubernetesJobV1Config_updateMutableFields(name, imageName, "121", "5", "false", "2", "false"),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesJobV1Exists(resourceName, &conf2),
resource.TestCheckResourceAttr(resourceName, "spec.0.backoff_limit", "5"),
testAccCheckKubernetesJobV1ForceNew(&conf1, &conf2, false),
),
},
{
Config: testAccKubernetesJobV1Config_updateMutableFields(name, imageName, "121", "5", "true", "2"),
Config: testAccKubernetesJobV1Config_updateMutableFields(name, imageName, "121", "5", "true", "2", "false"),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesJobV1Exists(resourceName, &conf2),
resource.TestCheckResourceAttr(resourceName, "spec.0.manual_selector", "true"),
testAccCheckKubernetesJobV1ForceNew(&conf1, &conf2, false),
),
},
{
Config: testAccKubernetesJobV1Config_updateMutableFields(name, imageName, "121", "5", "true", "3"),
Config: testAccKubernetesJobV1Config_updateMutableFields(name, imageName, "121", "5", "true", "3", "false"),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesJobV1Exists(resourceName, &conf2),
resource.TestCheckResourceAttr(resourceName, "spec.0.parallelism", "3"),
testAccCheckKubernetesJobV1ForceNew(&conf1, &conf2, false),
),
},
{
Config: testAccKubernetesJobV1Config_updateMutableFields(name, imageName, "121", "5", "true", "3", "true"),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesJobV1Exists(resourceName, &conf2),
resource.TestCheckResourceAttr(resourceName, "spec.0.suspend", "true"),
testAccCheckKubernetesJobV1ForceNew(&conf1, &conf2, false),
),
},
{
Config: testAccKubernetesJobV1Config_updateImmutableFields(name, imageName, "6"),
Check: resource.ComposeAggregateTestCheckFunc(
Expand Down Expand Up @@ -237,6 +250,68 @@ func TestAccKubernetesJobV1_ttl_seconds_after_finished(t *testing.T) {
})
}

func TestAccKubernetesJobV1_suspend(t *testing.T) {
var conf batchv1.Job
name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum))
imageName := busyboxImage
resourceName := "kubernetes_job_v1.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
skipIfClusterVersionLessThan(t, "1.24.0")
},
IDRefreshName: resourceName,
IDRefreshIgnore: []string{"metadata.0.resource_version"},
ProviderFactories: testAccProviderFactories,
CheckDestroy: testAccCheckKubernetesJobV1Destroy,
Steps: []resource.TestStep{
{
Config: testAccKubernetesJobV1Config_suspend(name, imageName),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckKubernetesJobV1Exists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "spec.0.suspend", "true"),
),
},
{
Config: testAccKubernetesJobV1Config_wait_for_completion(name, imageName),
Check: resource.ComposeAggregateTestCheckFunc(
// NOTE this is to check that Terraform actually waited for the Job to complete
// before considering the Job resource as created
testAccCheckJobV1Waited(time.Duration(10)*time.Second),
testAccCheckKubernetesJobV1Exists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "wait_for_completion", "true"),
resource.TestCheckResourceAttr(resourceName, "spec.0.suspend", "false"),
),
},
},
})
}

func TestAccKubernetesJobV1_suspendExpectErrors(t *testing.T) {
name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum))
imageName := busyboxImage
resourceName := "kubernetes_job_v1.test"
wantError := waitForCompletionSuspendError

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
skipIfClusterVersionLessThan(t, "1.24.0")
},
IDRefreshName: resourceName,
IDRefreshIgnore: []string{"metadata.0.resource_version"},
ProviderFactories: testAccProviderFactories,
CheckDestroy: testAccCheckKubernetesJobV1Destroy,
Steps: []resource.TestStep{
{ // Expect an error when both `wait_for_completion` and `suspend` are set to true.
Config: testAccKubernetesJobV1Config_suspendExpectErrors(name, imageName),
ExpectError: regexp.MustCompile(wantError),
},
},
})
}

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
Expand Down Expand Up @@ -366,7 +441,7 @@ func testAccKubernetesJobV1Config_basic(name, imageName string) string {
}`, name, imageName)
}

func testAccKubernetesJobV1Config_updateMutableFields(name, imageName, activeDeadlineSeconds, backoffLimit, manualSelector, parallelism string) string {
func testAccKubernetesJobV1Config_updateMutableFields(name, imageName, activeDeadlineSeconds, backoffLimit, manualSelector, parallelism, suspend string) string {
return fmt.Sprintf(`resource "kubernetes_job_v1" "test" {
metadata {
name = "%s"
Expand All @@ -377,6 +452,7 @@ func testAccKubernetesJobV1Config_updateMutableFields(name, imageName, activeDea
completions = 4
manual_selector = %s
parallelism = %s
suspend = %s
pod_failure_policy {
rule {
action = "FailJob"
Expand Down Expand Up @@ -407,7 +483,7 @@ func testAccKubernetesJobV1Config_updateMutableFields(name, imageName, activeDea
}

wait_for_completion = false
}`, name, activeDeadlineSeconds, backoffLimit, manualSelector, parallelism, imageName)
}`, name, activeDeadlineSeconds, backoffLimit, manualSelector, parallelism, suspend, imageName)
}

func testAccKubernetesJobV1Config_updateImmutableFields(name, imageName, completions string) string {
Expand Down Expand Up @@ -457,6 +533,60 @@ func testAccKubernetesJobV1Config_ttl_seconds_after_finished(name, imageName str
}`, name, imageName)
}

func testAccKubernetesJobV1Config_suspend(name, imageName string) string {
return fmt.Sprintf(`resource "kubernetes_job_v1" "test" {
metadata {
name = "%s"
}
spec {
suspend = true
template {
metadata {
name = "wait-test"
}
spec {
container {
name = "wait-test"
image = "%s"
command = ["sleep", "10"]
}
}
}
}
wait_for_completion = false
timeouts {
create = "1m"
}
}`, name, imageName)
}

func testAccKubernetesJobV1Config_suspendExpectErrors(name, imageName string) string {
return fmt.Sprintf(`resource "kubernetes_job_v1" "test" {
metadata {
name = "%s"
}
spec {
suspend = true
template {
metadata {
name = "wait-test"
}
spec {
container {
name = "wait-test"
image = "%s"
command = ["sleep", "10"]
}
}
}
}
wait_for_completion = true
timeouts {
create = "1m"
}
}`, name, imageName)
}

func testAccKubernetesJobV1Config_wait_for_completion(name, imageName string) string {
return fmt.Sprintf(`resource "kubernetes_job_v1" "test" {
metadata {
Expand Down
6 changes: 6 additions & 0 deletions kubernetes/schema_job_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,12 @@ func jobSpecFields(specUpdatable bool) map[string]*schema.Schema {
},
},
},
"suspend": {
Type: schema.TypeBool,
Optional: true,
ForceNew: false,
Description: "Tells the controller to suspend subsequent executions, and terminate all active executions. Defaults to false. More info: https://kubernetes.io/docs/concepts/workloads/controllers/job/#suspending-a-job",
},
// PodTemplate fields are immutable in Jobs.
"template": {
Type: schema.TypeList,
Expand Down
16 changes: 16 additions & 0 deletions kubernetes/structure_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ func flattenJobV1Spec(in batchv1.JobSpec, d *schema.ResourceData, meta interface
att["selector"] = flattenLabelSelector(in.Selector)
}

if in.Suspend != nil {
att["suspend"] = *in.Suspend
}

removeGeneratedLabels(in.Template.ObjectMeta.Labels)

podSpec, err := flattenPodTemplateSpec(in.Template)
Expand Down Expand Up @@ -120,6 +124,10 @@ func expandJobV1Spec(j []interface{}) (batchv1.JobSpec, error) {
obj.Selector = expandLabelSelector(v)
}

if v, ok := in["suspend"].(bool); ok {
obj.Suspend = ptr.To(v)
}

template, err := expandPodTemplate(in["template"].([]interface{}))
if err != nil {
return obj, err
Expand Down Expand Up @@ -330,6 +338,14 @@ func patchJobV1Spec(pathPrefix, prefix string, d *schema.ResourceData) PatchOper
})
}

if d.HasChange(prefix + "suspend") {
v := d.Get(prefix + "suspend").(bool)
ops = append(ops, &ReplaceOperation{
Path: pathPrefix + "/suspend",
Value: v,
})
}

return ops
}

Expand Down