Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pulumi: Improve login flow #1617

Merged
merged 30 commits into from
Oct 26, 2023
Merged

Pulumi: Improve login flow #1617

merged 30 commits into from
Oct 26, 2023

Conversation

JimMadge
Copy link
Member

@JimMadge JimMadge commented Sep 22, 2023

✅ Checklist

  • You have given your pull request a meaningful title (e.g. Enable foobar integration rather than 515 foobar).
  • You are targeting the appropriate branch. If you're not certain which one this is, it should be develop.
  • Your branch is up-to-date with the target branch (it probably was when you started, but it may have changed since then).
  • You have marked this pull request as a draft and added '[WIP]' to the title if needed (if you're not yet ready to merge).
  • You have formatted your code using appropriate automated tools (for example ./tests/AutoFormat_Powershell.ps1 -TargetPath <path to file or directory> for Powershell).

⤴️ Summary

  • Ensure Azure CLI credential is used
  • Prompt to confirm the correct Azure CLI account is used
  • Still use a subprocess call to get Azure CLI credential information (not sure if there is a better way to do this)
  • Prompt to confirm the correct Pulumi account is used
  • Attempt to log into the Pulumi account corresponding to the DSH backend
  • Also uses subprocess calls
  • Subprocess call should be safer as they don't take arguments from the user and use absolute paths

🌂 Related issues

Closes #1511

🔬 Tests

Tested locally by triggering the new prompts

@jemrobinson jemrobinson changed the base branch from develop to python-migration September 25, 2023 08:47
@jemrobinson
Copy link
Member

This looks very nice. Is it ready to test?

@jemrobinson jemrobinson changed the title Login flow Pulumi: Login flow Sep 25, 2023
Copy link
Member

@jemrobinson jemrobinson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial thoughts

data_safe_haven/infrastructure/stack_manager.py Outdated Show resolved Hide resolved
data_safe_haven/infrastructure/stack_manager.py Outdated Show resolved Hide resolved
data_safe_haven/infrastructure/stack_manager.py Outdated Show resolved Hide resolved
data_safe_haven/infrastructure/stack_manager.py Outdated Show resolved Hide resolved
@JimMadge JimMadge marked this pull request as ready for review October 19, 2023 12:53
@JimMadge JimMadge requested a review from a team October 19, 2023 12:53
@JimMadge JimMadge changed the title Pulumi: Login flow Pulumi: Improve login flow Oct 19, 2023
@JimMadge JimMadge requested review from a team and removed request for a team October 20, 2023 13:53
Copy link
Contributor

@craddm craddm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When attempting to deploy a new SHM, I hit an (extra long) error, which indicates a failure when it runs pulumi whoami

2023-10-20 13:51:31 [    INFO] Reading project settings from '/home/deploydsh/.config/data_safe_haven/config.yaml'.                                                                                                                                                      backend_settings.py:110
2023-10-20 13:51:33 [    INFO] AzureAD tenant ID will be cb94a6f6-ef7a-42ab-bcad-4f0b887cfd3e.                                                                                                                                                                                     config.py:176
2023-10-20 13:51:33 [    INFO] Admin email address will be [email protected].                                                                                                                                                                                                 config.py:183
2023-10-20 13:51:33 [    INFO] IP addresses used by administrators will be ['193.60.220.253/32'].                                                                                                                                                                                  config.py:190
2023-10-20 13:51:33 [    INFO] Fully-qualified domain name will be green.develop.turingsafehaven.ac.uk.                                                                                                                                                                            config.py:197
2023-10-20 13:51:33 [    INFO] Timezone will be Europe/London.                                                                                                                                                                                                                     config.py:204
2023-10-20 13:51:34 [    INFO] name: [email protected] (id: 3f1a8e26-eae2-4539-952a-0a6184ec248a                                                                                                                                                                            azure_cli.py:67
tenant: 4395f4a7-e455-4f95-8a9f-1fbaef6384f9                                                                                                                                                                                                                                                    
Is this the Azure account you expect?
 [y/N]: y
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /workspaces/data-safe-haven/data_safe_haven/infrastructure/stack_manager.py:62 in confirm        │
│                                                                                                  │
│    59 │   │   # do this with a minimal dummy stack which also works with the Azure backend.      │
│    60 │   │   # A subprocess call is used here.                                                  │
│    61 │   │   try:                                                                               │
│ ❱  62 │   │   │   result = subprocess.check_output(                                              │
│    63 │   │   │   │   [self.path, "whoami", "--verbose"],                                        │
│    64 │   │   │   │   stderr=subprocess.PIPE,                                                    │
│    65 │   │   │   │   encoding="utf8",                                                           │
│                                                                                                  │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ self = <data_safe_haven.infrastructure.stack_manager.PulumiAccount object at 0xffff8df8c8b0> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                  │
│ /usr/local/lib/python3.10/subprocess.py:421 in check_output                                      │
│                                                                                                  │
│    418 │   │   │   empty = b''                                                                   │
│    419 │   │   kwargs['input'] = empty                                                           │
│    420 │                                                                                         │
│ ❱  421 │   return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,                      │
│    422 │   │   │      **kwargs).stdout                                                           │
│    423                                                                                           │
│    424                                                                                           │
│                                                                                                  │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │    kwargs = {                                                                                │ │
│ │             │   'stderr': -1,                                                                │ │
│ │             │   'encoding': 'utf8',                                                          │ │
│ │             │   'env': {                                                                     │ │
│ │             │   │   'AZURE_STORAGE_ACCOUNT': 'shmgreenbackend',                              │ │
│ │             │   │   'AZURE_STORAGE_KEY':                                                     │ │
│ │             'gkUkFf9cFNOJt4m2OkwmRZlsYYW3eNwvZW2CiCOPiJdU1gW4620a3tH7F1YPp+/LEzxEWrVnbikP+A… │ │
│ │             │   │   'AZURE_KEYVAULT_AUTH_VIA_CLI': 'true'                                    │ │
│ │             │   }                                                                            │ │
│ │             }                                                                                │ │
│ │ popenargs = (['/home/deploydsh/.pulumi/bin/pulumi', 'whoami', '--verbose'],)                 │ │
│ │   timeout = None                                                                             │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                  │
│ /usr/local/lib/python3.10/subprocess.py:526 in run                                               │
│                                                                                                  │
│    523 │   │   │   raise                                                                         │
│    524 │   │   retcode = process.poll()                                                          │
│    525 │   │   if check and retcode:                                                             │
│ ❱  526 │   │   │   raise CalledProcessError(retcode, process.args,                               │
│    527 │   │   │   │   │   │   │   │   │    output=stdout, stderr=stderr)                        │
│    528 │   return CompletedProcess(process.args, retcode, stdout, stderr)                        │
│    529                                                                                           │
│                                                                                                  │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ capture_output = False                                                                       │ │
│ │          check = True                                                                        │ │
│ │          input = None                                                                        │ │
│ │         kwargs = {                                                                           │ │
│ │                  │   'stdout': -1,                                                           │ │
│ │                  │   'stderr': -1,                                                           │ │
│ │                  │   'encoding': 'utf8',                                                     │ │
│ │                  │   'env': {                                                                │ │
│ │                  │   │   'AZURE_STORAGE_ACCOUNT': 'shmgreenbackend',                         │ │
│ │                  │   │   'AZURE_STORAGE_KEY':                                                │ │
│ │                  'gkUkFf9cFNOJt4m2OkwmRZlsYYW3eNwvZW2CiCOPiJdU1gW4620a3tH7F1YPp+/LEzxEWrVnb… │ │
│ │                  │   │   'AZURE_KEYVAULT_AUTH_VIA_CLI': 'true'                               │ │
│ │                  │   }                                                                       │ │
│ │                  }                                                                           │ │
│ │      popenargs = (['/home/deploydsh/.pulumi/bin/pulumi', 'whoami', '--verbose'],)            │ │
│ │        process = <Popen: returncode: 255 args: ['/home/deploydsh/.pulumi/bin/pulumi',        │ │
│ │                  'whoami...>                                                                 │ │
│ │        retcode = 255                                                                         │ │
│ │         stderr = 'error: PULUMI_ACCESS_TOKEN must be set for login during non-interactive    │ │
│ │                  CLI sess'+5                                                                 │ │
│ │         stdout = ''                                                                          │ │
│ │        timeout = None                                                                        │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
CalledProcessError: Command '['/home/deploydsh/.pulumi/bin/pulumi', 'whoami', '--verbose']' returned non-zero exit status 255.

During handling of the above exception, another exception occurred:

╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /workspaces/data-safe-haven/data_safe_haven/commands/deploy.py:83 in shm                         │
│                                                                                                  │
│    80 │   ] = None,                                                                              │
│    81 ) -> None:                                                                                 │
│    82 │   """Deploy a Safe Haven Management component"""                                         │
│ ❱  83 │   deploy_shm(                                                                            │
│    84 │   │   aad_tenant_id=aad_tenant_id,                                                       │
│    85 │   │   admin_email_address=admin_email_address,                                           │
│    86 │   │   admin_ip_addresses=admin_ip_addresses,                                             │
│                                                                                                  │
│ ╭─────────────────────────── locals ───────────────────────────╮                                 │
│ │       aad_tenant_id = 'cb94a6f6-ef7a-42ab-bcad-4f0b887cfd3e' │                                 │
│ │ admin_email_address = '[email protected]'               │                                 │
│ │  admin_ip_addresses = ['193.60.220.253/32']                  │                                 │
│ │              domain = 'green.develop.turingsafehaven.ac.uk'  │                                 │
│ │               force = None                                   │                                 │
│ │            timezone = 'Europe/London'                        │                                 │
│ ╰──────────────────────────────────────────────────────────────╯                                 │
│                                                                                                  │
│ /workspaces/data-safe-haven/data_safe_haven/commands/deploy_shm.py:43 in deploy_shm              │
│                                                                                                  │
│   40 │   │   verification_record = graph_api.add_custom_domain(config.shm.fqdn)                  │
│   41 │   │                                                                                       │
│   42 │   │   # Initialise Pulumi stack                                                           │
│ ❱ 43 │   │   PulumiAccount(config).handle_login()                                                │
│   44 │   │   stack = SHMStackManager(config)                                                     │
│   45 │   │   # Set Azure options                                                                 │
│   46 │   │   stack.add_option("azure-native:location", config.azure.location, replace=False)     │
│                                                                                                  │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │       aad_tenant_id = 'cb94a6f6-ef7a-42ab-bcad-4f0b887cfd3e'                                 │ │
│ │ admin_email_address = '[email protected]'                                               │ │
│ │  admin_ip_addresses = ['193.60.220.253/32']                                                  │ │
│ │              config = <data_safe_haven.config.config.Config object at 0xffff8df4b490>        │ │
│ │               force = None                                                                   │ │
│ │                fqdn = 'green.develop.turingsafehaven.ac.uk'                                  │ │
│ │           graph_api = <data_safe_haven.external.api.graph_api.GraphApi object at             │ │
│ │                       0xffff8df8c820>                                                        │ │
│ │            timezone = 'Europe/London'                                                        │ │
│ │ verification_record = 'MS='                                                                  │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                  │
│ /workspaces/data-safe-haven/data_safe_haven/infrastructure/stack_manager.py:102 in handle_login  │
│                                                                                                  │
│    99 │                                                                                          │
│   100 │   def handle_login(self) -> None:                                                        │
│   101 │   │   """Ensure the user is using the DSH Pulumi backend"""                              │
│ ❱ 102 │   │   if not self.confirm():                                                             │
│   103 │   │   │   msg = (                                                                        │
│   104 │   │   │   │   "Attempting to login to Pulumi account using"                              │
│   105 │   │   │   │   f" container [green]{self.cfg.pulumi.storage_container_name}[/]"           │
│                                                                                                  │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ self = <data_safe_haven.infrastructure.stack_manager.PulumiAccount object at 0xffff8df8c8b0> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                  │
│ /workspaces/data-safe-haven/data_safe_haven/infrastructure/stack_manager.py:69 in confirm        │
│                                                                                                  │
│    66 │   │   │   │   env=self.env,                                                              │
│    67 │   │   │   )                                                                              │
│    68 │   │   except subprocess.CalledProcessError as exc:                                       │
│ ❱  69 │   │   │   msg = f"Logging into Pulumi failed.\n{exc}\n{result}"                          │
│    70 │   │   │   raise DataSafeHavenPulumiError(msg) from exc                                   │
│    71 │   │                                                                                      │
│    72 │   │   self.logger.info("Confirming Pulumi account details")                              │
│                                                                                                  │
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
│ │ self = <data_safe_haven.infrastructure.stack_manager.PulumiAccount object at 0xffff8df8c8b0> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
UnboundLocalError: local variable 'result' referenced before assignment

I think what happens is that handle_login() tries to check if the user is logged in to the right pulumi account, using confirm(). confirm() then tries to run pulumi whoami. If you aren't already logged in to pulumi, running pulumi whoami makes it try to log you in. By default, it'll try to log you in to the Pulumi Cloud, which then fails - the relevant error for that is

stderr = 'error: PULUMI_ACCESS_TOKEN must be set for login during non-interactive CLI sess'+5

PULUMI_ACCESS_TOKEN shouldn't be necessary when using a non-pulumi backed, like the Azure one we're using, but at this point whoami doesn't know what backend we want to use - we haven't run login() yet.

I think the current logic'll catch people being logged in to the wrong account but doesn't correctly handle people who aren't logged in at all.

@craddm
Copy link
Contributor

craddm commented Oct 20, 2023

PULUMI_ACCESS_TOKEN shouldn't be necessary when using a non-pulumi backed, like the Azure one we're using, but at this point whoami doesn't know what backend we want to use - we haven't run login() yet.`

On this point: one way around this is to set the PULUMI_BACKEND_URL. Formatting is a bit off below, but you can set that like so:

def env(self) -> dict[str, Any]:
    """Get necessary Pulumi environment variables"""
        if not self.env_:
            azure_api = AzureApi(self.cfg.subscription_name)
            backend_storage_account_keys = azure_api.get_storage_account_keys(
                self.cfg.backend.resource_group_name,
                self.cfg.backend.storage_account_name,
            )
            self.env_ = {
                "AZURE_STORAGE_ACCOUNT": self.cfg.backend.storage_account_name,
                "AZURE_STORAGE_KEY": str(backend_storage_account_keys[0].value),
                "AZURE_KEYVAULT_AUTH_VIA_CLI": "true",
                "PULUMI_BACKEND_URL": f"azblob://{self.cfg.pulumi.storage_container_name}"
            }
        return self.env_

pulumi whoami then knows you want to use the Azure backend, not Pulumi Cloud. I haven't tested this thoroughly, so I'm not sure if it'll have any unintended consequences, but changing the above in stack_manager.py prevented the error I was having above. Thinking more broadly, this might hint at a way you could support other backends, as you'd be able to point pulumi in the right direction by setting this environment variable (but that'd require more thinking through!)

@JimMadge
Copy link
Member Author

JimMadge commented Oct 20, 2023

Interesting. Setting that environment variable makes whoami query that backend irrespective of which (if any) you are logged in to, and doesn't force logout/login.
(I.e. I am logged into Pulumi cloud before and after running dsh ... but dsh shows the Azure backend)
If that works with other Pulumi commands too (through the automation API) that would be a better way to go about this, rather than making a user change which account they are connected to.

(Maybe Pulumi login is really just about getting a local token so you don't have to authenticate/specify with every command and the env variables can override this)

@JimMadge JimMadge requested a review from craddm October 23, 2023 14:11
@JimMadge
Copy link
Member Author

@craddm Seems to work. Setting the appropriate environment variables and passing them to the Pulumi automation API workspace means you don't need to use pulumi login.

You can run dsh command using the Azure backend without changing your Pulumi CLI account.

This is nice actually as it gives us much less code to maintain/make ABI compatible.

@craddm
Copy link
Contributor

craddm commented Oct 23, 2023

Screenshot 2023-10-23 at 16 22 36

Is there a way to track whether the Az account has been confirmed already? Getting asked to confirm multiple times in the same call. Maybe we can cache it and check if it changed, or just set a variable tracking whether it's been confirmed or not.

@JimMadge JimMadge requested a review from craddm October 24, 2023 12:38
@JimMadge
Copy link
Member Author

JimMadge commented Oct 24, 2023

@craddm Good catch, I thought I had fixed that. I have make the AzureCli class a singleton so it shouldn't happen now.

Oops, didn't see that change didn't push. It has been now.

@craddm
Copy link
Contributor

craddm commented Oct 25, 2023

LGTM 👍

@JimMadge JimMadge merged commit 653e86d into python-migration Oct 26, 2023
6 checks passed
@JimMadge JimMadge deleted the login_flow branch October 26, 2023 08:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Subprocess calls etc. in PulumiStack
3 participants