hashicorp/nomad · error

missing ExternalID

Error message

missing ExternalID

What it means

ControllerUnpublishVolumeRequest.Validate() rejects requests with an empty ExternalID, the provider-side volume identifier to detach. Without it the plugin cannot identify which volume to un-publish, so it fails before issuing the controller RPC.

Source

Thrown at plugins/csi/plugin.go:446

	NodeID     string
	Secrets    structs.CSISecrets
}

func (r *ControllerUnpublishVolumeRequest) ToCSIRepresentation() *csipbv1.ControllerUnpublishVolumeRequest {
	if r == nil {
		return nil
	}

	return &csipbv1.ControllerUnpublishVolumeRequest{
		VolumeId: r.ExternalID,
		NodeId:   r.NodeID,
		Secrets:  r.Secrets,
	}
}

func (r *ControllerUnpublishVolumeRequest) Validate() error {
	if r.ExternalID == "" {
		return errors.New("missing ExternalID")
	}
	if r.NodeID == "" {
		// the spec allows this but it would unpublish the
		// volume from all nodes
		return errors.New("missing NodeID")
	}
	return nil
}

type ControllerUnpublishVolumeResponse struct{}

type ControllerCreateVolumeRequest struct {
	// note that Name is intentionally differentiated from both CSIVolume.ID
	// and ExternalVolumeID. This name is only a recommendation for the
	// storage provider, and many will discard this suggestion
	Name                      string
	CapacityRange             *CapacityRange
	VolumeCapabilities        []*VolumeCapability

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Set ExternalID from the persisted volume claim state before calling ControllerUnpublishVolume
  2. If the volume was deleted, skip unpublish rather than calling it with an empty ID
  3. Persist the external ID at publish time so teardown can recover it

Example fix

// before
req := &csi.ControllerUnpublishVolumeRequest{
    NodeID: nodeID,
}
// after
req := &csi.ControllerUnpublishVolumeRequest{
    ExternalID: claim.ExternalVolumeID,
    NodeID:     nodeID,
}
Defensive patterns

Strategy: validation

Validate before calling

func validateUnpublish(req *csi.ControllerUnpublishVolumeRequest) error {
    if req.ExternalID == "" {
        return errors.New("ExternalID required for unpublish; recover it from claim state")
    }
    return nil
}

Type guard

func canUnpublish(req *csi.ControllerUnpublishVolumeRequest) bool {
    return req != nil && req.ExternalID != "" && req.NodeID != ""
}

Try / catch

if err := req.Validate(); err != nil {
    if strings.Contains(err.Error(), "missing ExternalID") {
        return fmt.Errorf("unpublish refused: volume external ID unavailable (was the volume already deleted?): %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling ControllerUnpublishVolume with ExternalID empty, e.g. a zero-value request, a lost ID across retries, or reconcilers that only carry NodeID for teardown.

Common situations: Teardown code paths where the volume was already deleted and its external ID is gone; state store corruption/deserialization losing the external ID; hand-built unpublish requests in tests.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/73f7a36c88ac0f36. Report an issue: GitHub.