Repository: getsentry/sentry
Pull request: fix(cli): Create user fails with --force-update
Fixes #19139.


Problem statement:
createuser: Error when trying to --force-update user
`createuser` fails when run against an existing user. 

```
$ sudo docker-compose run --rm web sentry --version
sentry, version 10.1.0.dev0 (0bf9ffa08ff2)
$ sudo docker-compose run --rm web sentry createuser --email testuser@test.com --password pass1 --no-superuser --no-input --force-update                                                                  
21:32:17 [WARNING] sentry.utils.geo: settings.GEOIP_PATH_MMDB not configured.
21:32:21 [INFO] sentry.plugins.github: apps-not-configured
User created: testuser@test.com
Added to organization: sentry
$ sudo docker-compose run --rm web sentry createuser --email testuser@test.com --password pass2 --no-superuser --no-input --force-update                                                                  
21:33:46 [WARNING] sentry.utils.geo: settings.GEOIP_PATH_MMDB not configured.
21:33:49 [INFO] sentry.plugins.github: apps-not-configured
Traceback (most recent call last):
  File "/usr/local/bin/sentry", line 8, in <module>
    sys.exit(main())
  File "/usr/local/lib/python2.7/site-packages/sentry/runner/__init__.py", line 166, in main
    cli(prog_name=get_prog(), obj={}, max_content_width=100)
  File "/usr/local/lib/python2.7/site-packages/click/core.py", line 722, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/click/core.py", line 697, in main
    rv = self.invoke(ctx)
  File "/usr/local/lib/python2.7/site-packages/click/core.py", line 1066, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/local/lib/python2.7/site-packages/click/core.py", line 895, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/local/lib/python2.7/site-packages/click/core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/click/decorators.py", line 17, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/sentry/runner/decorators.py", line 30, in inner
    return ctx.invoke(f, *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/click/core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/sentry/runner/commands/createuser.py", line 83, in createuser
    user.save(force_update=force_update)
  File "/usr/local/lib/python2.7/site-packages/sentry/models/user.py", line 141, in save
    return super(User, self).save(*args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/django/contrib/auth/base_user.py", line 80, in save
    super(AbstractBaseUser, self).save(*args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/django/db/models/base.py", line 808, in save
    force_update=force_update, update_fields=update_fields)
  File "/usr/local/lib/python2.7/site-packages/django/db/models/base.py", line 838, in save_base
    updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
  File "/usr/local/lib/python2.7/site-packages/django/db/models/base.py", line 896, in _save_table
    raise ValueError("Cannot force an update in save() with no primary key.")
ValueError: Cannot force an update in save() with no primary key.
```

Hints from issue discussion:
We have the same issue.
same thing
I experienced that issue too. The whole redis configuration went down causing downtime.
`root@sentry-web:/# sentry createuser
09:03:05 [WARNING] sentry.utils.geo: settings.GEOIP_PATH_MMDB not configured.
09:03:13 [INFO] sentry.plugins.github: apps-not-configured
Email: admin@sentry.local
Password: 
Repeat for confirmation: 
Should this user be a superuser? [y/N]: y
`
I also just experienced this issue.
This issue has gone three weeks without activity. In another week, I will close it.

But! If you comment or otherwise update it, I will reset the clock, and if you label it `Status: Accepted`, I will leave it alone ... forever!

----

"A weed is but an unloved flower." ― _Ella Wheeler Wilcox_ 🥀
Same here
Won't close this without confirming.
Just replicated locally, will try to submit a fix soon.

Commit message: don't use create_user

Patch to review:
diff --git a/src/sentry/runner/commands/createuser.py b/src/sentry/runner/commands/createuser.py
index 96853014dd128a..5baaa1adebc557 100644
--- a/src/sentry/runner/commands/createuser.py
+++ b/src/sentry/runner/commands/createuser.py
@@ -69,23 +69,27 @@ def createuser(email, password, superuser, no_password, no_input, force_update):
     from sentry.models import User
     from django.conf import settings
 
-    user = User(
+    fields = dict(
         email=email, username=email, is_superuser=superuser, is_staff=superuser, is_active=True
     )
 
-    if password:
-        user.set_password(password)
+    verb = None
+    try:
+        user = User.objects.get(username=email)
+    except User.DoesNotExist:
+        user = None
 
-    if User.objects.filter(username=email).exists():
+    if user is not None:
         if force_update:
-            user.save(force_update=force_update)
-            click.echo(f"User updated: {email}")
+            user.update(**fields)
+            verb = "updated"
         else:
             click.echo(f"User: {email} exists, use --force-update to force")
             sys.exit(3)
     else:
+        user = User(**fields)
         user.save()
-        click.echo(f"User created: {email}")
+        verb = "created"
 
         # TODO(dcramer): kill this when we improve flows
         if settings.SENTRY_SINGLE_ORGANIZATION:
@@ -104,3 +108,9 @@ def createuser(email, password, superuser, no_password, no_input, force_update):
             if len(teams) == 1:
                 OrganizationMemberTeam.objects.create(team=teams[0], organizationmember=member)
             click.echo(f"Added to organization: {org.slug}")
+
+    if password:
+        user.set_password(password)
+        user.save()
+
+    click.echo(f"User {verb}: {email}")


Code review comments:
diff_hunk: @@ -69,23 +69,27 @@ def createuser(email, password, superuser, no_password, no_input, force_update):
     from sentry.models import User
     from django.conf import settings
 
-    user = User(
+    fields = dict(
         email=email, username=email, is_superuser=superuser, is_staff=superuser, is_active=True
     )
 
-    if password:
-        user.set_password(password)
+    verb = None
+    try:
+        user = User.objects.get(username=email)
+    except User.DoesNotExist:
+        user = None
 
-    if User.objects.filter(username=email).exists():
+    if user is not None:
         if force_update:
-            user.save(force_update=force_update)
-            click.echo(f"User updated: {email}")
+            user.update(**fields)
+            verb = "updated"
         else:
             click.echo(f"User: {email} exists, use --force-update to force")
             sys.exit(3)
     else:
+        user = User(**fields)
path: src/sentry/runner/commands/createuser.py
text: @user1:
You could just use `User.objects.create` here as well

@author:
Ah, didn't know about this one. I thought Python was the language where there was only one obvious way to do a certain thing? 🤣