argoproj/argo-workflows · error

object %+v is not an unstructured

Error message

object %+v is not an unstructured

What it means

In deleteOffloadedNodesForWorkflow (workflow/controller/controller.go:841), the UID index lookup over the workflow informer returned exactly one object, but its dynamic type is not *unstructured.Unstructured. The informer cache is expected to hold unstructured objects, so this indicates an unexpected object type in the cache. It is a defensive type-assertion failure inside the offloaded-node-status GC.

Source

Thrown at workflow/controller/controller.go:841

			}
		}
	}
}

func (wfc *WorkflowController) deleteOffloadedNodesForWorkflow(ctx context.Context, uid string, versions []string) error {
	workflows, err := wfc.wfInformer.GetIndexer().ByIndex(indexes.UIDIndex, uid)
	if err != nil {
		return err
	}
	var wf *wfv1.Workflow
	logger := logging.RequireLoggerFromContext(ctx)
	switch l := len(workflows); l {
	case 0:
		logger.WithField("uid", uid).Info(ctx, "Workflow missing, probably deleted")
	case 1:
		un, ok := workflows[0].(*unstructured.Unstructured)
		if !ok {
			return fmt.Errorf("object %+v is not an unstructured", workflows[0])
		}
		key := un.GetNamespace() + "/" + un.GetName()
		wfc.workflowKeyLock.Lock(key)
		defer wfc.workflowKeyLock.Unlock(key)

		obj, ok := wfc.getWorkflowByKey(ctx, key)
		if !ok {
			return fmt.Errorf("failed to get workflow by key after locking")
		}
		un, ok = obj.(*unstructured.Unstructured)
		if !ok {
			return fmt.Errorf("object %+v is not an unstructured", obj)
		}
		wf, err = util.FromUnstructured(un)
		if err != nil {
			return err
		}

View on GitHub (pinned to 35bff19146)

Solutions

  1. Verify the workflow informer is created with an unstructured decoder (unstructured scheme) as the stock controller does.
  2. If you embed the controller, pass the same unstructured informer type the controller constructs for itself.
  3. In tests, seed the indexer with *unstructured.Unstructured objects built via util.FromWorkflow, not typed *wfv1.Workflow.

Example fix

// before (test seeding typed object)
indexer.Add(&wfv1.Workflow{ObjectMeta: metav1.ObjectMeta{Name: "wf", Namespace: "ns"}})
// after
un, _ := util.ToUnstructured(&wfv1.Workflow{ObjectMeta: metav1.ObjectMeta{Name: "wf", Namespace: "ns"}})
indexer.Add(un)
Defensive patterns

Strategy: type-guard

Validate before calling

// Before relying on informer objects, verify cache contents
if !isUnstructured(obj) { log.Fatalf("workflow informer cache holds non-unstructured object: %T", obj) }

Type guard

func isUnstructured(obj any) bool {
    _, ok := obj.(*unstructured.Unstructured)
    return ok
}

Try / catch

un, ok := workflows[0].(*unstructured.Unstructured)
if !ok {
    return fmt.Errorf("object %+v is not an unstructured", workflows[0])
}

Prevention

When it happens

Trigger: The workflow informer's indexer (UIDIndex) returned an object whose concrete type is not *unstructured.Unstructured — e.g. the informer was configured with a typed decoder or a test injected a *wfv1.Workflow instead of an unstructured object. Practically only reachable with a misconfigured informer or non-standard embedding.

Common situations: Custom controller builds or tests that wire the workflow informer to typed objects instead of unstructured ones; upstream client-go behavior changes in exotic setups.

Related errors


AI-assisted analysis of argoproj/argo-workflows@35bff19146 (2026-09-03). Data as JSON: /api/errors/6932ab822221fcaa. Report an issue: GitHub.