derailed/k9s · error

expecting a job resource

Error message

expecting a job resource

What it means

Job.TailLogs fetches one Job from the informer cache and converts it into batchv1.Job to read its selector before delegating to podLogs (internal/dao/job.go:85). If conversion fails, the sentinel 'expecting a job resource' (lowercase 'a') is returned and the real converter error is discarded, so the offending field is invisible. It fires before any log streaming starts.

Solutions

  1. Inspect the job: kubectl get job.batch <name> -n <ns> -o yaml and fix unexpected or mistyped fields.
  2. Upgrade k9s to a build whose k8s.io/* dependencies match the cluster minor version.
  3. Confirm jobs.batch is served by the core apiserver.
  4. Restart k9s to rebuild informer caches.
  5. If embedding, wrap the converter error with %w to reveal the offending field.

Example fix

// before
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &job)
if err != nil {
    return nil, errors.New("expecting a job resource")
}
// after
u, ok := o.(*unstructured.Unstructured)
if !ok {
    return nil, fmt.Errorf("expected unstructured job, got %T", o)
}
var job batchv1.Job
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &job); err != nil {
    return nil, fmt.Errorf("job %q does not match v1 schema: %w", opts.Path, err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

o, err := factory.Get(client.JobGVR, opts.Path, true, labels.Everything())
if err != nil { return err }
u, ok := o.(*unstructured.Unstructured)
if !ok || u.GroupVersionKind().Kind != "Job" {
    return fmt.Errorf("not a job: %s", u.GroupVersionKind())
}

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

var job batchv1.Job
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &job); err != nil {
    return nil, fmt.Errorf("job %q conversion: %w", opts.Path, err)
}

Prevention

When it happens

Trigger: Pressing the logs key on a Job whose stored object cannot convert to the compiled batchv1.Job struct — fields added or retyped by a newer/older apiserver, an aggregated API shadowing jobs.batch, or webhook-patched out-of-schema values. Also hit by embedding code calling TailLogs on such an object.

Common situations: Log tailing failing on specific jobs after cluster upgrades while k9s uses older k8s.io/api; jobs mutated by custom controllers with legacy fields; version skew between k9s and the cluster.

Related errors


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

Appendix: source

Thrown at internal/dao/job.go:85

				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) {
	o, err := j.getFactory().Get(j.gvr, opts.Path, true, labels.Everything())
	if err != nil {
		return nil, err
	}

	var job batchv1.Job
	err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &job)
	if err != nil {
		return nil, errors.New("expecting a job resource")
	}

	if job.Spec.Selector == nil || len(job.Spec.Selector.MatchLabels) == 0 {
		return nil, fmt.Errorf("no valid selector found for job: %s", opts.Path)
	}

	return podLogs(ctx, job.Spec.Selector.MatchLabels, opts)
}

func (j *Job) GetInstance(fqn string) (*batchv1.Job, error) {
	o, err := j.getFactory().Get(j.gvr, fqn, true, labels.Everything())
	if err != nil {
		return nil, err
	}

	var job batchv1.Job
	err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &job)
	if err != nil {

View on GitHub (pinned to 2d3ccc6ba2)