{"record":{"id":"1a45cbf78bd4991e","repo":"hashicorp/nomad","slug":"node-pool-cannot-be-updated","errorCode":null,"errorMessage":"node pool cannot be updated","messagePattern":"node pool cannot be updated","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nomad/structs/host_volumes.go","lineNumber":192,"sourceCode":"func (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\n// RPCs with correctly populated fields from the existing volume, even if the\n// RPC request includes otherwise valid zero-values. This method should be\n// called on request objects or a copy, never on a state store object directly.","sourceCodeStart":174,"sourceCodeEnd":210,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/nomad/structs/host_volumes.go#L174-L210","documentation":"Like NodeID, a host volume's NodePool is immutable after creation. ValidateUpdate appends 'node pool cannot be updated' when the request supplies a non-empty NodePool that differs from existing.NodePool. This prevents volumes from silently moving between node pools while allocations reference them.","triggerScenarios":"Calling ValidateUpdate on a host volume where hv.NodePool is non-empty and differs from the existing volume's NodePool.","commonSituations":"Renaming/organizing node pools and trying to move existing volumes into the new pool; multi-cluster tooling that stamps a pool name onto update specs; policy changes reassigning volumes across pools.","solutions":["Keep node_pool identical to the existing volume's value in update requests","Omit node_pool from the update payload (empty string bypasses the check)","Create a new volume in the target node pool and migrate workloads instead","Adjust automation to preserve immutable fields (node_id, node_pool) on update"],"exampleFix":"// before\nupdate := existing.Copy()\nupdate.NodePool = \"gpu-pool\" // not allowed\n\n// after\nupdate := existing.Copy()\n// keep update.NodePool == existing.NodePool\nupdate.RequestedCapacityMaxBytes = 200 * 1024 * 1024 * 1024","handlingStrategy":"validation","validationCode":"if update.NodePool != \"\" && update.NodePool != existing.NodePool {\n\treturn fmt.Errorf(\"node_pool is immutable (%s != %s)\", existing.NodePool, update.NodePool)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Preserve existing node_pool in update tooling","Plan node-pool moves as create-new-volume + migrate operations","Whitelist mutable fields in automation that updates volumes"],"tags":["nomad","host-volume","node-pool","immutability"],"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"}