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

Adding workspace/account logic for python #81

Merged
merged 3 commits into from
Mar 20, 2025
Merged

Adding workspace/account logic for python #81

merged 3 commits into from
Mar 20, 2025

Conversation

IaroslavTitov
Copy link
Contributor

@IaroslavTitov IaroslavTitov commented Mar 19, 2025

Summary

  • Fixes Add logic in python SDK to use ESC accounts #79
  • Adding credentials loading system into Python SDK
  • This is not 1:1 with ESC CLI capabilities, only the necessary parts for SDK to load credentials
  • The default configuration will now look for env vars first, then fallback onto accounts (the same behavior as CLI)

Testing

  • Adding new tests, verifying file loading and parsing logic
  • Manually tested using existing creds file setup, running tests on Pulumi prod and review stack, switching between them only using CLI

Note: I'm not a Python expert, so please correct any conventions I missed

@IaroslavTitov IaroslavTitov marked this pull request as ready for review March 19, 2025 19:04
@@ -441,6 +442,14 @@ def default_config(host=None,
access_token = os.getenv("PULUMI_ACCESS_TOKEN")
if not host:
host = os.getenv("PULUMI_BACKEND_URL")

account, backend_url = workspace.get_current_account(False)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we check whether access token or host because this is probing the file system and we should avoid it if we have what we need?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, will check if either accessToken or backend URL is missing before loading account

Copy link
Contributor

Choose a reason for hiding this comment

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

lets mark this and the workspace_models.py as linguist-generated=false in .gitattributes

@@ -4,6 +4,8 @@
- All SDK now automatically read in configuration environment variables
- Go SDK also automatically picks up configuration from CLI Pulumi accounts
[#76](https://github.com/pulumi/esc-sdk/pull/76)
- Adding CLI Pulumi accounts loading logic into Python SDK
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
- Adding CLI Pulumi accounts loading logic into Python SDK
- Adds support for reading credentials from disk to Python SDK

@@ -0,0 +1,89 @@
# Copyright 2025, Pulumi Corporation. All rights reserved.

"""Recreating Pulumi workspace and account logic for python SDK."""
Copy link
Member

Choose a reason for hiding this comment

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

I'd also recommend adding a reference to the go implementation here so we know what this is implementing and make it clear that we are porting over some existing code.

Suggested change
"""Recreating Pulumi workspace and account logic for python SDK."""
"""Pulumi workspace and account logic for python SDK."""

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, will link to ESC CLI code

# Otherwise, use the current user's home dir + .pulumi
home_dir = pathlib.Path.home()

return str(home_dir / ".pulumi")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return str(home_dir / ".pulumi")
return str(home_dir.joinpath(".pulumi"))

Comment on lines +35 to +39
def get_esc_bookkeeping_dir() -> str:
"""
Returns the path of the '.esc' folder inside Pulumi home dir.
"""
return get_pulumi_path(".esc")
Copy link
Member

Choose a reason for hiding this comment

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

What is that directory for? I don't see it on my machine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This one appears if you login via esc cli, not the pulumi one. It is a single-value file like this

{
    "name": "https://api.pulumi.com/"
}

that allows you to override current account. So if your current account in normal credentials file is moolumi.com, and you use pulumi CLI, that's what it will use. But if using esc CLI you then login to boolumi.com, it will use that as default in ESC CLI. See this method as go reference.

Copy link
Member

Choose a reason for hiding this comment

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

Okay sweet can we just add a comment to that effect?

"""
Returns the path to the esc credentials file on disk.
"""
return str(pathlib.Path(dir) / "credentials.json")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return str(pathlib.Path(dir) / "credentials.json")
return str(pathlib.Path(dir).joinpath("credentials.json"))

@@ -0,0 +1,41 @@
# Copyright 2025, Pulumi Corporation. All rights reserved.

"""Pulumi workspace models."""
Copy link
Member

Choose a reason for hiding this comment

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

Reference the file/code this is porting

"lastValidatedAt": "2026-03-17T11:19:28.392525488-06:00"
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
}
}

@@ -0,0 +1,3 @@
{
"name": "https://api.boolumi.com"
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
}
}

"lastValidatedAt": "2026-03-17T11:19:28.392525488-06:00"
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
}
}

self.assertTrue(self.config.api_key['Authorization'], "pul-fake-token-boo")

if __name__ == '__main__':
unittest.main()
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
unittest.main()
unittest.main()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for the thorough review, fixed up these issues!

Copy link
Contributor

@seanyeh seanyeh left a comment

Choose a reason for hiding this comment

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

mostly small things, looks good otherwise


return str(home_dir.joinpath(".pulumi"))

def get_pulumi_path(*elem: str) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

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

I see this method is only called with ".esc" and with no args. can the no args call be replaced with get_pulumi_home_dir()? If so, then I don't think this method has any purpose

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair point, I was porting over method by method, and in CLI there's more uses for it. But in this case, I don't expect more usage, so can eliminate this for sure.

"""
creds_file = get_path_to_creds_file(get_esc_bookkeeping_dir())
try:
with open(creds_file, 'r') as f:
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: let's be consistent with using double quotes, here and elsewhere

try:
with open(creds_file, 'r') as f:
data = json.loads(f.read())
return data['name']
Copy link
Contributor

Choose a reason for hiding this comment

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

we should add a case for if name doesn't exist. whether that's a conditional here or caught as an except ..., either is fine to me

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, no reason to print "unexpected error" when it is rather expected. I'll make it return None on KeyError

accounts: Dict[str, Account]

@classmethod
def from_json(self, dataJson: str):
Copy link
Contributor

Choose a reason for hiding this comment

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

Account.from_json reads from a dict, but this reads from a json string? maybe we can name this from_file and pass the file? (json.loads can accept a file so we can skip the extra f.read() step)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, not great that they are different. I think it would be better to keep the methods the same, both from_jsons receiving dict with values, and we can follow the same pattern of data = json.loads(f.read()) on file loading

def from_json(self, dataJson: str):
data = json.loads(dataJson)
accounts: Dict[str, Account] = {}
accounts_dict = data.get("accounts")
Copy link
Contributor

@seanyeh seanyeh Mar 20, 2025

Choose a reason for hiding this comment

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

this seems more natural to me, but what you have is fine

if "accounts" in data:
  for ...

Also, should we take into account if the creds file is in the wrong format? I assume this will throw an ambiguous attribute error, so if we can have a helpful error that might be better. (do we give a helpful error in pulumi/pulumi for the equivalent?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ty, I've no idea what's natural or not in python 😅

Hmm, creds files are not meant to be edited by hand, so I think it's fine to just throw an ambiguous "unexpected error". But I think it might be worth adding a test that makes sure that's what happens and SDK doesn't break.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would prefer a clearer error but not a huge deal

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In most scenarios it won't throw an error at all, just return None, it's only if you screw us the schema in very specific ways (like for example adding "accounts" key, but making it a string and not an object). I just think it would be overkill to try to catch all potential errors on bad deserializations

print(f"An unexpected error occured: {e}")
return None

def get_current_account(shared) -> tuple[Account, str]:
Copy link
Contributor

Choose a reason for hiding this comment

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

what is shared for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If Shared is true, we would ignore the ESC override and use the account selected in main credentials file - same thing as with the path method, not strictly necessary in this implementation, I was just keeping it as in the CLI.

Copy link
Contributor

Choose a reason for hiding this comment

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

if we're not using it, i would opt to remove it. but if you want to keep it to match the CLI, i would at least document it

@IaroslavTitov IaroslavTitov merged commit 64cf557 into main Mar 20, 2025
8 checks passed
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.

Add logic in python SDK to use ESC accounts
4 participants