Repository: furiko-io/furiko
Pull request #65 (2022-04-20 18:21:53)

refactor: Restructure packages
This is a major restructure to keep packages more consistent, as described in https://github.com/furiko-io/furiko/blob/main/DEVELOPMENT.md#navigating-the-project.


Patch:
diff --git a/cmd/execution-controller/main.go b/cmd/execution-controller/main.go
index 5fa696b..e067575 100644
--- a/cmd/execution-controller/main.go
+++ b/cmd/execution-controller/main.go
@@ -25,7 +25,6 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 
 	configv1alpha1 "github.com/furiko-io/furiko/apis/config/v1alpha1"
-	"github.com/furiko-io/furiko/pkg/bootstrap"
 	"github.com/furiko-io/furiko/pkg/execution/controllers/croncontroller"
 	"github.com/furiko-io/furiko/pkg/execution/controllers/jobconfigcontroller"
 	"github.com/furiko-io/furiko/pkg/execution/controllers/jobcontroller"
@@ -34,6 +33,7 @@ import (
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
 	"github.com/furiko-io/furiko/pkg/runtime/controllermanager"
 	"github.com/furiko-io/furiko/pkg/runtime/httphandler"
+	"github.com/furiko-io/furiko/pkg/runtime/util"
 )
 
 // +kubebuilder:rbac:groups="",resources=events;pods,verbs=get;list;watch;create;update;patch;delete
@@ -59,13 +59,13 @@ func main() {
 	var options configv1alpha1.ExecutionControllerConfig
 	if configFile != "" {
 		klog.Infof("loading configuration from %v", configFile)
-		if err := bootstrap.UnmarshalFromFile(configFile, &options); err != nil {
+		if err := util.UnmarshalFromFile(configFile, &options); err != nil {
 			klog.Fatalf(`cannot read config at "%v": %v`, configFile, err)
 		}
 	}
 
 	// Dump loaded configuration.
-	optionsMarshaled, err := bootstrap.Marshal(options)
+	optionsMarshaled, err := util.Marshal(options)
 	if err != nil {
 		klog.Fatalf("cannot marshal configuration: %v", err)
 	}
diff --git a/cmd/execution-webhook/main.go b/cmd/execution-webhook/main.go
index 51687b8..3440c15 100644
--- a/cmd/execution-webhook/main.go
+++ b/cmd/execution-webhook/main.go
@@ -25,7 +25,6 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 
 	configv1alpha1 "github.com/furiko-io/furiko/apis/config/v1alpha1"
-	"github.com/furiko-io/furiko/pkg/bootstrap"
 	"github.com/furiko-io/furiko/pkg/execution/webhooks/jobconfigmutatingwebhook"
 	"github.com/furiko-io/furiko/pkg/execution/webhooks/jobconfigvalidatingwebhook"
 	"github.com/furiko-io/furiko/pkg/execution/webhooks/jobmutatingwebhook"
@@ -33,6 +32,7 @@ import (
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
 	"github.com/furiko-io/furiko/pkg/runtime/controllermanager"
 	"github.com/furiko-io/furiko/pkg/runtime/httphandler"
+	"github.com/furiko-io/furiko/pkg/runtime/util"
 )
 
 // +kubebuilder:rbac:groups=execution.furiko.io,resources=jobs,verbs=get;list;watch
@@ -55,13 +55,13 @@ func main() {
 	var options configv1alpha1.ExecutionWebhookConfig
 	if configFile != "" {
 		klog.Infof("loading configuration from %v", configFile)
-		if err := bootstrap.UnmarshalFromFile(configFile, &options); err != nil {
+		if err := util.UnmarshalFromFile(configFile, &options); err != nil {
 			klog.Fatalf(`cannot read config at "%v": %v`, configFile, err)
 		}
 	}
 
 	// Dump loaded configuration.
-	optionsMarshaled, err := bootstrap.Marshal(options)
+	optionsMarshaled, err := util.Marshal(options)
 	if err != nil {
 		klog.Fatalf("cannot marshal configuration: %v", err)
 	}
diff --git a/pkg/errors/reason.go b/pkg/core/errors/reason.go
similarity index 100%
rename from pkg/errors/reason.go
rename to pkg/core/errors/reason.go
diff --git a/pkg/execution/controllers/croncontroller/reconciler.go b/pkg/execution/controllers/croncontroller/reconciler.go
index c845aa0..6f77ad7 100644
--- a/pkg/execution/controllers/croncontroller/reconciler.go
+++ b/pkg/execution/controllers/croncontroller/reconciler.go
@@ -27,9 +27,9 @@ import (
 
 	configv1alpha1 "github.com/furiko-io/furiko/apis/config/v1alpha1"
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
+	"github.com/furiko-io/furiko/pkg/execution/util/jobconfig"
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
 	"github.com/furiko-io/furiko/pkg/runtime/controllerutil"
-	"github.com/furiko-io/furiko/pkg/utils/execution/jobconfig"
 )
 
 // Reconciler creates Jobs that are queued to be started from the workqueue,
diff --git a/pkg/execution/controllers/jobconfigcontroller/differ.go b/pkg/execution/controllers/jobconfigcontroller/differ.go
index ba8eb43..e923ede 100644
--- a/pkg/execution/controllers/jobconfigcontroller/differ.go
+++ b/pkg/execution/controllers/jobconfigcontroller/differ.go
@@ -18,7 +18,7 @@ package jobconfigcontroller
 
 import (
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
-	jobutil "github.com/furiko-io/furiko/pkg/utils/execution/job"
+	jobutil "github.com/furiko-io/furiko/pkg/execution/util/job"
 )
 
 type jobListDiffer struct {
diff --git a/pkg/execution/controllers/jobconfigcontroller/informer.go b/pkg/execution/controllers/jobconfigcontroller/informer.go
index 552fd86..6125d79 100644
--- a/pkg/execution/controllers/jobconfigcontroller/informer.go
+++ b/pkg/execution/controllers/jobconfigcontroller/informer.go
@@ -22,8 +22,8 @@ import (
 	"k8s.io/client-go/tools/cache"
 	"k8s.io/klog/v2"
 
+	"github.com/furiko-io/furiko/pkg/execution/util/jobconfig"
 	"github.com/furiko-io/furiko/pkg/utils/eventhandler"
-	"github.com/furiko-io/furiko/pkg/utils/execution/jobconfig"
 )
 
 // InformerWorker receives events from the informer and enqueues work to be done
diff --git a/pkg/execution/controllers/jobconfigcontroller/reconciler.go b/pkg/execution/controllers/jobconfigcontroller/reconciler.go
index 05fd79d..00da211 100644
--- a/pkg/execution/controllers/jobconfigcontroller/reconciler.go
+++ b/pkg/execution/controllers/jobconfigcontroller/reconciler.go
@@ -31,9 +31,9 @@ import (
 
 	configv1alpha1 "github.com/furiko-io/furiko/apis/config/v1alpha1"
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
+	"github.com/furiko-io/furiko/pkg/execution/util/job"
+	"github.com/furiko-io/furiko/pkg/execution/util/jobconfig"
 	"github.com/furiko-io/furiko/pkg/runtime/controllerutil"
-	"github.com/furiko-io/furiko/pkg/utils/execution/job"
-	"github.com/furiko-io/furiko/pkg/utils/execution/jobconfig"
 	"github.com/furiko-io/furiko/pkg/utils/ktime"
 	"github.com/furiko-io/furiko/pkg/utils/logvalues"
 )
diff --git a/pkg/execution/controllers/jobcontroller/reconciler.go b/pkg/execution/controllers/jobcontroller/reconciler.go
index f5a1cd8..bcf535f 100644
--- a/pkg/execution/controllers/jobcontroller/reconciler.go
+++ b/pkg/execution/controllers/jobcontroller/reconciler.go
@@ -32,13 +32,13 @@ import (
 	configv1alpha1 "github.com/furiko-io/furiko/apis/config/v1alpha1"
 	executiongroup "github.com/furiko-io/furiko/apis/execution"
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
-	rerrors "github.com/furiko-io/furiko/pkg/errors"
+	coreerrors "github.com/furiko-io/furiko/pkg/core/errors"
 	jobtasks "github.com/furiko-io/furiko/pkg/execution/tasks"
+	jobutil "github.com/furiko-io/furiko/pkg/execution/util/job"
 	"github.com/furiko-io/furiko/pkg/execution/variablecontext"
 	"github.com/furiko-io/furiko/pkg/runtime/controllerutil"
-	jobutil "github.com/furiko-io/furiko/pkg/utils/execution/job"
-	"github.com/furiko-io/furiko/pkg/utils/k8sutils"
 	"github.com/furiko-io/furiko/pkg/utils/ktime"
+	"github.com/furiko-io/furiko/pkg/utils/meta"
 	timeutil "github.com/furiko-io/furiko/pkg/utils/time"
 )
 
@@ -358,8 +358,8 @@ func (w *Reconciler) syncCreateNewTask(
 	task, err := w.createTask(ctx, rj)
 	if err != nil {
 		// Handle this as a normal error.
-		rerr := rerrors.Error(nil)
-		if !errors.As(err, &rerr) || !rerrors.IsAdmissionRefused(err) {
+		rerr := coreerrors.Error(nil)
+		if !errors.As(err, &rerr) || !coreerrors.IsAdmissionRefused(err) {
 			return rj, tasks, errors.Wrapf(err, "could not create task")
 		}
 
@@ -820,7 +820,7 @@ func (w *Reconciler) handleFinishFinalizer(
 	newRj := rj.DeepCopy()
 
 	// Remove the finalizer.
-	newRj.Finalizers = k8sutils.RemoveFinalizer(newRj.Finalizers, executiongroup.DeleteDependentsFinalizer)
+	newRj.Finalizers = meta.RemoveFinalizer(newRj.Finalizers, executiongroup.DeleteDependentsFinalizer)
 
 	klog.InfoS("jobcontroller: job finalized",
 		"worker", w.Name(),
diff --git a/pkg/execution/controllers/jobcontroller/util.go b/pkg/execution/controllers/jobcontroller/util.go
index e3719c0..5231eae 100644
--- a/pkg/execution/controllers/jobcontroller/util.go
+++ b/pkg/execution/controllers/jobcontroller/util.go
@@ -19,7 +19,7 @@ package jobcontroller
 import (
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
 	"github.com/furiko-io/furiko/pkg/utils/cmp"
-	"github.com/furiko-io/furiko/pkg/utils/k8sutils"
+	"github.com/furiko-io/furiko/pkg/utils/meta"
 )
 
 // IsJobEqual returns true if the Job is not equal and should be updated.
@@ -44,5 +44,5 @@ func isDeleted(rj *execution.Job) bool {
 }
 
 func isFinalized(rj *execution.Job, finalizer string) bool {
-	return isDeleted(rj) && !k8sutils.ContainsFinalizer(rj.Finalizers, finalizer)
+	return isDeleted(rj) && !meta.ContainsFinalizer(rj.Finalizers, finalizer)
 }
diff --git a/pkg/execution/controllers/jobqueuecontroller/control.go b/pkg/execution/controllers/jobqueuecontroller/control.go
index b32ae70..df728b8 100644
--- a/pkg/execution/controllers/jobqueuecontroller/control.go
+++ b/pkg/execution/controllers/jobqueuecontroller/control.go
@@ -26,8 +26,8 @@ import (
 	"k8s.io/klog/v2"
 
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
+	"github.com/furiko-io/furiko/pkg/execution/util/job"
 	executionv1alpha1 "github.com/furiko-io/furiko/pkg/generated/clientset/versioned/typed/execution/v1alpha1"
-	"github.com/furiko-io/furiko/pkg/utils/execution/job"
 	"github.com/furiko-io/furiko/pkg/utils/ktime"
 	"github.com/furiko-io/furiko/pkg/utils/logvalues"
 )
diff --git a/pkg/execution/controllers/jobqueuecontroller/informer.go b/pkg/execution/controllers/jobqueuecontroller/informer.go
index 72ba9ed..2b0eaa8 100644
--- a/pkg/execution/controllers/jobqueuecontroller/informer.go
+++ b/pkg/execution/controllers/jobqueuecontroller/informer.go
@@ -23,8 +23,8 @@ import (
 	"k8s.io/client-go/util/workqueue"
 	"k8s.io/klog/v2"
 
+	"github.com/furiko-io/furiko/pkg/execution/util/jobconfig"
 	"github.com/furiko-io/furiko/pkg/utils/eventhandler"
-	"github.com/furiko-io/furiko/pkg/utils/execution/jobconfig"
 )
 
 // InformerWorker receives events from the informer and enqueues work to be done
diff --git a/pkg/execution/controllers/jobqueuecontroller/reconciler_independent.go b/pkg/execution/controllers/jobqueuecontroller/reconciler_independent.go
index 3adfe42..ed813be 100644
--- a/pkg/execution/controllers/jobqueuecontroller/reconciler_independent.go
+++ b/pkg/execution/controllers/jobqueuecontroller/reconciler_independent.go
@@ -29,8 +29,8 @@ import (
 
 	configv1alpha1 "github.com/furiko-io/furiko/apis/config/v1alpha1"
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
+	"github.com/furiko-io/furiko/pkg/execution/util/job"
 	"github.com/furiko-io/furiko/pkg/runtime/controllerutil"
-	"github.com/furiko-io/furiko/pkg/utils/execution/job"
 	"github.com/furiko-io/furiko/pkg/utils/ktime"
 	timeutil "github.com/furiko-io/furiko/pkg/utils/time"
 )
diff --git a/pkg/execution/controllers/jobqueuecontroller/reconciler_perjobconfig.go b/pkg/execution/controllers/jobqueuecontroller/reconciler_perjobconfig.go
index 5c302d0..62f4bb6 100644
--- a/pkg/execution/controllers/jobqueuecontroller/reconciler_perjobconfig.go
+++ b/pkg/execution/controllers/jobqueuecontroller/reconciler_perjobconfig.go
@@ -31,10 +31,10 @@ import (
 
 	configv1alpha1 "github.com/furiko-io/furiko/apis/config/v1alpha1"
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
+	"github.com/furiko-io/furiko/pkg/execution/util/job"
+	"github.com/furiko-io/furiko/pkg/execution/util/jobconfig"
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
 	"github.com/furiko-io/furiko/pkg/runtime/controllerutil"
-	"github.com/furiko-io/furiko/pkg/utils/execution/job"
-	"github.com/furiko-io/furiko/pkg/utils/execution/jobconfig"
 	"github.com/furiko-io/furiko/pkg/utils/ktime"
 	timeutil "github.com/furiko-io/furiko/pkg/utils/time"
 )
diff --git a/pkg/execution/mutation/mutation.go b/pkg/execution/mutation/mutation.go
index 27aedb2..60bb0df 100644
--- a/pkg/execution/mutation/mutation.go
+++ b/pkg/execution/mutation/mutation.go
@@ -35,14 +35,14 @@ import (
 	"github.com/furiko-io/furiko/apis/execution"
 	"github.com/furiko-io/furiko/apis/execution/v1alpha1"
 	"github.com/furiko-io/furiko/pkg/core/options"
+	"github.com/furiko-io/furiko/pkg/execution/util/jobconfig"
 	"github.com/furiko-io/furiko/pkg/execution/variablecontext"
 	executionlister "github.com/furiko-io/furiko/pkg/generated/listers/execution/v1alpha1"
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
-	"github.com/furiko-io/furiko/pkg/utils/execution/jobconfig"
+	"github.com/furiko-io/furiko/pkg/runtime/webhook"
 	"github.com/furiko-io/furiko/pkg/utils/jsonyaml"
-	"github.com/furiko-io/furiko/pkg/utils/k8sutils"
 	"github.com/furiko-io/furiko/pkg/utils/ktime"
-	"github.com/furiko-io/furiko/pkg/utils/webhook"
+	"github.com/furiko-io/furiko/pkg/utils/meta"
 )
 
 var (
@@ -140,8 +140,8 @@ func (m *Mutator) MutateCreateJob(rj *v1alpha1.Job) *webhook.Result {
 
 	// Add default finalizers if not set.
 	// NOTE(irvinlim): We should not add on update, because it affects deletion.
-	if !k8sutils.ContainsFinalizer(rj.Finalizers, execution.DeleteDependentsFinalizer) {
-		rj.Finalizers = k8sutils.MergeFinalizers(rj.Finalizers, []string{execution.DeleteDependentsFinalizer})
+	if !meta.ContainsFinalizer(rj.Finalizers, execution.DeleteDependentsFinalizer) {
+		rj.Finalizers = meta.MergeFinalizers(rj.Finalizers, []string{execution.DeleteDependentsFinalizer})
 	}
 
 	// Evaluate configName and populate fields from the JobConfig.
@@ -202,7 +202,7 @@ func (m *Mutator) evaluateConfigName(rj *v1alpha1.Job, rjcName string, fldPath *
 	// Merge metadata fields down, with the incoming Job's metadata taking precedence.
 	rj.Labels = labels.Merge(baseJob.Labels, rj.Labels)
 	rj.Annotations = labels.Merge(baseJob.Annotations, rj.Annotations)
-	rj.Finalizers = k8sutils.MergeFinalizers(baseJob.Finalizers, rj.Finalizers)
+	rj.Finalizers = meta.MergeFinalizers(baseJob.Finalizers, rj.Finalizers)
 
 	// Ensure that JobConfig references are intact.
 	rj.OwnerReferences = baseJob.OwnerReferences
@@ -272,7 +272,7 @@ func (m *Mutator) evaluateOptionValues(rj *v1alpha1.Job, rjc *v1alpha1.JobConfig
 			warning := fmt.Sprintf("failed to store option spec hash in annotations: %v", err)
 			result.Warnings = append(result.Warnings, warning)
 		} else {
-			k8sutils.SetAnnotation(rj, jobconfig.AnnotationKeyOptionSpecHash, hash)
+			meta.SetAnnotation(rj, jobconfig.AnnotationKeyOptionSpecHash, hash)
 		}
 	}
 
diff --git a/pkg/execution/mutation/patcher_jobconfigs.go b/pkg/execution/mutation/patcher_jobconfigs.go
index 738678a..00c2114 100644
--- a/pkg/execution/mutation/patcher_jobconfigs.go
+++ b/pkg/execution/mutation/patcher_jobconfigs.go
@@ -21,7 +21,7 @@ import (
 
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
-	"github.com/furiko-io/furiko/pkg/utils/webhook"
+	"github.com/furiko-io/furiko/pkg/runtime/webhook"
 )
 
 // JobConfigPatcher encapsulates high-level patch methods for JobConfigs.
diff --git a/pkg/execution/mutation/patcher_jobs.go b/pkg/execution/mutation/patcher_jobs.go
index 22dfa89..000d34b 100644
--- a/pkg/execution/mutation/patcher_jobs.go
+++ b/pkg/execution/mutation/patcher_jobs.go
@@ -21,7 +21,7 @@ import (
 
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
-	"github.com/furiko-io/furiko/pkg/utils/webhook"
+	"github.com/furiko-io/furiko/pkg/runtime/webhook"
 )
 
 // JobPatcher encapsulates high-level patch methods for Jobs.
diff --git a/pkg/execution/stores/activejobstore/store.go b/pkg/execution/stores/activejobstore/store.go
index 2b0ec62..fee18e2 100644
--- a/pkg/execution/stores/activejobstore/store.go
+++ b/pkg/execution/stores/activejobstore/store.go
@@ -27,11 +27,11 @@ import (
 	"k8s.io/klog/v2"
 
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
+	"github.com/furiko-io/furiko/pkg/execution/util/job"
+	"github.com/furiko-io/furiko/pkg/execution/util/jobconfig"
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
 	"github.com/furiko-io/furiko/pkg/runtime/controllerutil"
 	utilatomic "github.com/furiko-io/furiko/pkg/utils/atomic"
-	"github.com/furiko-io/furiko/pkg/utils/execution/job"
-	"github.com/furiko-io/furiko/pkg/utils/execution/jobconfig"
 )
 
 var (
diff --git a/pkg/execution/taskexecutor/podtaskexecutor/pod_client.go b/pkg/execution/taskexecutor/podtaskexecutor/pod_client.go
index fe80cfd..67fe123 100644
--- a/pkg/execution/taskexecutor/podtaskexecutor/pod_client.go
+++ b/pkg/execution/taskexecutor/podtaskexecutor/pod_client.go
@@ -26,7 +26,7 @@ import (
 	v1 "k8s.io/client-go/kubernetes/typed/core/v1"
 
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
-	rerrors "github.com/furiko-io/furiko/pkg/errors"
+	coreerrors "github.com/furiko-io/furiko/pkg/core/errors"
 	"github.com/furiko-io/furiko/pkg/execution/tasks"
 )
 
@@ -68,7 +68,7 @@ func (p *PodTaskClient) CreateIndex(ctx context.Context, index int64) (tasks.Tas
 	// Rejected by apiserver, do not attempt to retry and raise an
 	// AdmissionRefusedError instead.
 	if kerrors.IsInvalid(err) {
-		return nil, rerrors.NewAdmissionRefusedError(err.Error())
+		return nil, coreerrors.NewAdmissionRefusedError(err.Error())
 	}
 
 	if err != nil {
diff --git a/pkg/execution/taskexecutor/podtaskexecutor/pod_task.go b/pkg/execution/taskexecutor/podtaskexecutor/pod_task.go
index a1909f9..f8e5386 100644
--- a/pkg/execution/taskexecutor/podtaskexecutor/pod_task.go
+++ b/pkg/execution/taskexecutor/podtaskexecutor/pod_task.go
@@ -28,9 +28,9 @@ import (
 	v1 "k8s.io/client-go/kubernetes/typed/core/v1"
 
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
-	"github.com/furiko-io/furiko/pkg/utils/execution/job"
-	"github.com/furiko-io/furiko/pkg/utils/k8sutils"
+	"github.com/furiko-io/furiko/pkg/execution/util/job"
 	"github.com/furiko-io/furiko/pkg/utils/ktime"
+	"github.com/furiko-io/furiko/pkg/utils/meta"
 )
 
 const (
@@ -123,7 +123,7 @@ func (p *PodTask) SetKillTimestamp(ctx context.Context, ts time.Time) error {
 
 	// Add annotation for kill timestamp.
 	// This will be the authoritative source of truth for the time we want to kill the pod.
-	k8sutils.SetAnnotation(newPod, LabelKeyTaskKillTimestamp, strconv.Itoa(int(ts.Unix())))
+	meta.SetAnnotation(newPod, LabelKeyTaskKillTimestamp, strconv.Itoa(int(ts.Unix())))
 
 	// Compute active deadline.
 	var ads int64
@@ -169,7 +169,7 @@ func (p *PodTask) GetKilledFromPendingTimeoutMarker() bool {
 
 func (p *PodTask) SetKilledFromPendingTimeoutMarker(ctx context.Context) error {
 	newPod := p.Pod.DeepCopy()
-	k8sutils.SetAnnotation(newPod, LabelKeyKilledFromPendingTimeout, "1")
+	meta.SetAnnotation(newPod, LabelKeyKilledFromPendingTimeout, "1")
 
 	updatedPod, err := p.client.Update(ctx, newPod, metav1.UpdateOptions{})
 	if err != nil {
diff --git a/pkg/utils/execution/job/job_utils.go b/pkg/execution/util/job/job_utils.go
similarity index 95%
rename from pkg/utils/execution/job/job_utils.go
rename to pkg/execution/util/job/job_utils.go
index 309142b..3a1d67d 100644
--- a/pkg/utils/execution/job/job_utils.go
+++ b/pkg/execution/util/job/job_utils.go
@@ -18,7 +18,7 @@ package job
 
 import (
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
-	"github.com/furiko-io/furiko/pkg/utils/k8sutils"
+	"github.com/furiko-io/furiko/pkg/utils/meta"
 )
 
 // AllowedToCreateNewTask returns whether a Job is allowed to create more tasks
@@ -75,7 +75,7 @@ func HasSuccessTask(rj *execution.Job) bool {
 
 // MarkAdmissionError updates a Job to add the AdmissionError annotation.
 func MarkAdmissionError(rj *execution.Job, msg string) {
-	k8sutils.SetAnnotation(rj, LabelKeyAdmissionErrorMessage, msg)
+	meta.SetAnnotation(rj, LabelKeyAdmissionErrorMessage, msg)
 }
 
 // GetAdmissionErrorMessage returns the error message if the Job contains the
diff --git a/pkg/execution/validation/validation.go b/pkg/execution/validation/validation.go
index 5272f10..568df6e 100644
--- a/pkg/execution/validation/validation.go
+++ b/pkg/execution/validation/validation.go
@@ -34,9 +34,9 @@ import (
 	"github.com/furiko-io/furiko/pkg/core/tzutils"
 	"github.com/furiko-io/furiko/pkg/core/validation"
 	"github.com/furiko-io/furiko/pkg/execution/util/cronparser"
+	"github.com/furiko-io/furiko/pkg/execution/util/jobconfig"
 	executionlister "github.com/furiko-io/furiko/pkg/generated/listers/execution/v1alpha1"
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
-	"github.com/furiko-io/furiko/pkg/utils/execution/jobconfig"
 )
 
 const (
diff --git a/pkg/execution/webhooks/jobconfigmutatingwebhook/webhook.go b/pkg/execution/webhooks/jobconfigmutatingwebhook/webhook.go
index 795794e..8d0fbbf 100644
--- a/pkg/execution/webhooks/jobconfigmutatingwebhook/webhook.go
+++ b/pkg/execution/webhooks/jobconfigmutatingwebhook/webhook.go
@@ -33,8 +33,8 @@ import (
 	"github.com/furiko-io/furiko/pkg/execution/mutation"
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
 	"github.com/furiko-io/furiko/pkg/runtime/controllermanager"
+	"github.com/furiko-io/furiko/pkg/runtime/webhook"
 	"github.com/furiko-io/furiko/pkg/utils/cmp"
-	"github.com/furiko-io/furiko/pkg/utils/webhook"
 )
 
 const (
diff --git a/pkg/execution/webhooks/jobmutatingwebhook/webhook.go b/pkg/execution/webhooks/jobmutatingwebhook/webhook.go
index 9593497..3089a3d 100644
--- a/pkg/execution/webhooks/jobmutatingwebhook/webhook.go
+++ b/pkg/execution/webhooks/jobmutatingwebhook/webhook.go
@@ -38,8 +38,8 @@ import (
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
 	"github.com/furiko-io/furiko/pkg/runtime/controllermanager"
 	"github.com/furiko-io/furiko/pkg/runtime/controllerutil"
+	"github.com/furiko-io/furiko/pkg/runtime/webhook"
 	"github.com/furiko-io/furiko/pkg/utils/cmp"
-	"github.com/furiko-io/furiko/pkg/utils/webhook"
 )
 
 const (
diff --git a/pkg/bootstrap/config.go b/pkg/runtime/util/config.go
similarity index 98%
rename from pkg/bootstrap/config.go
rename to pkg/runtime/util/config.go
index 495e4ff..240714b 100644
--- a/pkg/bootstrap/config.go
+++ b/pkg/runtime/util/config.go
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package bootstrap
+package util
 
 import (
 	"os"
diff --git a/pkg/utils/webhook/result.go b/pkg/runtime/webhook/result.go
similarity index 100%
rename from pkg/utils/webhook/result.go
rename to pkg/runtime/webhook/result.go
diff --git a/pkg/utils/k8sutils/annotations.go b/pkg/utils/meta/annotations.go
similarity index 98%
rename from pkg/utils/k8sutils/annotations.go
rename to pkg/utils/meta/annotations.go
index d23da74..f70b9bd 100644
--- a/pkg/utils/k8sutils/annotations.go
+++ b/pkg/utils/meta/annotations.go
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package k8sutils
+package meta
 
 type ObjectMeta interface {
 	GetAnnotations() map[string]string
diff --git a/pkg/utils/k8sutils/finalizers.go b/pkg/utils/meta/finalizers.go
similarity index 99%
rename from pkg/utils/k8sutils/finalizers.go
rename to pkg/utils/meta/finalizers.go
index 70aee6d..be5316f 100644
--- a/pkg/utils/k8sutils/finalizers.go
+++ b/pkg/utils/meta/finalizers.go
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package k8sutils
+package meta
 
 import (
 	"strings"


Test patch:
diff --git a/pkg/errors/errors.go b/pkg/core/errors/errors.go
similarity index 100%
rename from pkg/errors/errors.go
rename to pkg/core/errors/errors.go
diff --git a/pkg/errors/errors_test.go b/pkg/core/errors/errors_test.go
similarity index 59%
rename from pkg/errors/errors_test.go
rename to pkg/core/errors/errors_test.go
index 377a410..e9fd7fd 100644
--- a/pkg/errors/errors_test.go
+++ b/pkg/core/errors/errors_test.go
@@ -21,24 +21,24 @@ import (
 
 	"github.com/pkg/errors"
 
-	rerrors "github.com/furiko-io/furiko/pkg/errors"
+	coreerrors "github.com/furiko-io/furiko/pkg/core/errors"
 )
 
 func TestAdmissionRefused(t *testing.T) {
 	var err error
 
-	err = rerrors.NewAdmissionRefusedError("test message")
-	if !rerrors.IsAdmissionRefused(err) {
+	err = coreerrors.NewAdmissionRefusedError("test message")
+	if !coreerrors.IsAdmissionRefused(err) {
 		t.Errorf("expected IsAdmissionRefused to be true")
 	} else {
-		if rerr := rerrors.Error(nil); !errors.As(err, &rerr) {
+		if rerr := coreerrors.Error(nil); !errors.As(err, &rerr) {
 			t.Errorf("expected err to be Error")
 		}
-		if rerrors.GetReason(err) != rerrors.ReasonAdmissionRefused {
-			t.Errorf(`expected Reason to be "AdmissionRefused", got "%v"`, rerrors.GetReason(err))
+		if coreerrors.GetReason(err) != coreerrors.ReasonAdmissionRefused {
+			t.Errorf(`expected Reason to be "AdmissionRefused", got "%v"`, coreerrors.GetReason(err))
 		}
-		if rerrors.GetMessage(err) != "test message" {
-			t.Errorf(`expected Message to be "test message", got "%v"`, rerrors.GetMessage(err))
+		if coreerrors.GetMessage(err) != "test message" {
+			t.Errorf(`expected Message to be "test message", got "%v"`, coreerrors.GetMessage(err))
 		}
 	}
 	if err.Error() != "AdmissionRefused - test message" {
@@ -46,16 +46,16 @@ func TestAdmissionRefused(t *testing.T) {
 	}
 
 	err = errors.New("some other error")
-	if rerrors.IsAdmissionRefused(err) {
+	if coreerrors.IsAdmissionRefused(err) {
 		t.Errorf("expected IsAdmissionRefused to be false")
 	}
-	if rerr := rerrors.Error(nil); errors.As(err, &rerr) {
+	if rerr := coreerrors.Error(nil); errors.As(err, &rerr) {
 		t.Errorf("expected err to not be Error")
 	}
-	if rerrors.GetReason(err) != rerrors.ReasonUnknown {
-		t.Errorf(`expected Reason to be "ReasonUnknown", got "%v"`, rerrors.GetReason(err))
+	if coreerrors.GetReason(err) != coreerrors.ReasonUnknown {
+		t.Errorf(`expected Reason to be "ReasonUnknown", got "%v"`, coreerrors.GetReason(err))
 	}
-	if rerrors.GetMessage(err) != "" {
-		t.Errorf(`expected Message to be "", got "%v"`, rerrors.GetMessage(err))
+	if coreerrors.GetMessage(err) != "" {
+		t.Errorf(`expected Message to be "", got "%v"`, coreerrors.GetMessage(err))
 	}
 }
diff --git a/pkg/execution/controllers/jobconfigcontroller/reconciler_test.go b/pkg/execution/controllers/jobconfigcontroller/reconciler_test.go
index 4a72a10..8489df5 100644
--- a/pkg/execution/controllers/jobconfigcontroller/reconciler_test.go
+++ b/pkg/execution/controllers/jobconfigcontroller/reconciler_test.go
@@ -30,11 +30,11 @@ import (
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
 	"github.com/furiko-io/furiko/pkg/execution/controllers/jobconfigcontroller"
 	"github.com/furiko-io/furiko/pkg/execution/stores/activejobstore"
+	"github.com/furiko-io/furiko/pkg/execution/util/jobconfig"
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext/mock"
 	"github.com/furiko-io/furiko/pkg/runtime/reconciler"
 	runtimetesting "github.com/furiko-io/furiko/pkg/runtime/testing"
-	"github.com/furiko-io/furiko/pkg/utils/execution/jobconfig"
 	"github.com/furiko-io/furiko/pkg/utils/ktime"
 	"github.com/furiko-io/furiko/pkg/utils/testutils"
 )
diff --git a/pkg/execution/controllers/jobcontroller/controller_test.go b/pkg/execution/controllers/jobcontroller/controller_test.go
index 9fc383d..5392ccd 100644
--- a/pkg/execution/controllers/jobcontroller/controller_test.go
+++ b/pkg/execution/controllers/jobcontroller/controller_test.go
@@ -30,8 +30,8 @@ import (
 	"github.com/furiko-io/furiko/pkg/execution/controllers/jobcontroller"
 	"github.com/furiko-io/furiko/pkg/execution/taskexecutor/podtaskexecutor"
 	"github.com/furiko-io/furiko/pkg/execution/tasks"
-	"github.com/furiko-io/furiko/pkg/utils/execution/job"
-	"github.com/furiko-io/furiko/pkg/utils/k8sutils"
+	"github.com/furiko-io/furiko/pkg/execution/util/job"
+	"github.com/furiko-io/furiko/pkg/utils/meta"
 	"github.com/furiko-io/furiko/pkg/utils/testutils"
 )
 
@@ -130,7 +130,7 @@ var (
 	// Job with deletion timestamp whose pods are killed.
 	fakeJobWithDeletionTimestampAndDeletedPods = func() *execution.Job {
 		newJob := fakeJobWithDeletionTimestamp.DeepCopy()
-		newJob.Finalizers = k8sutils.RemoveFinalizer(newJob.Finalizers, executiongroup.DeleteDependentsFinalizer)
+		newJob.Finalizers = meta.RemoveFinalizer(newJob.Finalizers, executiongroup.DeleteDependentsFinalizer)
 		newJob.Status.Phase = execution.JobKilled
 		newJob.Status.Condition = execution.JobCondition{
 			Finished: &execution.JobConditionFinished{
@@ -265,7 +265,7 @@ var (
 	// timeout.
 	fakePodPendingTimeoutTerminating = func() *corev1.Pod {
 		newPod := killPod(fakePodPending, testutils.Mktime(later15m))
-		k8sutils.SetAnnotation(newPod, podtaskexecutor.LabelKeyKilledFromPendingTimeout, "1")
+		meta.SetAnnotation(newPod, podtaskexecutor.LabelKeyKilledFromPendingTimeout, "1")
 		return newPod
 	}()
 
@@ -305,7 +305,7 @@ func generateJobStatusFromPod(rj *execution.Job, pod *corev1.Pod) *execution.Job
 // killPod returns a new Pod after setting the kill timestamp.
 func killPod(pod *corev1.Pod, ts time.Time) *corev1.Pod {
 	newPod := pod.DeepCopy()
-	k8sutils.SetAnnotation(newPod, podtaskexecutor.LabelKeyTaskKillTimestamp, strconv.Itoa(int(ts.Unix())))
+	meta.SetAnnotation(newPod, podtaskexecutor.LabelKeyTaskKillTimestamp, strconv.Itoa(int(ts.Unix())))
 	newPod.Spec.ActiveDeadlineSeconds = pointer.Int64(int64(ts.Sub(newPod.Status.StartTime.Time).Seconds()))
 	return newPod
 }
diff --git a/pkg/execution/controllers/jobcontroller/reconciler_test.go b/pkg/execution/controllers/jobcontroller/reconciler_test.go
index e0ac824..b57e688 100644
--- a/pkg/execution/controllers/jobcontroller/reconciler_test.go
+++ b/pkg/execution/controllers/jobcontroller/reconciler_test.go
@@ -32,7 +32,7 @@ import (
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
 	"github.com/furiko-io/furiko/pkg/runtime/reconciler"
 	runtimetesting "github.com/furiko-io/furiko/pkg/runtime/testing"
-	"github.com/furiko-io/furiko/pkg/utils/k8sutils"
+	"github.com/furiko-io/furiko/pkg/utils/meta"
 	"github.com/furiko-io/furiko/pkg/utils/testutils"
 )
 
@@ -115,7 +115,7 @@ func TestReconciler(t *testing.T) {
 					ActionGenerators: []runtimetesting.ActionGenerator{
 						func() (runtimetesting.Action, error) {
 							newPod := fakePodPending.DeepCopy()
-							k8sutils.SetAnnotation(newPod, podtaskexecutor.LabelKeyKilledFromPendingTimeout, "1")
+							meta.SetAnnotation(newPod, podtaskexecutor.LabelKeyKilledFromPendingTimeout, "1")
 							return runtimetesting.NewUpdatePodAction(jobNamespace, newPod), nil
 						},
 						func() (runtimetesting.Action, error) {
diff --git a/pkg/execution/controllers/jobqueuecontroller/reconciler_test.go b/pkg/execution/controllers/jobqueuecontroller/reconciler_test.go
index e03ef62..6a60026 100644
--- a/pkg/execution/controllers/jobqueuecontroller/reconciler_test.go
+++ b/pkg/execution/controllers/jobqueuecontroller/reconciler_test.go
@@ -21,7 +21,7 @@ import (
 
 	executiongroup "github.com/furiko-io/furiko/apis/execution"
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
-	"github.com/furiko-io/furiko/pkg/utils/execution/jobconfig"
+	"github.com/furiko-io/furiko/pkg/execution/util/jobconfig"
 	"github.com/furiko-io/furiko/pkg/utils/testutils"
 )
 
diff --git a/pkg/execution/mutation/mutation_test.go b/pkg/execution/mutation/mutation_test.go
index 903a687..e94d4fe 100644
--- a/pkg/execution/mutation/mutation_test.go
+++ b/pkg/execution/mutation/mutation_test.go
@@ -42,11 +42,11 @@ import (
 	"github.com/furiko-io/furiko/pkg/core/options"
 	"github.com/furiko-io/furiko/pkg/execution/mutation"
 	"github.com/furiko-io/furiko/pkg/execution/tasks"
+	"github.com/furiko-io/furiko/pkg/execution/util/jobconfig"
 	"github.com/furiko-io/furiko/pkg/execution/variablecontext"
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext/mock"
-	"github.com/furiko-io/furiko/pkg/utils/execution/jobconfig"
-	"github.com/furiko-io/furiko/pkg/utils/webhook"
+	"github.com/furiko-io/furiko/pkg/runtime/webhook"
 )
 
 const (
diff --git a/pkg/execution/stores/activejobstore/store_test.go b/pkg/execution/stores/activejobstore/store_test.go
index 188e575..a9d5ba6 100644
--- a/pkg/execution/stores/activejobstore/store_test.go
+++ b/pkg/execution/stores/activejobstore/store_test.go
@@ -28,10 +28,10 @@ import (
 
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
 	"github.com/furiko-io/furiko/pkg/execution/stores/activejobstore"
+	"github.com/furiko-io/furiko/pkg/execution/util/jobconfig"
 	furiko "github.com/furiko-io/furiko/pkg/generated/clientset/versioned"
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext/mock"
-	"github.com/furiko-io/furiko/pkg/utils/execution/jobconfig"
-	"github.com/furiko-io/furiko/pkg/utils/k8sutils"
+	"github.com/furiko-io/furiko/pkg/utils/meta"
 )
 
 const (
@@ -173,7 +173,7 @@ func TestStore(t *testing.T) {
 		go func(i int) {
 			defer wg.Done()
 			newRj := rj.DeepCopy()
-			k8sutils.SetAnnotation(newRj, "index", strconv.Itoa(i))
+			meta.SetAnnotation(newRj, "index", strconv.Itoa(i))
 			newRj.Status.Phase = execution.JobSucceeded
 			updatedRj, err := updateJob(ctx, client, newRj)
 			assert.NoError(t, err)
diff --git a/pkg/execution/taskexecutor/podtaskexecutor/pod_task_test.go b/pkg/execution/taskexecutor/podtaskexecutor/pod_task_test.go
index 0629c53..ada0005 100644
--- a/pkg/execution/taskexecutor/podtaskexecutor/pod_task_test.go
+++ b/pkg/execution/taskexecutor/podtaskexecutor/pod_task_test.go
@@ -24,7 +24,7 @@ import (
 
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
 	"github.com/furiko-io/furiko/pkg/execution/taskexecutor/podtaskexecutor"
-	"github.com/furiko-io/furiko/pkg/utils/execution/job"
+	"github.com/furiko-io/furiko/pkg/execution/util/job"
 )
 
 func TestPodTask_GetState(t *testing.T) {
diff --git a/pkg/utils/execution/job/active.go b/pkg/execution/util/job/active.go
similarity index 100%
rename from pkg/utils/execution/job/active.go
rename to pkg/execution/util/job/active.go
diff --git a/pkg/utils/execution/job/active_test.go b/pkg/execution/util/job/active_test.go
similarity index 98%
rename from pkg/utils/execution/job/active_test.go
rename to pkg/execution/util/job/active_test.go
index 4ae8207..1d9b44f 100644
--- a/pkg/utils/execution/job/active_test.go
+++ b/pkg/execution/util/job/active_test.go
@@ -20,7 +20,7 @@ import (
 	"testing"
 
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
-	"github.com/furiko-io/furiko/pkg/utils/execution/job"
+	"github.com/furiko-io/furiko/pkg/execution/util/job"
 )
 
 func TestIsActive(t *testing.T) {
diff --git a/pkg/utils/execution/job/concurrent.go b/pkg/execution/util/job/concurrent.go
similarity index 100%
rename from pkg/utils/execution/job/concurrent.go
rename to pkg/execution/util/job/concurrent.go
diff --git a/pkg/utils/execution/job/condition.go b/pkg/execution/util/job/condition.go
similarity index 100%
rename from pkg/utils/execution/job/condition.go
rename to pkg/execution/util/job/condition.go
diff --git a/pkg/utils/execution/job/condition_test.go b/pkg/execution/util/job/condition_test.go
similarity index 99%
rename from pkg/utils/execution/job/condition_test.go
rename to pkg/execution/util/job/condition_test.go
index 87f26f5..1a85a76 100644
--- a/pkg/utils/execution/job/condition_test.go
+++ b/pkg/execution/util/job/condition_test.go
@@ -26,7 +26,7 @@ import (
 
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
 	"github.com/furiko-io/furiko/pkg/execution/tasks"
-	jobutil "github.com/furiko-io/furiko/pkg/utils/execution/job"
+	jobutil "github.com/furiko-io/furiko/pkg/execution/util/job"
 	"github.com/furiko-io/furiko/pkg/utils/ktime"
 )
 
diff --git a/pkg/utils/execution/job/job_utils_test.go b/pkg/execution/util/job/job_utils_test.go
similarity index 98%
rename from pkg/utils/execution/job/job_utils_test.go
rename to pkg/execution/util/job/job_utils_test.go
index 150e40b..321015c 100644
--- a/pkg/utils/execution/job/job_utils_test.go
+++ b/pkg/execution/util/job/job_utils_test.go
@@ -21,7 +21,7 @@ import (
 
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
 	jobtasks "github.com/furiko-io/furiko/pkg/execution/tasks"
-	jobutil "github.com/furiko-io/furiko/pkg/utils/execution/job"
+	jobutil "github.com/furiko-io/furiko/pkg/execution/util/job"
 )
 
 func TestAllowedToCreateNewTask(t *testing.T) {
diff --git a/pkg/utils/execution/job/labels.go b/pkg/execution/util/job/labels.go
similarity index 100%
rename from pkg/utils/execution/job/labels.go
rename to pkg/execution/util/job/labels.go
diff --git a/pkg/utils/execution/job/phase.go b/pkg/execution/util/job/phase.go
similarity index 100%
rename from pkg/utils/execution/job/phase.go
rename to pkg/execution/util/job/phase.go
diff --git a/pkg/utils/execution/job/phase_test.go b/pkg/execution/util/job/phase_test.go
similarity index 99%
rename from pkg/utils/execution/job/phase_test.go
rename to pkg/execution/util/job/phase_test.go
index d3320df..89f072e 100644
--- a/pkg/utils/execution/job/phase_test.go
+++ b/pkg/execution/util/job/phase_test.go
@@ -20,7 +20,7 @@ import (
 	"testing"
 
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
-	"github.com/furiko-io/furiko/pkg/utils/execution/job"
+	"github.com/furiko-io/furiko/pkg/execution/util/job"
 )
 
 func TestGetPhase(t *testing.T) {
diff --git a/pkg/utils/execution/job/task_status.go b/pkg/execution/util/job/task_status.go
similarity index 100%
rename from pkg/utils/execution/job/task_status.go
rename to pkg/execution/util/job/task_status.go
diff --git a/pkg/utils/execution/job/task_status_test.go b/pkg/execution/util/job/task_status_test.go
similarity index 99%
rename from pkg/utils/execution/job/task_status_test.go
rename to pkg/execution/util/job/task_status_test.go
index 1bbf664..cd0ea26 100644
--- a/pkg/utils/execution/job/task_status_test.go
+++ b/pkg/execution/util/job/task_status_test.go
@@ -24,7 +24,7 @@ import (
 
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
 	"github.com/furiko-io/furiko/pkg/execution/tasks"
-	jobutil "github.com/furiko-io/furiko/pkg/utils/execution/job"
+	jobutil "github.com/furiko-io/furiko/pkg/execution/util/job"
 )
 
 func TestGenerateTaskRefs(t *testing.T) {
diff --git a/pkg/utils/execution/job/task_test.go b/pkg/execution/util/job/task_test.go
similarity index 100%
rename from pkg/utils/execution/job/task_test.go
rename to pkg/execution/util/job/task_test.go
diff --git a/pkg/utils/execution/job/task_utils.go b/pkg/execution/util/job/task_utils.go
similarity index 100%
rename from pkg/utils/execution/job/task_utils.go
rename to pkg/execution/util/job/task_utils.go
diff --git a/pkg/utils/execution/job/task_utils_test.go b/pkg/execution/util/job/task_utils_test.go
similarity index 89%
rename from pkg/utils/execution/job/task_utils_test.go
rename to pkg/execution/util/job/task_utils_test.go
index a078cf8..0177d32 100644
--- a/pkg/utils/execution/job/task_utils_test.go
+++ b/pkg/execution/util/job/task_utils_test.go
@@ -23,7 +23,7 @@ import (
 	"k8s.io/utils/pointer"
 
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
-	"github.com/furiko-io/furiko/pkg/utils/execution/job"
+	jobutil "github.com/furiko-io/furiko/pkg/execution/util/job"
 )
 
 func TestGetNextAllowedRetry(t *testing.T) {
@@ -69,7 +69,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 							FinishTimestamp:   &finishTime,
 							Status: execution.TaskStatus{
 								State:  execution.TaskFailed,
-								Result: job.GetResultPtr(execution.JobResultTaskFailed),
+								Result: jobutil.GetResultPtr(execution.JobResultTaskFailed),
 							},
 						},
 					},
@@ -94,7 +94,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 							FinishTimestamp:   &finishTime,
 							Status: execution.TaskStatus{
 								State:  execution.TaskFailed,
-								Result: job.GetResultPtr(execution.JobResultTaskFailed),
+								Result: jobutil.GetResultPtr(execution.JobResultTaskFailed),
 							},
 						},
 					},
@@ -120,7 +120,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 							FinishTimestamp:   &finishTime,
 							Status: execution.TaskStatus{
 								State:  execution.TaskFailed,
-								Result: job.GetResultPtr(execution.JobResultTaskFailed),
+								Result: jobutil.GetResultPtr(execution.JobResultTaskFailed),
 							},
 						},
 					},
@@ -145,7 +145,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 							CreationTimestamp: createTime,
 							Status: execution.TaskStatus{
 								State:  execution.TaskFailed,
-								Result: job.GetResultPtr(execution.JobResultTaskFailed),
+								Result: jobutil.GetResultPtr(execution.JobResultTaskFailed),
 							},
 						},
 					},
@@ -172,7 +172,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 							FinishTimestamp:   &finishTime,
 							Status: execution.TaskStatus{
 								State:  execution.TaskFailed,
-								Result: job.GetResultPtr(execution.JobResultTaskFailed),
+								Result: jobutil.GetResultPtr(execution.JobResultTaskFailed),
 							},
 						},
 					},
@@ -198,7 +198,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 							FinishTimestamp:   &finishTime,
 							Status: execution.TaskStatus{
 								State:  execution.TaskFailed,
-								Result: job.GetResultPtr(execution.JobResultTaskFailed),
+								Result: jobutil.GetResultPtr(execution.JobResultTaskFailed),
 							},
 						},
 						{
@@ -207,7 +207,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 							FinishTimestamp:   &finishTime2,
 							Status: execution.TaskStatus{
 								State:  execution.TaskFailed,
-								Result: job.GetResultPtr(execution.JobResultTaskFailed),
+								Result: jobutil.GetResultPtr(execution.JobResultTaskFailed),
 							},
 						},
 					},
@@ -233,7 +233,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 							FinishTimestamp:   &finishTime,
 							Status: execution.TaskStatus{
 								State:  execution.TaskFailed,
-								Result: job.GetResultPtr(execution.JobResultTaskFailed),
+								Result: jobutil.GetResultPtr(execution.JobResultTaskFailed),
 							},
 						},
 						{
@@ -242,7 +242,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 							FinishTimestamp:   &finishTime2,
 							Status: execution.TaskStatus{
 								State:  execution.TaskFailed,
-								Result: job.GetResultPtr(execution.JobResultTaskFailed),
+								Result: jobutil.GetResultPtr(execution.JobResultTaskFailed),
 							},
 						},
 					},
@@ -254,7 +254,7 @@ func TestGetNextAllowedRetry(t *testing.T) {
 	for _, tt := range tests {
 		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := job.GetNextAllowedRetry(tt.rj)
+			got, err := jobutil.GetNextAllowedRetry(tt.rj)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("GetNextAllowedRetry() error = %v, wantErr %v", err, tt.wantErr)
 				return
diff --git a/pkg/utils/execution/job/timeout.go b/pkg/execution/util/job/timeout.go
similarity index 100%
rename from pkg/utils/execution/job/timeout.go
rename to pkg/execution/util/job/timeout.go
diff --git a/pkg/utils/execution/job/utils.go b/pkg/execution/util/job/utils.go
similarity index 100%
rename from pkg/utils/execution/job/utils.go
rename to pkg/execution/util/job/utils.go
diff --git a/pkg/utils/execution/jobconfig/job.go b/pkg/execution/util/jobconfig/job.go
similarity index 100%
rename from pkg/utils/execution/jobconfig/job.go
rename to pkg/execution/util/jobconfig/job.go
diff --git a/pkg/utils/execution/jobconfig/job_utils.go b/pkg/execution/util/jobconfig/job_utils.go
similarity index 100%
rename from pkg/utils/execution/jobconfig/job_utils.go
rename to pkg/execution/util/jobconfig/job_utils.go
diff --git a/pkg/utils/execution/jobconfig/job_utils_test.go b/pkg/execution/util/jobconfig/job_utils_test.go
similarity index 97%
rename from pkg/utils/execution/jobconfig/job_utils_test.go
rename to pkg/execution/util/jobconfig/job_utils_test.go
index 48605c1..e6e01f3 100644
--- a/pkg/utils/execution/jobconfig/job_utils_test.go
+++ b/pkg/execution/util/jobconfig/job_utils_test.go
@@ -22,7 +22,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
-	"github.com/furiko-io/furiko/pkg/utils/execution/jobconfig"
+	"github.com/furiko-io/furiko/pkg/execution/util/jobconfig"
 )
 
 var (
diff --git a/pkg/utils/execution/jobconfig/labels.go b/pkg/execution/util/jobconfig/labels.go
similarity index 100%
rename from pkg/utils/execution/jobconfig/labels.go
rename to pkg/execution/util/jobconfig/labels.go
diff --git a/pkg/utils/execution/jobconfig/name.go b/pkg/execution/util/jobconfig/name.go
similarity index 100%
rename from pkg/utils/execution/jobconfig/name.go
rename to pkg/execution/util/jobconfig/name.go
diff --git a/pkg/utils/execution/jobconfig/state.go b/pkg/execution/util/jobconfig/state.go
similarity index 100%
rename from pkg/utils/execution/jobconfig/state.go
rename to pkg/execution/util/jobconfig/state.go
diff --git a/pkg/utils/execution/jobconfig/state_test.go b/pkg/execution/util/jobconfig/state_test.go
similarity index 97%
rename from pkg/utils/execution/jobconfig/state_test.go
rename to pkg/execution/util/jobconfig/state_test.go
index 2140f95..0d4d545 100644
--- a/pkg/utils/execution/jobconfig/state_test.go
+++ b/pkg/execution/util/jobconfig/state_test.go
@@ -20,7 +20,7 @@ import (
 	"testing"
 
 	execution "github.com/furiko-io/furiko/apis/execution/v1alpha1"
-	"github.com/furiko-io/furiko/pkg/utils/execution/jobconfig"
+	"github.com/furiko-io/furiko/pkg/execution/util/jobconfig"
 )
 
 var (
diff --git a/pkg/execution/validation/validation_test.go b/pkg/execution/validation/validation_test.go
index 1c1c5c2..04a88d0 100644
--- a/pkg/execution/validation/validation_test.go
+++ b/pkg/execution/validation/validation_test.go
@@ -33,10 +33,10 @@ import (
 
 	configv1alpha1 "github.com/furiko-io/furiko/apis/config/v1alpha1"
 	"github.com/furiko-io/furiko/apis/execution/v1alpha1"
+	"github.com/furiko-io/furiko/pkg/execution/util/jobconfig"
 	"github.com/furiko-io/furiko/pkg/execution/validation"
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
 	"github.com/furiko-io/furiko/pkg/runtime/controllercontext/mock"
-	"github.com/furiko-io/furiko/pkg/utils/execution/jobconfig"
 	"github.com/furiko-io/furiko/pkg/utils/testutils"
 )
 
diff --git a/pkg/utils/k8sutils/annotations_test.go b/pkg/utils/meta/annotations_test.go
similarity index 93%
rename from pkg/utils/k8sutils/annotations_test.go
rename to pkg/utils/meta/annotations_test.go
index 3aa5de4..653fb5d 100644
--- a/pkg/utils/k8sutils/annotations_test.go
+++ b/pkg/utils/meta/annotations_test.go
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
-package k8sutils_test
+package meta_test
 
 import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
 
-	"github.com/furiko-io/furiko/pkg/utils/k8sutils"
+	"github.com/furiko-io/furiko/pkg/utils/meta"
 )
 
 type object struct {
@@ -39,7 +39,7 @@ func (o *object) SetAnnotations(annotations map[string]string) {
 func TestSetAnnotation(t *testing.T) {
 	tests := []struct {
 		name   string
-		object k8sutils.ObjectMeta
+		object meta.ObjectMeta
 		key    string
 		value  string
 		want   map[string]string
@@ -87,7 +87,7 @@ func TestSetAnnotation(t *testing.T) {
 	for _, tt := range tests {
 		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
-			k8sutils.SetAnnotation(tt.object, tt.key, tt.value)
+			meta.SetAnnotation(tt.object, tt.key, tt.value)
 			got := tt.object.GetAnnotations()
 			if !cmp.Equal(got, tt.want) {
 				t.Errorf("annotations not equal after SetAnnotation():\ndiff = %v", cmp.Diff(tt.want, got))