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 []*VolumeCapabilityView on GitHub (pinned to 482b49bf1a)
Solutions
- Set ExternalID from the persisted volume claim state before calling ControllerUnpublishVolume
- If the volume was deleted, skip unpublish rather than calling it with an empty ID
- 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
- Persist ExternalVolumeID on the claim at publish time and read it during teardown
- Skip controller unpublish for claims that never reached the publish stage
- Log claim state before teardown RPCs to spot lost IDs
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
- missing StagingTargetPath
- missing NodeID
- missing Name
- one of LimitBytes or RequiredBytes must be set if CapacityRa
- CSI.ControllerAttachVolume: VolumeID is required
AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04).
Data as JSON: /api/errors/73f7a36c88ac0f36.
Report an issue: GitHub.