{"record":{"id":"f4bc6f8f5f17db9f","repo":"hashicorp/nomad","slug":"node-id-cannot-be-updated","errorCode":null,"errorMessage":"node ID cannot be updated","messagePattern":"node ID cannot be updated","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nomad/structs/host_volumes.go","lineNumber":189,"sourceCode":"}\n\n// ValidateUpdate verifies that an update to a volume is safe to make.\nfunc (hv *HostVolume) ValidateUpdate(existing *HostVolume) error {\n\tif existing == nil {\n\t\treturn nil\n\t}\n\n\tvar mErr *multierror.Error\n\tif len(existing.Allocations) > 0 {\n\t\tallocIDs := helper.ConvertSlice(existing.Allocations,\n\t\t\tfunc(a *AllocListStub) string { return a.ID })\n\t\tmErr = multierror.Append(mErr, fmt.Errorf(\n\t\t\t\"cannot update a volume in use: claimed by allocs (%s)\",\n\t\t\tstrings.Join(allocIDs, \", \")))\n\t}\n\n\tif hv.NodeID != \"\" && hv.NodeID != existing.NodeID {\n\t\tmErr = multierror.Append(mErr, errors.New(\"node ID cannot be updated\"))\n\t}\n\tif hv.NodePool != \"\" && hv.NodePool != existing.NodePool {\n\t\tmErr = multierror.Append(mErr, errors.New(\"node pool cannot be updated\"))\n\t}\n\n\tif hv.RequestedCapacityMaxBytes > 0 &&\n\t\thv.RequestedCapacityMaxBytes < existing.CapacityBytes {\n\t\tmErr = multierror.Append(mErr, fmt.Errorf(\n\t\t\t\"capacity_max (%d) cannot be less than existing provisioned capacity (%d)\",\n\t\t\thv.RequestedCapacityMaxBytes, existing.CapacityBytes))\n\t}\n\n\treturn mErr.ErrorOrNil()\n}\n\nconst DefaultHostVolumePlugin = \"default\"\n\n// CanonicalizeForCreate is called in the RPC handler to ensure we call client","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/nomad/structs/host_volumes.go#L171-L207","documentation":"HostVolume.ValidateUpdate prevents mutating a host volume's node placement after creation. If the update request sets NodeID and it differs from the existing volume's NodeID, the sentinel 'node ID cannot be updated' is appended. Immutable placement keeps allocations and scheduler bookkeeping consistent.","triggerScenarios":"Calling ValidateUpdate (via validateVolumeUpdate) on a host volume where the incoming hv.NodeID is non-empty and != existing.NodeID.","commonSituations":"Attempting to move a volume to a different node via the volumes update API; copy/pasting a volume definition between nodes and re-registering; automation that rewrites the whole volume spec including node_id.","solutions":["Remove the node_id change — keep it identical to the existing volume's NodeID","Omit node_id from the update payload entirely (empty NodeID skips this check)","Delete and re-create the volume on the desired node if relocation is truly needed","Validate the diff before submission to ensure only mutable fields (e.g. capacity, capabilities) change"],"exampleFix":"// before\nupdate := existing.Copy()\nupdate.NodeID = \"new-node-uuid\" // mutation not allowed\n\n// after\nupdate := existing.Copy()\n// NodeID left unchanged; update only mutable fields\nupdate.RequestedCapacityMaxBytes = 200 * 1024 * 1024 * 1024","handlingStrategy":"validation","validationCode":"if update.NodeID != \"\" && update.NodeID != existing.NodeID {\n\treturn fmt.Errorf(\"node_id is immutable (%s != %s)\", existing.NodeID, update.NodeID)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Build updates by copying existing volume and mutating only allowed fields","Treat node_id/node_pool as read-only after creation","Diff update payloads against current state in automation"],"tags":["nomad","host-volume","immutability","update-validation"],"backgroundTag":"immutable-field-update","analyzedSha":"482b49bf1aec006f089bcfc7e632d8f6ac303e5e","analyzedAt":"2026-09-04T07:54:14.808Z","contentChangedAt":"2026-09-04T07:54:14.808Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}