Repository: furiko-io/furiko
Pull request #79 (2022-05-06 16:41:48)

chore(api): Update JobTemplate APIs
- Swap JobTemplate <-> JobTemplateSpec naming
- Flatten TaskSpec into JobTemplate to avoid too deeply nested APIs


Patch:
diff --git a/apis/execution/v1alpha1/job_types.go b/apis/execution/v1alpha1/job_types.go
index ce716e6..c292e8c 100644
--- a/apis/execution/v1alpha1/job_types.go
+++ b/apis/execution/v1alpha1/job_types.go
@@ -49,7 +49,7 @@ type JobSpec struct {
 
 	// Template specifies how to create the Job.
 	// +optional
-	Template *JobTemplateSpec `json:"template,omitempty"`
+	Template *JobTemplate `json:"template,omitempty"`
 
 	// Specifies key-values pairs of values for Options, in JSON or YAML format.
 	//
@@ -126,8 +126,8 @@ type StartPolicySpec struct {
 	StartAfter *metav1.Time `json:"startAfter,omitempty"`
 }
 
-// JobTemplate specifies how to create a Job with metadata.
-type JobTemplate struct {
+// JobTemplateSpec specifies how to create a Job with metadata.
+type JobTemplateSpec struct {
 	// Standard object's metadata that will be added to Job. More info:
 	// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
 	//
@@ -136,13 +136,13 @@ type JobTemplate struct {
 	metav1.ObjectMeta `json:"metadata,omitempty"`
 
 	// Specification of the desired behavior of the job.
-	Spec JobTemplateSpec `json:"spec"`
+	Spec JobTemplate `json:"spec"`
 }
 
-// JobTemplateSpec specifies how to create the Job.
-type JobTemplateSpec struct {
-	// Describes the tasks to be created for the Job.
-	Task TaskSpec `json:"task"`
+// JobTemplate specifies how to create the Job.
+type JobTemplate struct {
+	// Defines the template to create a single task in the Job.
+	TaskTemplate TaskTemplate `json:"taskTemplate"`
 
 	// Specifies maximum number of attempts for the Job. Each attempt will create a
 	// single task at a time, and if the task fails, the controller will wait
@@ -159,6 +159,29 @@ type JobTemplateSpec struct {
 	//
 	// +optional
 	RetryDelaySeconds *int64 `json:"retryDelaySeconds,omitempty"`
+
+	// Optional duration in seconds to wait before terminating the task if it is
+	// still pending. This field is useful to prevent jobs from being stuck forever
+	// if the Job has a deadline to start running by. If not set, it will be set to
+	// the DefaultPendingTimeoutSeconds configuration value in the controller. To
+	// disable pending timeout, set this to 0.
+	//
+	// +optional
+	TaskPendingTimeoutSeconds *int64 `json:"taskPendingTimeoutSeconds,omitempty"`
+
+	// Defines whether tasks are allowed to be force deleted or not. If the node is
+	// unresponsive, it may be possible that the task cannot be killed by normal
+	// graceful deletion. The controller may choose to force delete the task, which
+	// would ignore the final state of the task since the node is unable to return
+	// whether the task is actually still alive.
+	//
+	// If not set to true, there may be some cases when there may actually be two
+	// concurrently running tasks when even when ConcurrencyPolicyForbid. Setting
+	// this to true would prevent this from happening, but the Job may remain stuck
+	// indefinitely until the node recovers.
+	//
+	// +optional
+	ForbidTaskForceDeletion bool `json:"forbidTaskForceDeletion,omitempty"`
 }
 
 // JobStatus defines the observed state of a Job.
diff --git a/apis/execution/v1alpha1/jobconfig_types.go b/apis/execution/v1alpha1/jobconfig_types.go
index 4cb83cd..2e4db01 100644
--- a/apis/execution/v1alpha1/jobconfig_types.go
+++ b/apis/execution/v1alpha1/jobconfig_types.go
@@ -26,7 +26,7 @@ import (
 // JobConfigSpec defines the desired state of the JobConfig.
 type JobConfigSpec struct {
 	// Template for creating the Job.
-	Template JobTemplate `json:"template"`
+	Template JobTemplateSpec `json:"template"`
 
 	// Concurrency defines the behaviour of multiple concurrent Jobs.
 	Concurrency ConcurrencySpec `json:"concurrency"`
diff --git a/apis/execution/v1alpha1/task_types.go b/apis/execution/v1alpha1/task_types.go
index d9000c1..eecec43 100644
--- a/apis/execution/v1alpha1/task_types.go
+++ b/apis/execution/v1alpha1/task_types.go
@@ -21,34 +21,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-// TaskSpec describes a single task in the Job.
-type TaskSpec struct {
-	// Defines the template to create the task.
-	Template TaskTemplate `json:"template"`
-
-	// Optional duration in seconds to wait before terminating the task if it is
-	// still pending. This field is useful to prevent jobs from being stuck forever
-	// if the Job has a deadline to start running by. If not set, it will be set to
-	// the DefaultPendingTimeoutSeconds configuration value in the controller. To
-	// disable pending timeout, set this to 0.
-	//
-	// +optional
-	PendingTimeoutSeconds *int64 `json:"pendingTimeoutSeconds,omitempty"`
-
-	// ForbidForceDeletion, if true, means that tasks are not allowed to be
-	// force deleted. If the node is unresponsive, it may be possible that the task
-	// cannot be killed by normal graceful deletion. The controller may choose to
-	// force delete the task, which would ignore the final state of the task since
-	// the node is unable to return whether the task is actually still alive.
-	//
-	// As such, if not set to true, the Forbid ConcurrencyPolicy may in some cases
-	// be violated. Setting this to true would prevent this from happening, but the
-	// Job may remain in Killing indefinitely until the node recovers.
-	//
-	// +optional
-	ForbidForceDeletion bool `json:"forbidForceDeletion,omitempty"`
-}
-
 // TaskTemplate defines how to create a single task for this Job. Exactly one
 // field must be specified.
 type TaskTemplate struct {
diff --git a/apis/execution/v1alpha1/zz_generated.deepcopy.go b/apis/execution/v1alpha1/zz_generated.deepcopy.go
index 3058225..9c95288 100644
--- a/apis/execution/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/execution/v1alpha1/zz_generated.deepcopy.go
@@ -403,7 +403,7 @@ func (in *JobSpec) DeepCopyInto(out *JobSpec) {
 	}
 	if in.Template != nil {
 		in, out := &in.Template, &out.Template
-		*out = new(JobTemplateSpec)
+		*out = new(JobTemplate)
 		(*in).DeepCopyInto(*out)
 	}
 	if in.Substitutions != nil {
@@ -464,8 +464,22 @@ func (in *JobStatus) DeepCopy() *JobStatus {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *JobTemplate) DeepCopyInto(out *JobTemplate) {
 	*out = *in
-	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
-	in.Spec.DeepCopyInto(&out.Spec)
+	in.TaskTemplate.DeepCopyInto(&out.TaskTemplate)
+	if in.MaxAttempts != nil {
+		in, out := &in.MaxAttempts, &out.MaxAttempts
+		*out = new(int64)
+		**out = **in
+	}
+	if in.RetryDelaySeconds != nil {
+		in, out := &in.RetryDelaySeconds, &out.RetryDelaySeconds
+		*out = new(int64)
+		**out = **in
+	}
+	if in.TaskPendingTimeoutSeconds != nil {
+		in, out := &in.TaskPendingTimeoutSeconds, &out.TaskPendingTimeoutSeconds
+		*out = new(int64)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobTemplate.
@@ -481,17 +495,8 @@ func (in *JobTemplate) DeepCopy() *JobTemplate {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *JobTemplateSpec) DeepCopyInto(out *JobTemplateSpec) {
 	*out = *in
-	in.Task.DeepCopyInto(&out.Task)
-	if in.MaxAttempts != nil {
-		in, out := &in.MaxAttempts, &out.MaxAttempts
-		*out = new(int64)
-		**out = **in
-	}
-	if in.RetryDelaySeconds != nil {
-		in, out := &in.RetryDelaySeconds, &out.RetryDelaySeconds
-		*out = new(int64)
-		**out = **in
-	}
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobTemplateSpec.
@@ -764,27 +769,6 @@ func (in *TaskRef) DeepCopy() *TaskRef {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *TaskSpec) DeepCopyInto(out *TaskSpec) {
-	*out = *in
-	in.Template.DeepCopyInto(&out.Template)
-	if in.PendingTimeoutSeconds != nil {
-		in, out := &in.PendingTimeoutSeconds, &out.PendingTimeoutSeconds
-		*out = new(int64)
-		**out = **in
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TaskSpec.
-func (in *TaskSpec) DeepCopy() *TaskSpec {
-	if in == nil {
-		return nil
-	}
-	out := new(TaskSpec)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *TaskStatus) DeepCopyInto(out *TaskStatus) {
 	*out = *in
diff --git a/config/crd/bases/execution.furiko.io_jobconfigs.yaml b/config/crd/bases/execution.furiko.io_jobconfigs.yaml
index 29b9fd8..d07750d 100644
--- a/config/crd/bases/execution.furiko.io_jobconfigs.yaml
+++ b/config/crd/bases/execution.furiko.io_jobconfigs.yaml
@@ -209,6 +209,9 @@ spec:
                     spec:
                       description: Specification of the desired behavior of the job.
                       properties:
+                        forbidTaskForceDeletion:
+                          description: "Defines whether tasks are allowed to be force deleted or not. If the node is unresponsive, it may be possible that the task cannot be killed by normal graceful deletion. The controller may choose to force delete the task, which would ignore the final state of the task since the node is unable to return whether the task is actually still alive. \n If not set to true, there may be some cases when there may actually be two concurrently running tasks when even when ConcurrencyPolicyForbid. Setting this to true would prevent this from happening, but the Job may remain stuck indefinitely until the node recovers."
+                          type: boolean
                         maxAttempts:
                           description: Specifies maximum number of attempts for the Job. Each attempt will create a single task at a time, and if the task fails, the controller will wait retryDelaySeconds before creating the next task attempt. Once maxAttempts is reached, the Job terminates in RetryLimitExceeded. Value must be a positive integer. Defaults to 1.
                           format: int64
@@ -217,38 +220,29 @@ spec:
                           description: Optional duration in seconds to wait between retries. If left empty or zero, it means no delay (i.e. retry immediately). Value must be a non-negative integer.
                           format: int64
                           type: integer
-                        task:
-                          description: Describes the tasks to be created for the Job.
+                        taskPendingTimeoutSeconds:
+                          description: Optional duration in seconds to wait before terminating the task if it is still pending. This field is useful to prevent jobs from being stuck forever if the Job has a deadline to start running by. If not set, it will be set to the DefaultPendingTimeoutSeconds configuration value in the controller. To disable pending timeout, set this to 0.
+                          format: int64
+                          type: integer
+                        taskTemplate:
+                          description: Defines the template to create a single task in the Job.
                           properties:
-                            forbidForceDeletion:
-                              description: "ForbidForceDeletion, if true, means that tasks are not allowed to be force deleted. If the node is unresponsive, it may be possible that the task cannot be killed by normal graceful deletion. The controller may choose to force delete the task, which would ignore the final state of the task since the node is unable to return whether the task is actually still alive. \n As such, if not set to true, the Forbid ConcurrencyPolicy may in some cases be violated. Setting this to true would prevent this from happening, but the Job may remain in Killing indefinitely until the node recovers."
-                              type: boolean
-                            pendingTimeoutSeconds:
-                              description: Optional duration in seconds to wait before terminating the task if it is still pending. This field is useful to prevent jobs from being stuck forever if the Job has a deadline to start running by. If not set, it will be set to the DefaultPendingTimeoutSeconds configuration value in the controller. To disable pending timeout, set this to 0.
-                              format: int64
-                              type: integer
-                            template:
-                              description: Defines the template to create the task.
+                            pod:
+                              description: Describes how to create tasks as Pods.
                               properties:
-                                pod:
-                                  description: Describes how to create tasks as Pods.
-                                  properties:
-                                    metadata:
-                                      description: 'Standard object''s metadata that will be added to Pod. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata'
-                                      type: object
-                                      x-kubernetes-preserve-unknown-fields: true
-                                    spec:
-                                      description: "Specification of the desired behavior of the pod. API docs: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#podspec-v1-core \n Supports context variable substitution in the following fields for containers and initContainers: image, command, args, env.value"
-                                      type: object
-                                      x-kubernetes-map-type: atomic
-                                      x-kubernetes-preserve-unknown-fields: true
+                                metadata:
+                                  description: 'Standard object''s metadata that will be added to Pod. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata'
                                   type: object
+                                  x-kubernetes-preserve-unknown-fields: true
+                                spec:
+                                  description: "Specification of the desired behavior of the pod. API docs: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#podspec-v1-core \n Supports context variable substitution in the following fields for containers and initContainers: image, command, args, env.value"
+                                  type: object
+                                  x-kubernetes-map-type: atomic
+                                  x-kubernetes-preserve-unknown-fields: true
                               type: object
-                          required:
-                            - template
                           type: object
                       required:
-                        - task
+                        - taskTemplate
                       type: object
                   required:
                     - spec
diff --git a/config/crd/bases/execution.furiko.io_jobs.yaml b/config/crd/bases/execution.furiko.io_jobs.yaml
index e0f3c98..eaa0299 100644
--- a/config/crd/bases/execution.furiko.io_jobs.yaml
+++ b/config/crd/bases/execution.furiko.io_jobs.yaml
@@ -81,6 +81,9 @@ spec:
                 template:
                   description: Template specifies how to create the Job.
                   properties:
+                    forbidTaskForceDeletion:
+                      description: "Defines whether tasks are allowed to be force deleted or not. If the node is unresponsive, it may be possible that the task cannot be killed by normal graceful deletion. The controller may choose to force delete the task, which would ignore the final state of the task since the node is unable to return whether the task is actually still alive. \n If not set to true, there may be some cases when there may actually be two concurrently running tasks when even when ConcurrencyPolicyForbid. Setting this to true would prevent this from happening, but the Job may remain stuck indefinitely until the node recovers."
+                      type: boolean
                     maxAttempts:
                       description: Specifies maximum number of attempts for the Job. Each attempt will create a single task at a time, and if the task fails, the controller will wait retryDelaySeconds before creating the next task attempt. Once maxAttempts is reached, the Job terminates in RetryLimitExceeded. Value must be a positive integer. Defaults to 1.
                       format: int64
@@ -89,38 +92,29 @@ spec:
                       description: Optional duration in seconds to wait between retries. If left empty or zero, it means no delay (i.e. retry immediately). Value must be a non-negative integer.
                       format: int64
                       type: integer
-                    task:
-                      description: Describes the tasks to be created for the Job.
+                    taskPendingTimeoutSeconds:
+                      description: Optional duration in seconds to wait before terminating the task if it is still pending. This field is useful to prevent jobs from being stuck forever if the Job has a deadline to start running by. If not set, it will be set to the DefaultPendingTimeoutSeconds configuration value in the controller. To disable pending timeout, set this to 0.
+                      format: int64
+                      type: integer
+                    taskTemplate:
+                      description: Defines the template to create a single task in the Job.
                       properties:
-                        forbidForceDeletion:
-                          description: "ForbidForceDeletion, if true, means that tasks are not allowed to be force deleted. If the node is unresponsive, it may be possible that the task cannot be killed by normal graceful deletion. The controller may choose to force delete the task, which would ignore the final state of the task since the node is unable to return whether the task is actually still alive. \n As such, if not set to true, the Forbid ConcurrencyPolicy may in some cases be violated. Setting this to true would prevent this from happening, but the Job may remain in Killing indefinitely until the node recovers."
-                          type: boolean
-                        pendingTimeoutSeconds:
-                          description: Optional duration in seconds to wait before terminating the task if it is still pending. This field is useful to prevent jobs from being stuck forever if the Job has a deadline to start running by. If not set, it will be set to the DefaultPendingTimeoutSeconds configuration value in the controller. To disable pending timeout, set this to 0.
-                          format: int64
-                          type: integer
-                        template:
-                          description: Defines the template to create the task.
+                        pod:
+                          description: Describes how to create tasks as Pods.
                           properties:
-                            pod:
-                              description: Describes how to create tasks as Pods.
-                              properties:
-                                metadata:
-                                  description: 'Standard object''s metadata that will be added to Pod. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata'
-                                  type: object
-                                  x-kubernetes-preserve-unknown-fields: true
-                                spec:
-                                  description: "Specification of the desired behavior of the pod. API docs: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#podspec-v1-core \n Supports context variable substitution in the following fields for containers and initContainers: image, command, args, env.value"
-                                  type: object
-                                  x-kubernetes-map-type: atomic
-                                  x-kubernetes-preserve-unknown-fields: true
+                            metadata:
+                              description: 'Standard object''s metadata that will be added to Pod. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata'
                               type: object
+                              x-kubernetes-preserve-unknown-fields: true
+                            spec:
+                              description: "Specification of the desired behavior of the pod. API docs: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#podspec-v1-core \n Supports context variable substitution in the following fields for containers and initContainers: image, command, args, env.value"
+                              type: object
+                              x-kubernetes-map-type: atomic
+                              x-kubernetes-preserve-unknown-fields: true
                           type: object
-                      required:
-                        - template
                       type: object
                   required:
-                    - task
+                    - taskTemplate
                   type: object
                 ttlSecondsAfterFinished:
                   description: Specifies the maximum lifetime of a Job that is finished. If not set, it will be set to the DefaultTTLSecondsAfterFinished configuration value in the controller.
diff --git a/config/samples/execution_v1alpha1_job.yaml b/config/samples/execution_v1alpha1_job.yaml
index 6ba292c..19be9ce 100644
--- a/config/samples/execution_v1alpha1_job.yaml
+++ b/config/samples/execution_v1alpha1_job.yaml
@@ -20,30 +20,28 @@ spec:
     # Defines how long to wait between retries.
     retryDelaySeconds: 15
 
-    task:
-      # Maximum duration that a task can be Pending for.
-      pendingTimeoutSeconds: 1800
+    # Maximum duration that a task can be Pending for.
+    taskPendingTimeoutSeconds: 1800
 
-      # Template for creating the task as a Pod.
-      template:
-        # Defines how to create the task as a Pod.
-        pod:
-          spec:
-            restartPolicy: Never
-            containers:
-              - name: job-container
-                args:
-                  - echo
-                  - "Hello world, ${option.user-name}!"
-                env:
-                  - name: JOB_NAME
-                    value: "${job.name}"
-                  - name: TASK_NAME
-                    value: "${task.name}"
-                  - name: TASK_RETRY_INDEX
-                    value: "${task.retry_index}"
-                image: "alpine:${option.image-tag}"
-                resources:
-                  limits:
-                    cpu: 100m
-                    memory: 64Mi
+    # Defines a template to create a task as a Pod.
+    taskTemplate:
+      pod:
+        spec:
+          restartPolicy: Never
+          containers:
+            - name: job-container
+              args:
+                - echo
+                - "Hello world, ${option.user-name}!"
+              env:
+                - name: JOB_NAME
+                  value: "${job.name}"
+                - name: TASK_NAME
+                  value: "${task.name}"
+                - name: TASK_RETRY_INDEX
+                  value: "${task.retry_index}"
+              image: "alpine:${option.image-tag}"
+              resources:
+                limits:
+                  cpu: 100m
+                  memory: 64Mi
diff --git a/config/samples/execution_v1alpha1_jobconfig.yaml b/config/samples/execution_v1alpha1_jobconfig.yaml
index ef5ccef..9ce9b4d 100644
--- a/config/samples/execution_v1alpha1_jobconfig.yaml
+++ b/config/samples/execution_v1alpha1_jobconfig.yaml
@@ -55,36 +55,34 @@ spec:
         app.kubernetes.io/name: jobconfig-sample
 
     spec:
-      # Defines task options.
-      task:
-        # Defines an explicit pending timeout of 10 minutes per task created.
-        pendingTimeoutSeconds: 600
+      # Defines an explicit pending timeout of 10 minutes per task created.
+      taskPendingTimeoutSeconds: 600
 
-        # Defines a template to create a task as a Pod.
-        template:
-          pod:
-            # Metadata that should be added to the Pods.
-            metadata:
-              annotations:
-                app.kubernetes.io/name: jobconfig-sample-pod
+      # Defines a template to create a task as a Pod.
+      taskTemplate:
+        pod:
+          # Metadata that should be added to the Pods.
+          metadata:
+            annotations:
+              app.kubernetes.io/name: jobconfig-sample-pod
 
-            # Specification of the Pod.
-            # Any context variables (including job options) will be substituted when the Pod is created.
-            spec:
-              containers:
-                - name: job-container
-                  args:
-                    - echo
-                    - "Hello world, ${option.user-name}!"
-                  env:
-                    - name: JOBCONFIG_NAME
-                      value: "${jobconfig.name}"
-                    - name: JOB_NAME
-                      value: "${job.name}"
-                    - name: TASK_NAME
-                      value: "${task.name}"
-                  image: "alpine:${option.image-tag}"
-                  resources:
-                    limits:
-                      cpu: 100m
-                      memory: 64Mi
+          # Specification of the Pod.
+          # Any context variables (including job options) will be substituted when the Pod is created.
+          spec:
+            containers:
+              - name: job-container
+                args:
+                  - echo
+                  - "Hello world, ${option.user-name}!"
+                env:
+                  - name: JOBCONFIG_NAME
+                    value: "${jobconfig.name}"
+                  - name: JOB_NAME
+                    value: "${job.name}"
+                  - name: TASK_NAME
+                    value: "${task.name}"
+                image: "alpine:${option.image-tag}"
+                resources:
+                  limits:
+                    cpu: 100m
+                    memory: 64Mi
diff --git a/pkg/execution/controllers/jobcontroller/reconciler.go b/pkg/execution/controllers/jobcontroller/reconciler.go
index 46c1641..5b6132f 100644
--- a/pkg/execution/controllers/jobcontroller/reconciler.go
+++ b/pkg/execution/controllers/jobcontroller/reconciler.go
@@ -632,7 +632,7 @@ func (w *Reconciler) handleForceDeleteKillingTasks(
 
 	// This Job's tasks cannot be force deleted.
 	// Do not continue with the rest of the routine.
-	if rj.Spec.Template.Task.ForbidForceDeletion {
+	if rj.Spec.Template.ForbidTaskForceDeletion {
 		// TODO(irvinlim): Check if we need special handling here.
 		return rj, nil
 	}
diff --git a/pkg/execution/mutation/mutation.go b/pkg/execution/mutation/mutation.go
index 04f84a2..f0fb46b 100644
--- a/pkg/execution/mutation/mutation.go
+++ b/pkg/execution/mutation/mutation.go
@@ -125,9 +125,9 @@ func (m *Mutator) MutateJob(rj *v1alpha1.Job) *webhook.Result {
 		rj.Spec.TTLSecondsAfterFinished = cfg.DefaultTTLSecondsAfterFinished
 	}
 
-	// Specify default values for JobTemplateSpec.
+	// Specify default values for JobTemplate.
 	if rj.Spec.Template == nil {
-		rj.Spec.Template = &v1alpha1.JobTemplateSpec{}
+		rj.Spec.Template = &v1alpha1.JobTemplate{}
 	}
 	result.Merge(m.MutateJobTemplateSpec(rj.Spec.Template, field.NewPath("spec", "template")))
 
@@ -208,12 +208,12 @@ func (m *Mutator) evaluateConfigName(rj *v1alpha1.Job, rjcName string, fldPath *
 	rj.OwnerReferences = baseJob.OwnerReferences
 	rj.Labels[jobconfig.LabelKeyJobConfigUID] = baseJob.Labels[jobconfig.LabelKeyJobConfigUID]
 
-	// Return Warnings if the JobTemplate was not empty when overriding.
-	if rj.Spec.Template != nil && !reflect.DeepEqual(rj.Spec.Template, v1alpha1.JobTemplateSpec{}) {
-		result.Warnings = append(result.Warnings, "JobTemplate was overwritten with JobConfig's template")
+	// Return Warnings if the JobTemplateSpec was not empty when overriding.
+	if rj.Spec.Template != nil && !reflect.DeepEqual(rj.Spec.Template, v1alpha1.JobTemplate{}) {
+		result.Warnings = append(result.Warnings, "JobTemplateSpec was overwritten with JobConfig's template")
 	}
 
-	// The JobTemplate will be completely overwritten by the base Job.
+	// The JobTemplateSpec will be completely overwritten by the base Job.
 	rj.Spec.Template = baseJob.Spec.Template
 
 	// If StartPolicy is not set, we should default to the JobConfig's ConcurrencyPolicy.
@@ -291,8 +291,8 @@ func (m *Mutator) evaluateOptionValues(rj *v1alpha1.Job, rjc *v1alpha1.JobConfig
 	return result
 }
 
-// MutateJobTemplateSpec mutates a JobTemplateSpec in-place.
-func (m *Mutator) MutateJobTemplateSpec(spec *v1alpha1.JobTemplateSpec, fldPath *field.Path) *webhook.Result {
+// MutateJobTemplateSpec mutates a JobTemplate in-place.
+func (m *Mutator) MutateJobTemplateSpec(spec *v1alpha1.JobTemplate, fldPath *field.Path) *webhook.Result {
 	result := webhook.NewResult()
 
 	// Add MaxAttempts if not specified.
@@ -301,8 +301,8 @@ func (m *Mutator) MutateJobTemplateSpec(spec *v1alpha1.JobTemplateSpec, fldPath
 	}
 
 	// Mutate task's PodTemplateSpec.
-	if spec.Task.Template.Pod != nil {
-		result.Merge(m.MutatePodTemplateSpec(spec.Task.Template.Pod, fldPath.Child("task", "template")))
+	if spec.TaskTemplate.Pod != nil {
+		result.Merge(m.MutatePodTemplateSpec(spec.TaskTemplate.Pod, fldPath.Child("task", "template")))
 	}
 
 	return result
diff --git a/pkg/execution/taskexecutor/podtaskexecutor/pod_client.go b/pkg/execution/taskexecutor/podtaskexecutor/pod_client.go
index 59c0a51..89da873 100644
--- a/pkg/execution/taskexecutor/podtaskexecutor/pod_client.go
+++ b/pkg/execution/taskexecutor/podtaskexecutor/pod_client.go
@@ -59,8 +59,8 @@ func (p *PodTaskClient) CreateIndex(ctx context.Context, index int64) (tasks.Tas
 	var template *corev1.PodTemplateSpec
 
 	// Get pod template
-	if jobTemplate := p.rj.Spec.Template; jobTemplate != nil && jobTemplate.Task.Template.Pod != nil {
-		template = jobTemplate.Task.Template.Pod.ConvertToCoreSpec()
+	if jobTemplate := p.rj.Spec.Template; jobTemplate != nil && jobTemplate.TaskTemplate.Pod != nil {
+		template = jobTemplate.TaskTemplate.Pod.ConvertToCoreSpec()
 	}
 	if template == nil {
 		return nil, errors.New("pod template cannot be empty")
diff --git a/pkg/execution/util/job/task_status.go b/pkg/execution/util/job/task_status.go
index 753bd49..7e24a15 100644
--- a/pkg/execution/util/job/task_status.go
+++ b/pkg/execution/util/job/task_status.go
@@ -51,7 +51,7 @@ func GetTaskRef(existing *execution.TaskRef, task tasks.Task) execution.TaskRef
 	// 	// Overwrite the message if the task is set to forbid force deletion.
 	// 	// This will create a less confusing message than a perpetual killing message without a concrete reason.
 	// 	taskRef.Status.Message =
-	// 		"ForbidForceDeletion - Node is unresponsive, but task forbidden to be force deleted by policy"
+	// 		"ForbidTaskForceDeletion - Node is unresponsive, but task forbidden to be force deleted by policy"
 	// }
 
 	// Copy Status to DeletedStatus if the TaskRef is finished.
diff --git a/pkg/execution/util/job/timeout.go b/pkg/execution/util/job/timeout.go
index 04992eb..503f7ee 100644
--- a/pkg/execution/util/job/timeout.go
+++ b/pkg/execution/util/job/timeout.go
@@ -29,7 +29,7 @@ func GetPendingTimeout(rj *execution.Job, cfg *configv1alpha1.JobExecutionConfig
 	if spec := cfg.DefaultPendingTimeoutSeconds; spec != nil {
 		sec = *spec
 	}
-	if taskPt := rj.Spec.Template.Task.PendingTimeoutSeconds; taskPt != nil && *taskPt >= 0 {
+	if taskPt := rj.Spec.Template.TaskPendingTimeoutSeconds; taskPt != nil && *taskPt >= 0 {
 		sec = *taskPt
 	}
 	return time.Duration(sec) * time.Second
diff --git a/pkg/execution/validation/validation.go b/pkg/execution/validation/validation.go
index 4ede798..a8a51ae 100644
--- a/pkg/execution/validation/validation.go
+++ b/pkg/execution/validation/validation.go
@@ -194,8 +194,8 @@ func (v *Validator) ValidateJobConfigSpec(spec *v1alpha1.JobConfigSpec, fldPath
 	return allErrs
 }
 
-// ValidateJobTemplate validates a *v1alpha1.JobTemplate.
-func (v *Validator) ValidateJobTemplate(spec *v1alpha1.JobTemplate, fldPath *field.Path) field.ErrorList {
+// ValidateJobTemplate validates a *v1alpha1.JobTemplateSpec.
+func (v *Validator) ValidateJobTemplate(spec *v1alpha1.JobTemplateSpec, fldPath *field.Path) field.ErrorList {
 	allErrs := field.ErrorList{}
 	allErrs = append(allErrs, v.ValidateJobTemplateSpec(&spec.Spec, fldPath.Child("spec"))...)
 	return allErrs
@@ -331,10 +331,10 @@ func (v *Validator) ValidateJobSpecUpdate(oldSpec, spec *v1alpha1.JobSpec, fldPa
 	return allErrs
 }
 
-// ValidateJobTemplateSpecImmutable validates that fields in a Job's *v1alpha1.JobTemplateSpec are immutable.
-func (v *Validator) ValidateJobTemplateSpecImmutable(oldTemplate, template *v1alpha1.JobTemplateSpec, fldPath *field.Path) field.ErrorList {
+// ValidateJobTemplateSpecImmutable validates that fields in a Job's *v1alpha1.JobTemplate are immutable.
+func (v *Validator) ValidateJobTemplateSpecImmutable(oldTemplate, template *v1alpha1.JobTemplate, fldPath *field.Path) field.ErrorList {
 	allErrs := field.ErrorList{}
-	allErrs = append(allErrs, apivalidation.ValidateImmutableField(template.Task, oldTemplate.Task, fldPath.Child("task"))...)
+	allErrs = append(allErrs, apivalidation.ValidateImmutableField(template.TaskTemplate, oldTemplate.TaskTemplate, fldPath.Child("taskTemplate"))...)
 	allErrs = append(allErrs, apivalidation.ValidateImmutableField(template.MaxAttempts, oldTemplate.MaxAttempts, fldPath.Child("maxAttempts"))...)
 	allErrs = append(allErrs, apivalidation.ValidateImmutableField(template.RetryDelaySeconds, oldTemplate.RetryDelaySeconds, fldPath.Child("retryDelaySeconds"))...)
 	return allErrs
@@ -380,10 +380,13 @@ func (v *Validator) ValidateStartPolicySpec(spec *v1alpha1.StartPolicySpec, fldP
 	return allErrs
 }
 
-// ValidateJobTemplateSpec validates a *v1alpha1.JobTemplateSpec.
-func (v *Validator) ValidateJobTemplateSpec(template *v1alpha1.JobTemplateSpec, fldPath *field.Path) field.ErrorList {
+// ValidateJobTemplateSpec validates a *v1alpha1.JobTemplate.
+func (v *Validator) ValidateJobTemplateSpec(template *v1alpha1.JobTemplate, fldPath *field.Path) field.ErrorList {
 	allErrs := field.ErrorList{}
-	allErrs = append(allErrs, v.ValidateJobTaskSpec(&template.Task, fldPath.Child("task"))...)
+	allErrs = append(allErrs, v.ValidateTaskTemplate(&template.TaskTemplate, fldPath.Child("taskTemplate"))...)
+	if template.TaskPendingTimeoutSeconds != nil {
+		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(*template.TaskPendingTimeoutSeconds, fldPath.Child("taskPendingTimeoutSeconds"))...)
+	}
 	if template.MaxAttempts != nil {
 		allErrs = append(allErrs, v.ValidateMaxRetryAttempts(*template.MaxAttempts, fldPath.Child("maxAttempts"))...)
 	}
@@ -406,16 +409,6 @@ func (v *Validator) ValidateMaxRetryAttempts(attempts int64, fldPath *field.Path
 	return allErrs
 }
 
-// ValidateJobTaskSpec validates a *v1alpha1.TaskSpec.
-func (v *Validator) ValidateJobTaskSpec(spec *v1alpha1.TaskSpec, fldPath *field.Path) field.ErrorList {
-	allErrs := field.ErrorList{}
-	allErrs = append(allErrs, v.ValidateTaskTemplate(&spec.Template, fldPath.Child("template"))...)
-	if spec.PendingTimeoutSeconds != nil {
-		allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(*spec.PendingTimeoutSeconds, fldPath.Child("pendingTimeoutSeconds"))...)
-	}
-	return allErrs
-}
-
 // ValidateTaskTemplate validates a *v1alpha1.TaskTemplate.
 func (v *Validator) ValidateTaskTemplate(spec *v1alpha1.TaskTemplate, fldPath *field.Path) field.ErrorList {
 	allErrs := field.ErrorList{}


Test patch:
diff --git a/pkg/execution/controllers/croncontroller/control_test.go b/pkg/execution/controllers/croncontroller/control_test.go
index 672aa06..f52f262 100644
--- a/pkg/execution/controllers/croncontroller/control_test.go
+++ b/pkg/execution/controllers/croncontroller/control_test.go
@@ -49,19 +49,17 @@ var (
 		},
 		Spec: execution.JobSpec{
 			Type: execution.JobTypeAdhoc,
-			Template: &execution.JobTemplateSpec{
-				Task: execution.TaskSpec{
-					Template: execution.TaskTemplate{
-						Pod: &execution.PodTemplateSpec{
-							Spec: corev1.PodSpec{
-								Containers: []corev1.Container{
-									{
-										Name:  "container",
-										Image: "hello-world",
-										Args: []string{
-											"echo",
-											"Hello world!",
-										},
+			Template: &execution.JobTemplate{
+				TaskTemplate: execution.TaskTemplate{
+					Pod: &execution.PodTemplateSpec{
+						Spec: corev1.PodSpec{
+							Containers: []corev1.Container{
+								{
+									Name:  "container",
+									Image: "hello-world",
+									Args: []string{
+										"echo",
+										"Hello world!",
 									},
 								},
 							},
diff --git a/pkg/execution/controllers/jobcontroller/controller_test.go b/pkg/execution/controllers/jobcontroller/controller_test.go
index bba4571..a167b29 100644
--- a/pkg/execution/controllers/jobcontroller/controller_test.go
+++ b/pkg/execution/controllers/jobcontroller/controller_test.go
@@ -62,11 +62,9 @@ var (
 		},
 		Spec: execution.JobSpec{
 			Type: execution.JobTypeAdhoc,
-			Template: &execution.JobTemplateSpec{
-				Task: execution.TaskSpec{
-					Template: execution.TaskTemplate{
-						Pod: podTemplate,
-					},
+			Template: &execution.JobTemplate{
+				TaskTemplate: execution.TaskTemplate{
+					Pod: podTemplate,
 				},
 			},
 		},
@@ -159,7 +157,7 @@ var (
 	// Job without pending timeout.
 	fakeJobWithoutPendingTimeout = func() *execution.Job {
 		newJob := fakeJobPending.DeepCopy()
-		newJob.Spec.Template.Task.PendingTimeoutSeconds = pointer.Int64(0)
+		newJob.Spec.Template.TaskPendingTimeoutSeconds = pointer.Int64(0)
 		return newJob
 	}()
 
@@ -178,7 +176,7 @@ var (
 	// Job with pod being deleted and force deletion is not allowed.
 	fakeJobPodDeletingForbidForceDeletion = func() *execution.Job {
 		newJob := fakeJobPodDeleting.DeepCopy()
-		newJob.Spec.Template.Task.ForbidForceDeletion = true
+		newJob.Spec.Template.ForbidTaskForceDeletion = true
 		return newJob
 	}()
 
diff --git a/pkg/execution/mutation/mutation_test.go b/pkg/execution/mutation/mutation_test.go
index 5021878..4a130dd 100644
--- a/pkg/execution/mutation/mutation_test.go
+++ b/pkg/execution/mutation/mutation_test.go
@@ -155,22 +155,18 @@ var (
 		},
 	}
 
-	jobTemplateSpecBasic = v1alpha1.JobTemplate{
-		Spec: v1alpha1.JobTemplateSpec{
-			Task: v1alpha1.TaskSpec{
-				Template: v1alpha1.TaskTemplate{
-					Pod: &podTemplateSpecBasic,
-				},
+	jobTemplateSpecBasic = v1alpha1.JobTemplateSpec{
+		Spec: v1alpha1.JobTemplate{
+			TaskTemplate: v1alpha1.TaskTemplate{
+				Pod: &podTemplateSpecBasic,
 			},
 		},
 	}
 
-	jobTemplateSpecBasic2 = v1alpha1.JobTemplate{
-		Spec: v1alpha1.JobTemplateSpec{
-			Task: v1alpha1.TaskSpec{
-				Template: v1alpha1.TaskTemplate{
-					Pod: &podTemplateSpecBasic2,
-				},
+	jobTemplateSpecBasic2 = v1alpha1.JobTemplateSpec{
+		Spec: v1alpha1.JobTemplate{
+			TaskTemplate: v1alpha1.TaskTemplate{
+				Pod: &podTemplateSpecBasic2,
 			},
 		},
 	}
@@ -232,16 +228,14 @@ func TestMutator_MutateJobConfig(t *testing.T) {
 			},
 		},
 		{
-			name: "don't need to default JobTemplate",
+			name: "don't need to default JobTemplateSpec",
 			rjc: &v1alpha1.JobConfig{
 				ObjectMeta: objectMetaJobConfig,
 				Spec: v1alpha1.JobConfigSpec{
-					Template: v1alpha1.JobTemplate{
-						Spec: v1alpha1.JobTemplateSpec{
-							Task: v1alpha1.TaskSpec{
-								Template: v1alpha1.TaskTemplate{
-									Pod: &podTemplateSpecBare,
-								},
+					Template: v1alpha1.JobTemplateSpec{
+						Spec: v1alpha1.JobTemplate{
+							TaskTemplate: v1alpha1.TaskTemplate{
+								Pod: &podTemplateSpecBare,
 							},
 						},
 					},
@@ -633,11 +627,9 @@ func TestMutator_MutateJob(t *testing.T) {
 			rj: &v1alpha1.Job{
 				ObjectMeta: objectMetaJob,
 				Spec: v1alpha1.JobSpec{
-					Template: &v1alpha1.JobTemplateSpec{
-						Task: v1alpha1.TaskSpec{
-							Template: v1alpha1.TaskTemplate{
-								Pod: &podTemplateSpecBasic,
-							},
+					Template: &v1alpha1.JobTemplate{
+						TaskTemplate: v1alpha1.TaskTemplate{
+							Pod: &podTemplateSpecBasic,
 						},
 					},
 				},
@@ -646,11 +638,9 @@ func TestMutator_MutateJob(t *testing.T) {
 				ObjectMeta: objectMetaJob,
 				Spec: v1alpha1.JobSpec{
 					Type: v1alpha1.JobTypeAdhoc,
-					Template: &v1alpha1.JobTemplateSpec{
-						Task: v1alpha1.TaskSpec{
-							Template: v1alpha1.TaskTemplate{
-								Pod: &podTemplateSpecBasic,
-							},
+					Template: &v1alpha1.JobTemplate{
+						TaskTemplate: v1alpha1.TaskTemplate{
+							Pod: &podTemplateSpecBasic,
 						},
 						MaxAttempts: pointer.Int64(1),
 					},
@@ -664,11 +654,9 @@ func TestMutator_MutateJob(t *testing.T) {
 				ObjectMeta: objectMetaJob,
 				Spec: v1alpha1.JobSpec{
 					Type: v1alpha1.JobTypeAdhoc,
-					Template: &v1alpha1.JobTemplateSpec{
-						Task: v1alpha1.TaskSpec{
-							Template: v1alpha1.TaskTemplate{
-								Pod: &podTemplateSpecBasic,
-							},
+					Template: &v1alpha1.JobTemplate{
+						TaskTemplate: v1alpha1.TaskTemplate{
+							Pod: &podTemplateSpecBasic,
 						},
 						MaxAttempts: pointer.Int64(1),
 					},
@@ -1119,13 +1107,11 @@ func TestMutator_MutateCreateJob(t *testing.T) {
 				ObjectMeta: objectMetaJob,
 				Spec: v1alpha1.JobSpec{
 					ConfigName: objectMetaJobConfig.Name,
-					Template: &v1alpha1.JobTemplateSpec{
-						Task: v1alpha1.TaskSpec{
-							Template: v1alpha1.TaskTemplate{
-								Pod: &podTemplateSpecBasic,
-							},
-							ForbidForceDeletion: true,
+					Template: &v1alpha1.JobTemplate{
+						TaskTemplate: v1alpha1.TaskTemplate{
+							Pod: &podTemplateSpecBasic,
 						},
+						ForbidTaskForceDeletion: true,
 					},
 				},
 			},
@@ -1136,7 +1122,7 @@ func TestMutator_MutateCreateJob(t *testing.T) {
 					StartPolicy: &startPolicyBasic,
 				},
 			},
-			wantWarnings: []string{"JobTemplate was overwritten with JobConfig's template"},
+			wantWarnings: []string{"JobTemplateSpec was overwritten with JobConfig's template"},
 		},
 		{
 			name: "mutate ConfigName cannot get JobConfig",
diff --git a/pkg/execution/mutation/patcher_jobs_test.go b/pkg/execution/mutation/patcher_jobs_test.go
index a7e359c..212c399 100644
--- a/pkg/execution/mutation/patcher_jobs_test.go
+++ b/pkg/execution/mutation/patcher_jobs_test.go
@@ -46,11 +46,9 @@ func TestNewJobPatcher(t *testing.T) {
 			rj: &v1alpha1.Job{
 				ObjectMeta: objectMetaJob,
 				Spec: v1alpha1.JobSpec{
-					Template: &v1alpha1.JobTemplateSpec{
-						Task: v1alpha1.TaskSpec{
-							Template: v1alpha1.TaskTemplate{
-								Pod: &podTemplateSpecBare,
-							},
+					Template: &v1alpha1.JobTemplate{
+						TaskTemplate: v1alpha1.TaskTemplate{
+							Pod: &podTemplateSpecBare,
 						},
 					},
 				},
@@ -59,14 +57,12 @@ func TestNewJobPatcher(t *testing.T) {
 				ObjectMeta: objectMetaJobWithFinalizer,
 				Spec: v1alpha1.JobSpec{
 					Type: v1alpha1.JobTypeAdhoc,
-					Template: &v1alpha1.JobTemplateSpec{
-						Task: v1alpha1.TaskSpec{
-							Template: v1alpha1.TaskTemplate{
-								Pod: &v1alpha1.PodTemplateSpec{
-									Spec: corev1.PodSpec{
-										Containers:    podTemplateSpecBare.Spec.Containers,
-										RestartPolicy: corev1.RestartPolicyNever,
-									},
+					Template: &v1alpha1.JobTemplate{
+						TaskTemplate: v1alpha1.TaskTemplate{
+							Pod: &v1alpha1.PodTemplateSpec{
+								Spec: corev1.PodSpec{
+									Containers:    podTemplateSpecBare.Spec.Containers,
+									RestartPolicy: corev1.RestartPolicyNever,
 								},
 							},
 						},
@@ -89,9 +85,9 @@ func TestNewJobPatcher(t *testing.T) {
 				ObjectMeta: objectMetaJobWithAllReferences,
 				Spec: v1alpha1.JobSpec{
 					Type: v1alpha1.JobTypeAdhoc,
-					Template: &v1alpha1.JobTemplateSpec{
-						Task:        jobTemplateSpecBasic.Spec.Task,
-						MaxAttempts: pointer.Int64(1),
+					Template: &v1alpha1.JobTemplate{
+						TaskTemplate: jobTemplateSpecBasic.Spec.TaskTemplate,
+						MaxAttempts:  pointer.Int64(1),
 					},
 					StartPolicy:             &startPolicyBasic,
 					TTLSecondsAfterFinished: config.DefaultJobExecutionConfig.DefaultTTLSecondsAfterFinished,
diff --git a/pkg/execution/taskexecutor/podtaskexecutor/pod_lister_test.go b/pkg/execution/taskexecutor/podtaskexecutor/pod_lister_test.go
index b54615c..bf2c2f7 100644
--- a/pkg/execution/taskexecutor/podtaskexecutor/pod_lister_test.go
+++ b/pkg/execution/taskexecutor/podtaskexecutor/pod_lister_test.go
@@ -46,11 +46,9 @@ var (
 			UID:       jobUID,
 		},
 		Spec: execution.JobSpec{
-			Template: &execution.JobTemplateSpec{
-				Task: execution.TaskSpec{
-					Template: execution.TaskTemplate{
-						Pod: &execution.PodTemplateSpec{},
-					},
+			Template: &execution.JobTemplate{
+				TaskTemplate: execution.TaskTemplate{
+					Pod: &execution.PodTemplateSpec{},
 				},
 			},
 		},
diff --git a/pkg/execution/util/job/condition_test.go b/pkg/execution/util/job/condition_test.go
index 858c4e0..fb998a2 100644
--- a/pkg/execution/util/job/condition_test.go
+++ b/pkg/execution/util/job/condition_test.go
@@ -309,7 +309,7 @@ func TestGetCondition(t *testing.T) {
 			args: args{
 				rj: &execution.Job{
 					Spec: execution.JobSpec{
-						Template: &execution.JobTemplateSpec{
+						Template: &execution.JobTemplate{
 							MaxAttempts: pointer.Int64(2),
 						},
 					},
@@ -374,7 +374,7 @@ func TestGetCondition(t *testing.T) {
 			args: args{
 				rj: &execution.Job{
 					Spec: execution.JobSpec{
-						Template: &execution.JobTemplateSpec{
+						Template: &execution.JobTemplate{
 							MaxAttempts: pointer.Int64(2),
 						},
 					},
@@ -442,7 +442,7 @@ func TestGetCondition(t *testing.T) {
 			args: args{
 				rj: &execution.Job{
 					Spec: execution.JobSpec{
-						Template: &execution.JobTemplateSpec{
+						Template: &execution.JobTemplate{
 							MaxAttempts: pointer.Int64(2),
 						},
 					},
@@ -482,7 +482,7 @@ func TestGetCondition(t *testing.T) {
 			args: args{
 				rj: &execution.Job{
 					Spec: execution.JobSpec{
-						Template: &execution.JobTemplateSpec{
+						Template: &execution.JobTemplate{
 							MaxAttempts: pointer.Int64(2),
 						},
 					},
@@ -541,7 +541,7 @@ func TestGetCondition(t *testing.T) {
 				rj: &execution.Job{
 					Spec: execution.JobSpec{
 						KillTimestamp: &killTime,
-						Template: &execution.JobTemplateSpec{
+						Template: &execution.JobTemplate{
 							MaxAttempts: pointer.Int64(2),
 						},
 					},
@@ -585,7 +585,7 @@ func TestGetCondition(t *testing.T) {
 				rj: &execution.Job{
 					Spec: execution.JobSpec{
 						KillTimestamp: &killTime,
-						Template: &execution.JobTemplateSpec{
+						Template: &execution.JobTemplate{
 							MaxAttempts: pointer.Int64(2),
 						},
 					},
@@ -662,7 +662,7 @@ func TestGetCondition(t *testing.T) {
 			args: args{
 				rj: &execution.Job{
 					Spec: execution.JobSpec{
-						Template: &execution.JobTemplateSpec{
+						Template: &execution.JobTemplate{
 							MaxAttempts: pointer.Int64(2),
 						},
 					},
@@ -683,7 +683,7 @@ func TestGetCondition(t *testing.T) {
 			args: args{
 				rj: &execution.Job{
 					Spec: execution.JobSpec{
-						Template: &execution.JobTemplateSpec{
+						Template: &execution.JobTemplate{
 							MaxAttempts: pointer.Int64(2),
 						},
 					},
@@ -720,7 +720,7 @@ func TestGetCondition(t *testing.T) {
 				rj: &execution.Job{
 					Spec: execution.JobSpec{
 						KillTimestamp: &killTime,
-						Template: &execution.JobTemplateSpec{
+						Template: &execution.JobTemplate{
 							MaxAttempts: pointer.Int64(2),
 						},
 					},
diff --git a/pkg/execution/util/job/job_utils_test.go b/pkg/execution/util/job/job_utils_test.go
index e66f08d..01fb4ab 100644
--- a/pkg/execution/util/job/job_utils_test.go
+++ b/pkg/execution/util/job/job_utils_test.go
@@ -49,7 +49,7 @@ func TestAllowedToCreateNewTask(t *testing.T) {
 			args: args{
 				rj: &execution.Job{
 					Spec: execution.JobSpec{
-						Template: &execution.JobTemplateSpec{
+						Template: &execution.JobTemplate{
 							MaxAttempts: pointer.Int64(2),
 						},
 					},
@@ -71,7 +71,7 @@ func TestAllowedToCreateNewTask(t *testing.T) {
 			args: args{
 				rj: &execution.Job{
 					Spec: execution.JobSpec{
-						Template: &execution.JobTemplateSpec{
+						Template: &execution.JobTemplate{
 							MaxAttempts: pointer.Int64(2),
 						},
 					},
@@ -97,7 +97,7 @@ func TestAllowedToCreateNewTask(t *testing.T) {
 			args: args{
 				rj: &execution.Job{
 					Spec: execution.JobSpec{
-						Template: &execution.JobTemplateSpec{
+						Template: &execution.JobTemplate{
 							MaxAttempts: pointer.Int64(2),
 						},
 					},
@@ -125,7 +125,7 @@ func TestAllowedToCreateNewTask(t *testing.T) {
 			args: args{
 				rj: &execution.Job{
 					Spec: execution.JobSpec{
-						Template: &execution.JobTemplateSpec{
+						Template: &execution.JobTemplate{
 							MaxAttempts: pointer.Int64(2),
 						},
 					},
@@ -177,7 +177,7 @@ func TestAllowedToCreateNewTask(t *testing.T) {
 				rj: &execution.Job{
 					Spec: execution.JobSpec{
 						KillTimestamp: &killTime,
-						Template: &execution.JobTemplateSpec{
+						Template: &execution.JobTemplate{
 							MaxAttempts: pointer.Int64(2),
 						},
 					},
@@ -205,7 +205,7 @@ func TestAllowedToCreateNewTask(t *testing.T) {
 			args: args{
 				rj: &execution.Job{
 					Spec: execution.JobSpec{
-						Template: &execution.JobTemplateSpec{
+						Template: &execution.JobTemplate{
 							MaxAttempts: pointer.Int64(3),
 						},
 					},
diff --git a/pkg/execution/util/job/task_utils_test.go b/pkg/execution/util/job/task_utils_test.go
index 1451a53..c672ce2 100644
--- a/pkg/execution/util/job/task_utils_test.go
+++ b/pkg/execution/util/job/task_utils_test.go
@@ -37,7 +37,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 			name: "job has no tasks with no retry delay",
 			rj: &execution.Job{
 				Spec: execution.JobSpec{
-					Template: &execution.JobTemplateSpec{
+					Template: &execution.JobTemplate{
 						MaxAttempts: pointer.Int64(3),
 					},
 				},
@@ -48,7 +48,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 			name: "job has no tasks with retry delay",
 			rj: &execution.Job{
 				Spec: execution.JobSpec{
-					Template: &execution.JobTemplateSpec{
+					Template: &execution.JobTemplate{
 						MaxAttempts:       pointer.Int64(3),
 						RetryDelaySeconds: pointer.Int64(30),
 					},
@@ -81,7 +81,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 			name: "job has some more retries with no retry delay",
 			rj: &execution.Job{
 				Spec: execution.JobSpec{
-					Template: &execution.JobTemplateSpec{
+					Template: &execution.JobTemplate{
 						MaxAttempts: pointer.Int64(3),
 					},
 				},
@@ -106,7 +106,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 			name: "job has some more retries with some delay",
 			rj: &execution.Job{
 				Spec: execution.JobSpec{
-					Template: &execution.JobTemplateSpec{
+					Template: &execution.JobTemplate{
 						MaxAttempts:       pointer.Int64(3),
 						RetryDelaySeconds: pointer.Int64(30),
 					},
@@ -132,7 +132,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 			name: "job has some more retries with some delay, without finish timestamp set",
 			rj: &execution.Job{
 				Spec: execution.JobSpec{
-					Template: &execution.JobTemplateSpec{
+					Template: &execution.JobTemplate{
 						MaxAttempts:       pointer.Int64(3),
 						RetryDelaySeconds: pointer.Int64(30),
 					},
@@ -157,7 +157,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 			name: "job has some more retries with some delay, with kill timestamp set",
 			rj: &execution.Job{
 				Spec: execution.JobSpec{
-					Template: &execution.JobTemplateSpec{
+					Template: &execution.JobTemplate{
 						MaxAttempts:       pointer.Int64(3),
 						RetryDelaySeconds: pointer.Int64(30),
 					},
@@ -184,7 +184,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 			name: "job has no more retries with no delay",
 			rj: &execution.Job{
 				Spec: execution.JobSpec{
-					Template: &execution.JobTemplateSpec{
+					Template: &execution.JobTemplate{
 						MaxAttempts:       pointer.Int64(2),
 						RetryDelaySeconds: pointer.Int64(30),
 					},
@@ -219,7 +219,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 			name: "job has no more retries with some delay",
 			rj: &execution.Job{
 				Spec: execution.JobSpec{
-					Template: &execution.JobTemplateSpec{
+					Template: &execution.JobTemplate{
 						MaxAttempts:       pointer.Int64(2),
 						RetryDelaySeconds: pointer.Int64(30),
 					},
diff --git a/pkg/execution/validation/corev1_test.go b/pkg/execution/validation/corev1_test.go
index 2e13d91..4bdf62e 100644
--- a/pkg/execution/validation/corev1_test.go
+++ b/pkg/execution/validation/corev1_test.go
@@ -36,16 +36,31 @@ var (
 
 	podTemplateSpecBasic = v1alpha1.PodTemplateSpec{
 		Spec: corev1.PodSpec{
-			Containers: []corev1.Container{
-				{
-					Name:  "container",
-					Image: "alpine",
-					Command: []string{
-						"echo",
-						"Hello World",
-					},
-				},
-			},
+			Containers: []corev1.Container{containerBasic},
+		},
+	}
+
+	podTemplateSpecUpdatedCommand = v1alpha1.PodTemplateSpec{
+		Spec: corev1.PodSpec{
+			Containers: []corev1.Container{containerUpdatedCommand},
+		},
+	}
+
+	containerBasic = corev1.Container{
+		Name:  "container",
+		Image: "alpine",
+		Command: []string{
+			"echo",
+			"Hello World",
+		},
+	}
+
+	containerUpdatedCommand = corev1.Container{
+		Name:  containerBasic.Name,
+		Image: containerBasic.Image,
+		Command: []string{
+			"echo",
+			"Hello World 2",
 		},
 	}
 )
diff --git a/pkg/execution/validation/validation_test.go b/pkg/execution/validation/validation_test.go
index 7b1c5fa..e00f19c 100644
--- a/pkg/execution/validation/validation_test.go
+++ b/pkg/execution/validation/validation_test.go
@@ -84,51 +84,51 @@ var (
 		OwnerReferences: ownerReferences,
 	}
 
-	jobTemplateSpecBasic = v1alpha1.JobTemplate{
+	jobTemplateSpecBasic = v1alpha1.JobTemplateSpec{
 		ObjectMeta: metav1.ObjectMeta{
 			Labels: map[string]string{
 				"labels.furiko.io/custom-label": "123",
 			},
 		},
-		Spec: v1alpha1.JobTemplateSpec{
-			Task: v1alpha1.TaskSpec{
-				Template: v1alpha1.TaskTemplate{
-					Pod: &podTemplateSpecBasic,
-				},
-				PendingTimeoutSeconds: pointer.Int64(1800),
+		Spec: v1alpha1.JobTemplate{
+			TaskTemplate: v1alpha1.TaskTemplate{
+				Pod: &podTemplateSpecBasic,
 			},
-			MaxAttempts:       pointer.Int64(5),
-			RetryDelaySeconds: pointer.Int64(60),
+			TaskPendingTimeoutSeconds: pointer.Int64(1800),
+			MaxAttempts:               pointer.Int64(5),
+			RetryDelaySeconds:         pointer.Int64(60),
 		},
 	}
 
-	jobTemplateSpecLongPendingTimeout = v1alpha1.JobTemplate{
-		ObjectMeta: jobTemplateSpecBasic.ObjectMeta,
-		Spec: v1alpha1.JobTemplateSpec{
-			Task: v1alpha1.TaskSpec{
-				Template: v1alpha1.TaskTemplate{
-					Pod: &podTemplateSpecBasic,
-				},
-				PendingTimeoutSeconds: pointer.Int64(3600),
+	jobTemplateSpecContainerUpdatedCommand = v1alpha1.JobTemplateSpec{
+		ObjectMeta: metav1.ObjectMeta{
+			Labels: map[string]string{
+				"labels.furiko.io/custom-label": "123",
 			},
-			MaxAttempts:       jobTemplateSpecBasic.Spec.MaxAttempts,
-			RetryDelaySeconds: jobTemplateSpecBasic.Spec.RetryDelaySeconds,
+		},
+		Spec: v1alpha1.JobTemplate{
+			TaskTemplate: v1alpha1.TaskTemplate{
+				Pod: &podTemplateSpecUpdatedCommand,
+			},
+			TaskPendingTimeoutSeconds: pointer.Int64(1800),
+			MaxAttempts:               pointer.Int64(5),
+			RetryDelaySeconds:         pointer.Int64(60),
 		},
 	}
 
-	jobTemplateSpecMoreRetries = v1alpha1.JobTemplate{
+	jobTemplateSpecMoreRetries = v1alpha1.JobTemplateSpec{
 		ObjectMeta: jobTemplateSpecBasic.ObjectMeta,
-		Spec: v1alpha1.JobTemplateSpec{
-			Task:              jobTemplateSpecBasic.Spec.Task,
+		Spec: v1alpha1.JobTemplate{
+			TaskTemplate:      jobTemplateSpecBasic.Spec.TaskTemplate,
 			MaxAttempts:       pointer.Int64(10),
 			RetryDelaySeconds: jobTemplateSpecBasic.Spec.RetryDelaySeconds,
 		},
 	}
 
-	jobTemplateSpecTooManyRetries = v1alpha1.JobTemplate{
+	jobTemplateSpecTooManyRetries = v1alpha1.JobTemplateSpec{
 		ObjectMeta: jobTemplateSpecBasic.ObjectMeta,
-		Spec: v1alpha1.JobTemplateSpec{
-			Task:              jobTemplateSpecBasic.Spec.Task,
+		Spec: v1alpha1.JobTemplate{
+			TaskTemplate:      jobTemplateSpecBasic.Spec.TaskTemplate,
 			MaxAttempts:       pointer.Int64(100),
 			RetryDelaySeconds: jobTemplateSpecBasic.Spec.RetryDelaySeconds,
 		},
@@ -369,49 +369,43 @@ func TestValidateJobConfig(t *testing.T) {
 			rjc: &v1alpha1.JobConfig{
 				Spec: v1alpha1.JobConfigSpec{
 					Concurrency: concurrencySpecBasic,
-					Template: v1alpha1.JobTemplate{
-						Spec: v1alpha1.JobTemplateSpec{
-							Task: v1alpha1.TaskSpec{
-								Template: v1alpha1.TaskTemplate{},
-							},
+					Template: v1alpha1.JobTemplateSpec{
+						Spec: v1alpha1.JobTemplate{
+							TaskTemplate: v1alpha1.TaskTemplate{},
 						},
 					},
 				},
 			},
-			wantErr: "spec.template.spec.task.template: Required value",
+			wantErr: "spec.template.spec.taskTemplate: Required value",
 		},
 		{
 			name: "invalid pod template",
 			rjc: &v1alpha1.JobConfig{
 				Spec: v1alpha1.JobConfigSpec{
 					Concurrency: concurrencySpecBasic,
-					Template: v1alpha1.JobTemplate{
-						Spec: v1alpha1.JobTemplateSpec{
-							Task: v1alpha1.TaskSpec{
-								Template: v1alpha1.TaskTemplate{
-									Pod: &podTemplateSpecEmpty,
-								},
+					Template: v1alpha1.JobTemplateSpec{
+						Spec: v1alpha1.JobTemplate{
+							TaskTemplate: v1alpha1.TaskTemplate{
+								Pod: &podTemplateSpecEmpty,
 							},
 						},
 					},
 				},
 			},
-			wantErr: "spec.template.spec.task.template.pod.spec.containers: Required value",
+			wantErr: "spec.template.spec.taskTemplate.pod.spec.containers: Required value",
 		},
 		{
 			name: "cannot use restartPolicy Always",
 			rjc: &v1alpha1.JobConfig{
 				Spec: v1alpha1.JobConfigSpec{
 					Concurrency: concurrencySpecBasic,
-					Template: v1alpha1.JobTemplate{
-						Spec: v1alpha1.JobTemplateSpec{
-							Task: v1alpha1.TaskSpec{
-								Template: v1alpha1.TaskTemplate{
-									Pod: &v1alpha1.PodTemplateSpec{
-										Spec: corev1.PodSpec{
-											Containers:    []corev1.Container{{Name: "container", Image: "alpine"}},
-											RestartPolicy: corev1.RestartPolicyAlways,
-										},
+					Template: v1alpha1.JobTemplateSpec{
+						Spec: v1alpha1.JobTemplate{
+							TaskTemplate: v1alpha1.TaskTemplate{
+								Pod: &v1alpha1.PodTemplateSpec{
+									Spec: corev1.PodSpec{
+										Containers:    []corev1.Container{{Name: "container", Image: "alpine"}},
+										RestartPolicy: corev1.RestartPolicyAlways,
 									},
 								},
 							},
@@ -419,7 +413,7 @@ func TestValidateJobConfig(t *testing.T) {
 					},
 				},
 			},
-			wantErr: "spec.template.spec.task.template.pod.spec.restartPolicy: Invalid value: \"Always\": restartPolicy cannot be Always",
+			wantErr: "spec.template.spec.taskTemplate.pod.spec.restartPolicy: Invalid value: \"Always\": restartPolicy cannot be Always",
 		},
 	}
 	for _, tt := range tests {
@@ -559,14 +553,12 @@ func TestValidateJob(t *testing.T) {
 				ObjectMeta: objectMetaJob,
 				Spec: v1alpha1.JobSpec{
 					Type: v1alpha1.JobTypeAdhoc,
-					Template: &v1alpha1.JobTemplateSpec{
-						Task: v1alpha1.TaskSpec{
-							Template: v1alpha1.TaskTemplate{},
-						},
+					Template: &v1alpha1.JobTemplate{
+						TaskTemplate: v1alpha1.TaskTemplate{},
 					},
 				},
 			},
-			wantErr: "spec.template.task.template: Required value",
+			wantErr: "spec.template.taskTemplate: Required value",
 		},
 		{
 			name: "invalid pod template",
@@ -574,16 +566,14 @@ func TestValidateJob(t *testing.T) {
 				ObjectMeta: objectMetaJob,
 				Spec: v1alpha1.JobSpec{
 					Type: v1alpha1.JobTypeAdhoc,
-					Template: &v1alpha1.JobTemplateSpec{
-						Task: v1alpha1.TaskSpec{
-							Template: v1alpha1.TaskTemplate{
-								Pod: &podTemplateSpecEmpty,
-							},
+					Template: &v1alpha1.JobTemplate{
+						TaskTemplate: v1alpha1.TaskTemplate{
+							Pod: &podTemplateSpecEmpty,
 						},
 					},
 				},
 			},
-			wantErr: "spec.template.task.template.pod.spec.containers: Required value",
+			wantErr: "spec.template.taskTemplate.pod.spec.containers: Required value",
 		},
 		{
 			name: "cannot use restartPolicy Always",
@@ -591,21 +581,19 @@ func TestValidateJob(t *testing.T) {
 				ObjectMeta: objectMetaJob,
 				Spec: v1alpha1.JobSpec{
 					Type: v1alpha1.JobTypeAdhoc,
-					Template: &v1alpha1.JobTemplateSpec{
-						Task: v1alpha1.TaskSpec{
-							Template: v1alpha1.TaskTemplate{
-								Pod: &v1alpha1.PodTemplateSpec{
-									Spec: corev1.PodSpec{
-										Containers:    []corev1.Container{{Name: "container", Image: "alpine"}},
-										RestartPolicy: corev1.RestartPolicyAlways,
-									},
+					Template: &v1alpha1.JobTemplate{
+						TaskTemplate: v1alpha1.TaskTemplate{
+							Pod: &v1alpha1.PodTemplateSpec{
+								Spec: corev1.PodSpec{
+									Containers:    []corev1.Container{{Name: "container", Image: "alpine"}},
+									RestartPolicy: corev1.RestartPolicyAlways,
 								},
 							},
 						},
 					},
 				},
 			},
-			wantErr: "spec.template.task.template.pod.spec.restartPolicy: Invalid value: \"Always\": restartPolicy cannot be Always",
+			wantErr: "spec.template.taskTemplate.pod.spec.restartPolicy: Invalid value: \"Always\": restartPolicy cannot be Always",
 		},
 	}
 	for _, tt := range tests {
@@ -757,10 +745,10 @@ func TestValidateJobUpdate(t *testing.T) {
 				ObjectMeta: objectMetaJob,
 				Spec: v1alpha1.JobSpec{
 					Type:     v1alpha1.JobTypeAdhoc,
-					Template: &jobTemplateSpecLongPendingTimeout.Spec,
+					Template: &jobTemplateSpecContainerUpdatedCommand.Spec,
 				},
 			},
-			wantErr: "spec.template.task: Invalid value",
+			wantErr: "spec.template.taskTemplate: Invalid value",
 		},
 		{
 			name: "immutable field maxAttempts",