Skip to content

Commit

Permalink
add atomic write and reach 100% cov test
Browse files Browse the repository at this point in the history
  • Loading branch information
MacHu-GWU committed Apr 7, 2023
1 parent 1c124cf commit 3f2a3f2
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 12 deletions.
6 changes: 5 additions & 1 deletion README.rst
Expand Up @@ -62,6 +62,8 @@ Make sure you have done::

Set AWS Profile as Default
------------------------------------------------------------------------------
It is very common that you wants to set a profile as the default when using a tools that doesn't support explicit ``--profile ...`` argument. ``awscli_mate`` provides a command to do this for you. It will update your ``.aws/config`` and ``.aws/credentials`` file and set the ``default`` profile to the one you specified.

Example:

.. code-block:: python
Expand All @@ -71,13 +73,15 @@ Example:
One Click MFA auth
------------------------------------------------------------------------------
Based on this `AWS re:Post How do I use an MFA token to authenticate access to my AWS resources through the AWS CLI? <https://repost.aws/knowledge-center/authenticate-mfa-cli>`_, you have to run ``aws sts get-session-token ...`` command to get some token, and manually copy and paste them to either environment variable or ``.aws/credentials`` file. This is a bit tedious. ``awscli_mate`` provides a one-click command to do this for you. Basically, it will use a base profile to get the token, let's say it is ``your_profile``, and automatically create / update a new profile called ``your_profile_mfa`` in your ``.aws/config`` and ``.aws/credentials`` file. So you can keep using the ``your_profile_mfa`` in your application.

Example:

.. code-block:: python
awscli_mate mfa_auth --profile=your_profile --mfa_code=123456 --hours=12 --overwrite_default=True
Note that this command also automatically set the MFA profile as default profile.
Note that this command also automatically set the MFA profile as default profile. If you don't want to set the ``your_profile_mfa`` as default profile automatically, you can just remove the ``--overwrite_default`` part.


.. _install:
Expand Down
41 changes: 31 additions & 10 deletions awscli_mate/awscli.py
Expand Up @@ -9,6 +9,7 @@
from pathlib import Path
import configparser

from atomicwrites import atomic_write
from commentedconfigparser import CommentedConfigParser

from .paths import path_config, path_credentials
Expand Down Expand Up @@ -94,10 +95,14 @@ def copy_section_data(
config: CommentedConfigParser,
from_section_name: str,
to_section_name: str,
create_if_not_exist: bool = False,
):
"""
Copy section data from one profile to another.
"""
if create_if_not_exist:
if to_section_name not in config:
config[to_section_name] = {}
for k, v in list(config[from_section_name].items()):
config[to_section_name][k] = v

Expand All @@ -106,11 +111,18 @@ def replace_section_data(
config: CommentedConfigParser,
from_section_name: str,
to_section_name: str,
create_if_not_exist: bool = False,
) -> bool:
"""
Replace section data, return a boolean flag to indicate that whether
there is any data change.
"""
if create_if_not_exist:
if to_section_name not in config:
config[to_section_name] = {
"this_is_a_dummy_key": "this_is_a_dummy_value"
}

if dict(config[from_section_name]) == dict(config[to_section_name]):
return False

Expand Down Expand Up @@ -201,18 +213,27 @@ def mfa_auth(
# update config / credentials data in memory
new_profile = "{}_mfa".format(profile)

# update config data
# set initial value if section not exists
if f"profile {new_profile}" not in config:
config[f"profile {new_profile}"] = {}
if new_profile not in credentials:
credentials[new_profile] = {}
flag_is_config_changed = True
self.copy_section_data(
config,
from_section_name=f"profile {profile}",
to_section_name=f"profile {new_profile}",
)
else:
flag_is_config_changed = False

flag_is_config_changed = self.replace_section_data(
config,
from_section_name=f"profile {profile}",
to_section_name=f"profile {new_profile}",
)
# because mfa_auth is for the credentials
# we respect the ``..._mfa`` profile if it already exists,
# and it is different from te base profile
# we don't do ``replace_section_data`` here

# update credential data
# set initial value if section not exists
if new_profile not in credentials:
credentials[new_profile] = {}
self.clear_section_data(credentials, new_profile)
credentials[new_profile]["aws_access_key_id"] = aws_access_key_id
credentials[new_profile]["aws_secret_access_key"] = aws_secret_access_key
Expand All @@ -224,8 +245,8 @@ def mfa_auth(

# update ~/.aws/config and ~/.aws/credentials file
if flag_is_config_changed:
with self.path_config.open("w") as f:
with atomic_write(f"{self.path_config}", overwrite=True) as f:
config.write(f)

with self.path_credentials.open("w") as f:
with atomic_write(f"{self.path_credentials}", overwrite=True) as f:
credentials.write(f)
6 changes: 6 additions & 0 deletions release-history.rst
Expand Up @@ -21,12 +21,18 @@ Backlog

- add ``SectionTypeEnum``, ``ConfigKeyEnum``, ``CredentialKeyEnum`` to public API.

**Minor Improvements**

- use automic write to write config / credentials file

**Miscellaneous**

- add lots of unit test for edge cases.
- reach 100% coverage test.
- add more doc string.



0.2.1 (2023-04-05)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Features and Improvements**
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
@@ -1 +1,2 @@
commented-configparser==1.0.0
commented-configparser>=1.0.0,<2.0.0
atomicwrites>=1.0.0,<2.0.0
34 changes: 34 additions & 0 deletions tests/test_awscli.py
Expand Up @@ -28,6 +28,38 @@ def test(self, awscli_config: AWSCliConfig):
with pytest.raises(ProfileNotFoundError):
awscli_config.ensure_profile_exists(profile, config, credentials)

# ------------------------------------------------------------------------------
# copy section data
# ------------------------------------------------------------------------------
awscli_config.copy_section_data(
config,
from_section_name="profile p3",
to_section_name="profile p4",
create_if_not_exist=True,
)
assert "profile p4" in config

# ------------------------------------------------------------------------------
# replace section data
# ------------------------------------------------------------------------------
awscli_config.replace_section_data(
config,
from_section_name="profile p3",
to_section_name="profile p5",
create_if_not_exist=True,
)
assert "profile p5" in config

assert (
awscli_config.replace_section_data(
config,
from_section_name="profile p3",
to_section_name="profile p5",
create_if_not_exist=True,
)
is False
)

# ------------------------------------------------------------------------------
# set_profile_as_default
# ------------------------------------------------------------------------------
Expand All @@ -45,6 +77,8 @@ def test(self, awscli_config: AWSCliConfig):
assert dict(config["default"]) == dict(config[f"profile {profile}"])
assert dict(credentials["default"]) == dict(credentials[profile])

awscli_config.set_profile_as_default("default")


if __name__ == "__main__":
from awscli_mate.tests import run_cov_test
Expand Down

0 comments on commit 3f2a3f2

Please sign in to comment.