Repository: getsentry/sentry

Issue:
User-creation should not fail, when the account already exists
I'm creating an Ansible role for installing sentry on our network. The software is installed via `pip` -- version 8.22.0, which seems to be the latest as of today. Not using Docker -- just a dedicated VM.

One of the tasks is to create a Sentry-admin:
```
sentry createuser --superuser --no-input --password=FOO --email=sentry@example.com
```
Unfortunately, the above command only works _the first time_. Trying to run it again fails:
```
django.db.utils.IntegrityError: IntegrityError('duplicate key value violates unique constraint \"auth_user_username_key\"
DETAIL:  Key (username)=(sentry@example.com) already exists
```
I understand, why the same account can not be created again. But it should not be an _error_ -- simply update the existing record if necessary. Or, if it must be an error, there should be a specific exit-code for this case, which would allow systems like Ansible to move on. Currently, `sentry` exits with the same code 1 for all errors...

At the very least, there needs to be a way to check, whether the account exists already... Ansible would then be able to perform the query before attempting to add the user.



Discussion:
That's the same issue with standard createsuperuser command, a solution I use for now is failed_when: false. But a better solution would be to register the task and then use the stdout attribute of the register variable to improve the failed_when check.

```
  - name: Create super users
    failed_when: false
    with_items: '{{ active_users }}'
    tags: users
    expect:
      timeout: 30
      command: docker exec -it {{ django_instance }} su uwsgi -c bash -c "django-admin createsuperuser"
      responses:
        'Username': ["{{ item['name'] }}"]
        'Email': ["{{ item['email'] }}"]
        'Password': ["{{ admin_passwords[item['name']] }}", "{{ admin_passwords[item['name']] }}"]
```

I think we should just contribute to make a better createsuperuser command in Django.
We already implement our own command entirely: https://github.com/getsentry/sentry/blob/master/src/sentry/runner/commands/createuser.py
I recon it really shouldn't fail, because it will log the error:

![2018-01-15-045002_1139x852_scrot](https://user-images.githubusercontent.com/94636/34926433-bfbb671c-f9af-11e7-808c-fbc9108811e5.png)

Note that I'm using createuser now, thanks for the heads up !

```
  - name: Create super users
    failed_when: false
    with_items: '{{ active_users }}'
    tags: users
    command:
      docker run
      -t
      --rm
      --network sentry
      --env SENTRY_DB_PASSWORD={{ sentry_postgresql_password }}
      --env SENTRY_DB_USER=sentry
      --env SENTRY_POSTGRES_HOST=sentry-postgres
      --env SENTRY_REDIS_HOST=sentry-redis
      --env SENTRY_SECRET_KEY={{ sentry_secret_key }}
      sentry sentry createuser --no-input
      --superuser
      --email {{ item['email'] }}
      --password {{ admin_passwords[item['name']] }}
```

Resolving patch:
diff --git a/src/sentry/runner/commands/createuser.py b/src/sentry/runner/commands/createuser.py
--- a/src/sentry/runner/commands/createuser.py
+++ b/src/sentry/runner/commands/createuser.py
@@ -1,6 +1,7 @@
 from __future__ import absolute_import, print_function
 
 import click
+import sys
 from sentry.runner.decorators import configuration
 
 
@@ -42,8 +43,9 @@ def _get_superuser():
 @click.option("--superuser/--no-superuser", default=None, is_flag=True)
 @click.option("--no-password", default=False, is_flag=True)
 @click.option("--no-input", default=False, is_flag=True)
+@click.option("--force-update", default=False, is_flag=True)
 @configuration
-def createuser(email, password, superuser, no_password, no_input):
+def createuser(email, password, superuser, no_password, no_input, force_update):
     "Create a new user."
     if not no_input:
         if not email:
@@ -76,24 +78,31 @@ def createuser(email, password, superuser, no_password, no_input):
     if password:
         user.set_password(password)
 
-    user.save()
-
-    click.echo("User created: %s" % (email,))
-
-    # TODO(dcramer): kill this when we improve flows
-    if settings.SENTRY_SINGLE_ORGANIZATION:
-        from sentry.models import Organization, OrganizationMember, OrganizationMemberTeam, Team
-
-        org = Organization.get_default()
-        if superuser:
-            role = roles.get_top_dog().id
+    if User.objects.filter(username=email).exists():
+        if force_update:
+            user.save(force_update=force_update)
+            click.echo("User updated: %s" % (email,))
         else:
-            role = org.default_role
-        member = OrganizationMember.objects.create(organization=org, user=user, role=role)
-
-        # if we've only got a single team let's go ahead and give
-        # access to that team as its likely the desired outcome
-        teams = list(Team.objects.filter(organization=org)[0:2])
-        if len(teams) == 1:
-            OrganizationMemberTeam.objects.create(team=teams[0], organizationmember=member)
-        click.echo("Added to organization: %s" % (org.slug,))
+            click.echo("User: %s exists, use --force-update to force" % (email,))
+            sys.exit(3)
+    else:
+        user.save()
+        click.echo("User created: %s" % (email,))
+
+        # TODO(dcramer): kill this when we improve flows
+        if settings.SENTRY_SINGLE_ORGANIZATION:
+            from sentry.models import Organization, OrganizationMember, OrganizationMemberTeam, Team
+
+            org = Organization.get_default()
+            if superuser:
+                role = roles.get_top_dog().id
+            else:
+                role = org.default_role
+            member = OrganizationMember.objects.create(organization=org, user=user, role=role)
+
+            # if we've only got a single team let's go ahead and give
+            # access to that team as its likely the desired outcome
+            teams = list(Team.objects.filter(organization=org)[0:2])
+            if len(teams) == 1:
+                OrganizationMemberTeam.objects.create(team=teams[0], organizationmember=member)
+            click.echo("Added to organization: %s" % (org.slug,))


Test patch: