diff --git a/hscontrol/state/rename_test.go b/hscontrol/state/rename_test.go index 551a02de9..336154746 100644 --- a/hscontrol/state/rename_test.go +++ b/hscontrol/state/rename_test.go @@ -62,6 +62,7 @@ func TestNodeNameCannotShadowExtraRecord(t *testing.T) { _, _, err = s.RenameNode(existing.ID(), "Svc") require.ErrorIs(t, err, ErrNodeNameNotUnique) + require.EqualError(t, err, `name "Svc" is already taken`) _, err = s.UpdateNodeFromMapRequest(existing.ID(), tailcfg.MapRequest{ NodeKey: existing.NodeKey(), Hostinfo: &tailcfg.Hostinfo{Hostname: "svc"}, }) diff --git a/hscontrol/state/state.go b/hscontrol/state/state.go index 944ac6d67..5563cc9f6 100644 --- a/hscontrol/state/state.go +++ b/hscontrol/state/state.go @@ -89,8 +89,9 @@ var ErrInvalidNodeView = errors.New("invalid node view provided") // ErrNodeNotInNodeStore is returned when a node no longer exists in the [NodeStore]. var ErrNodeNotInNodeStore = errors.New("node no longer exists in NodeStore") -// ErrNodeNameNotUnique is returned when a node name is not unique. -var ErrNodeNameNotUnique = errors.New("node name is not unique") +// ErrNodeNameNotUnique is returned when a node name is not unique, wrapped +// as `name "X" is already taken`. +var ErrNodeNameNotUnique = errors.New("already taken") // nodeUpdateColumns lists all Node columns that should be written // during a struct-based GORM Updates() call. Listing them explicitly @@ -1297,6 +1298,8 @@ func (s *State) updateApprovedRoutes(nodeID types.NodeID, set UpdateNodeFunc) (t // auto-sanitisation) and collisions error out rather than silently // bumping a user-facing label. See HOSTNAME.md for the CLI contract. func (s *State) RenameNode(nodeID types.NodeID, newName string) (types.NodeView, change.Change, error) { + asked := newName + // DNS is case-insensitive but uniqueness and the client's certificate // name match are not, so "Foo" and "foo" would share one name. newName = strings.ToLower(newName) @@ -1315,7 +1318,7 @@ func (s *State) RenameNode(nodeID types.NodeID, newName string) (types.NodeView, if err != nil { switch { case errors.Is(err, ErrGivenNameTaken): - return types.NodeView{}, change.Change{}, fmt.Errorf("%w: %s", ErrNodeNameNotUnique, newName) + return types.NodeView{}, change.Change{}, fmt.Errorf("name %q is %w", asked, ErrNodeNameNotUnique) case errors.Is(err, ErrNodeNotFound): return types.NodeView{}, change.Change{}, fmt.Errorf("%w: %d", ErrNodeNotInNodeStore, nodeID) default: