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.

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)

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.