derailed/k9s · error

expecting Job resource

Error message

expecting Job resource

What it means

Job.List fetches all Jobs in a namespace via the generic resource list, converts each *unstructured.Unstructured into batchv1.Job, and filters by owner reference against the controller path carried in ctx (internal.KeyPath) for the CronJob detail view (internal/dao/job.go:58). If any listed Job fails conversion, the sentinel 'expecting Job resource' aborts the whole listing and the underlying converter error is dropped.

Source

Thrown at internal/dao/job.go:58

	return render.ExtractImages(&job.Spec.Template.Spec), nil
}

// List returns a collection of resources.
func (j *Job) List(ctx context.Context, ns string) ([]runtime.Object, error) {
	oo, err := j.Resource.List(ctx, ns)
	if err != nil {
		return nil, err
	}
	ctrl, _ := ctx.Value(internal.KeyPath).(string)
	_, n := client.Namespaced(ctrl)

	ll := make([]runtime.Object, 0, 10)
	for _, o := range oo {
		var j batchv1.Job
		err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &j)
		if err != nil {
			return nil, errors.New("expecting Job resource")
		}
		if n == "" {
			ll = append(ll, o)
			continue
		}

		for _, r := range j.OwnerReferences {
			if r.Name == n {
				ll = append(ll, o)
			}
		}
	}

	return ll, nil
}

// TailLogs tail logs for all pods represented by this Job.
func (j *Job) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) {

View on GitHub (pinned to 2d3ccc6ba2)

Solutions

  1. Audit the namespace: kubectl get jobs.batch -n <ns> -o yaml and look for unexpected or mistyped fields; fix offending manifests.
  2. Upgrade k9s so its vendored k8s.io/* versions match the cluster.
  3. Verify jobs.batch is served by the core apiserver and not shadowed.
  4. Restart k9s to rebuild informer caches.
  5. Patch the loop to skip and log unparsable Jobs instead of failing the list.

Example fix

// before
var j batchv1.Job
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &j)
if err != nil {
    return nil, errors.New("expecting Job resource")
}
// after
u, ok := o.(*unstructured.Unstructured)
if !ok {
    continue
}
var j batchv1.Job
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &j); err != nil {
    slog.Warn("skip unparsable job", "fqn", client.FQN(u.GetNamespace(), u.GetName()), "err", err)
    continue
}
Defensive patterns

Strategy: try-catch

Validate before calling

for _, o := range oo {
    u, ok := o.(*unstructured.Unstructured)
    if !ok || u.GroupVersionKind().Kind != "Job" {
        continue
    }
    // safe to convert below
}

Type guard

func isJob(o runtime.Object) bool {
    u, ok := o.(*unstructured.Unstructured)
    return ok && u.GroupVersionKind().Kind == "Job" && u.GroupVersionKind().Group == "batch"
}

Try / catch

if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &j); err != nil {
    slog.Warn("skip unparsable job", "fqn", fqn, "err", err)
    continue // listing must survive one bad object
}

Prevention

When it happens

Trigger: Browsing jobs (or opening a CronJob's owned-jobs list) in a namespace where at least one stored Job object cannot convert to the compiled batchv1.Job struct — k9s/cluster version skew, an aggregated API shadowing jobs.batch, or jobs patched with out-of-schema fields. A single bad Job breaks the entire job browser for that namespace.

Common situations: k9s builds with older k8s.io/api against newer clusters where job spec/status fields changed; namespaces with Jobs created by legacy CRD controllers; GitOps force-applied legacy job manifests.

Related errors


AI-assisted analysis of derailed/k9s@2d3ccc6ba2 (2026-08-15). Data as JSON: /api/errors/dda11988de66d372. Report an issue: GitHub.