Jobs

kontinue can spawn Kubernetes Jobs as part of an Execution. Use Jobs to run external workloads, scripts, or CLI tools that need to execute outside the worker process. This is useful for running pre-existing tools like helm, kubectl, terraform, or any containerized script.

Jobs are tracked like other child resources (Executions, Suspensions) with labels and OwnerReferences. They respect the parent’s child policy for retry behavior and are fully durable — if the worker crashes while waiting for a Job, it will resume waiting when restarted.

RunJob

Use kontinue.RunJob() to run a Kubernetes Job and wait for it to complete:

func DeployWithHelm(ktx *kontinue.ExecutionContext, args *DeployArgs) error {
    result, err := kontinue.RunJob(ktx, &kontinue.JobOptions{
        Spec: batchv1.JobSpec{
            Template: corev1.PodTemplateSpec{
                Spec: corev1.PodSpec{
                    Containers: []corev1.Container{{
                        Name:  "helm",
                        Image: "alpine/helm:3.14",
                        Command: []string{"helm"},
                        Args: []string{
                            "upgrade", "--install",
                            args.ReleaseName,
                            args.ChartPath,
                            "--namespace", args.Namespace,
                            "--wait",
                        },
                    }},
                    RestartPolicy: corev1.RestartPolicyNever,
                    ServiceAccountName: "helm-deployer",
                },
            },
        },
    })
    if err != nil {
        return err
    }

    if !result.Succeeded() {
        return fmt.Errorf("helm install failed: %s", result.Message)
    }

    return nil
}

The Job is created in the same namespace as the Execution and is automatically cleaned up based on the Execution’s TTL settings.

RunPod

Use kontinue.RunPod() when you need detailed information about a single pod execution, including container exit codes and access to logs:

func RunMigration(ktx *kontinue.ExecutionContext, args *MigrationArgs) error {
    result, err := kontinue.RunPod(ktx, &kontinue.PodOptions{
        Spec: corev1.PodSpec{
            Containers: []corev1.Container{{
                Name:  "migrate",
                Image: "myapp/migrations:latest",
                Command: []string{"./migrate", "up"},
                Env: []corev1.EnvVar{{
                    Name:  "DATABASE_URL",
                    Value: args.DatabaseURL,
                }},
            }},
        },
    })
    if err != nil {
        return err
    }

    if !result.Succeeded() {
        // Fetch the last line of logs for error context
        summary, _ := result.LogErrorSummary(ktx)
        return fmt.Errorf("migration failed: %s (logs: %s)", result.Message, summary)
    }

    return nil
}

RunPod creates a Job under the hood with BackoffLimit: 0 (no retries at the Job level), ensuring exactly one pod is created. It returns a PodResult with detailed pod information.

RunScript

For simple command execution, use kontinue.RunScript() as a convenience wrapper around RunPod:

result, err := kontinue.RunScript(ktx, &kontinue.ScriptOptions{
    Image:   "alpine:latest",
    Command: "echo 'Hello, World!' && sleep 5",
})
if err != nil {
    return err
}

if !result.Succeeded() {
    logs, _ := result.Logs(ktx, &kontinue.LogOptions{}) // Fetch last 100 lines
    return fmt.Errorf("script failed: %s", logs)
}

This creates a Job with a single container running the command via shell. The default shell is /bin/sh -c, but you can override it:

// Use bash
result, err := kontinue.RunScript(ktx, &kontinue.ScriptOptions{
    Image:   "bash:5",
    Command: "echo $BASH_VERSION",
    Shell:   []string{"/bin/bash", "-c"},
})

// Run Python script
result, err := kontinue.RunScript(ktx, &kontinue.ScriptOptions{
    Image:   "python:3.12-slim",
    Command: "print('Hello from Python')",
    Shell:   []string{"/usr/bin/python", "-c"},
})

JobOptions

type JobOptions struct {
    StepOptions
    Spec             batchv1.JobSpec   // The Kubernetes Job spec
    ExtraAnnotations map[string]string // Additional annotations
    ExtraLabels      map[string]string // Additional labels
}

FieldDescription
SpecFull Kubernetes JobSpec for complete control over the Job
ExtraAnnotationsAdditional annotations to add to the Job
ExtraLabelsAdditional labels to add to the Job

PodOptions

type PodOptions struct {
    StepOptions
    Spec             corev1.PodSpec    // The pod specification to run
    ExtraAnnotations map[string]string // Additional annotations
    ExtraLabels      map[string]string // Additional labels
}

FieldDescription
SpecPod specification. RestartPolicy is automatically set to Never.
ExtraAnnotationsAdditional annotations to add to the Job
ExtraLabelsAdditional labels to add to the Job

ScriptOptions

type ScriptOptions struct {
    StepOptions
    Image            string            // Container image to use
    Command          string            // Script/command to run
    Shell            []string          // Shell command (default: ["/bin/sh", "-c"])
    ExtraAnnotations map[string]string
    ExtraLabels      map[string]string
}

JobResult

RunJob returns a JobResult with information about all pods in the job:

type JobResult struct {
    SucceededPods int32        // Number of pods that completed successfully
    FailedPods    int32        // Number of pods that failed
    Message       string       // Error or status message (set on failure)
    Pods          []*PodResult // Results for all pods created by the job
}

func (r *JobResult) Succeeded() bool // Returns true if SucceededPods > 0 && FailedPods == 0

Check the result to handle success or failure:

result, err := kontinue.RunJob(ktx, opts)
if err != nil {
    // Infrastructure error (e.g., couldn't create Job)
    return err
}

if !result.Succeeded() {
    // Job ran but failed
    return fmt.Errorf("job failed: %s", result.Message)
}

// Access individual pod results
for _, pod := range result.Pods {
    fmt.Printf("Pod %s: phase=%s\n", pod.Pod.Name, pod.Phase)
}

PodResult

RunPod and RunScript return a PodResult with detailed pod information:

type PodResult struct {
    Pod     *corev1.Pod    // The completed Pod resource
    Message string         // Status message from pod conditions
    Phase   corev1.PodPhase // Pod phase (Succeeded, Failed, etc.)
}

func (r *PodResult) Succeeded() bool // Returns true if all containers exited with code 0
func (r *PodResult) Logs(ktx *ExecutionContext, opts *LogOptions) (string, error)
func (r *PodResult) LogErrorSummary(ktx *ExecutionContext) (string, error)

Checking Success

Succeeded() checks that all containers (including init containers) terminated with exit code 0:

if !result.Succeeded() {
    return fmt.Errorf("pod failed with phase: %s", result.Phase)
}

Fetching Logs

Use Logs() to fetch pod logs. By default, it returns the last 100 lines:

// Fetch last 100 lines (default)
logs, err := result.Logs(ktx, &kontinue.LogOptions{})

// Fetch last 500 lines
logs, err := result.Logs(ktx, &kontinue.LogOptions{
    TailLines: 500,
})

// Fetch logs from a specific container
logs, err := result.Logs(ktx, &kontinue.LogOptions{
    Container: "sidecar",
})

Error Summary

Use LogErrorSummary() to get the last line of logs, useful for error messages:

if !result.Succeeded() {
    summary, _ := result.LogErrorSummary(ktx)
    return fmt.Errorf("script failed: %s", summary)
}

LogOptions

type LogOptions struct {
    TailLines int64  // Number of lines from the end (default: 100)
    Container string // Container name (optional, defaults to first container)
}