Skip to content

Commit a265f8e

Browse files
authored
Merge pull request #512 from 3cham/feature/gcp-config
Introduce google cloud config for logsmith
2 parents 1e45697 + dbb9fa4 commit a265f8e

13 files changed

+193
-18
lines changed

README.md

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# logsmith
2-
Logsmith is a desktop trayicon to assume your favorite aws roles.
2+
Logsmith is a desktop trayicon to:
3+
- assume your favorite aws roles, and
4+
- login & configure your gcloud config
35

46
```
57
“Who are you and how did you get in here?” -
@@ -35,10 +37,36 @@ productive:
3537
- profile: live
3638
account: '123456789123'
3739
role: developer
40+
41+
# for google cloud:
42+
# - gcp project is the profile group name
43+
# - region and type are mandatory
44+
# - profiles section is no longer needed
45+
gcp-project-dev:
46+
color: '#FF0000'
47+
team: teama
48+
region: europe-west1
49+
type: gcp
50+
51+
gcp-project-prd:
52+
color: '#388E3C'
53+
team: teama
54+
region: europe-west1
55+
type: gcp
3856
```
3957
4058
If you have account ids with leading zeros, please make sure to put them in quotes.
4159
60+
### Google Cloud login
61+
Click on the project that you want to use, this will trigger the typical login flow for user and application
62+
credentials using browser.
63+
64+
If you have multiple browser profiles, please select the correct active browser.
65+
66+
The login flow will be automatically stopped after 60 seconds of inactivity or not completion.
67+
68+
It will trigger the login flow again after 8 hours.
69+
4270
### Chain Assume
4371
You may add a "source" profile which will be used to assume a given role.
4472

app/assets/google-cloud.svg

+17
Loading

app/core/config.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ def __init__(self, name, group: dict):
6464
self.region: str = group.get('region', None)
6565
self.color: str = group.get('color', None)
6666
self.profiles: List[Profile] = []
67+
self.type: str = group.get("type", "aws") # only aws (default) & gcp as values are allowed
68+
6769
for profile in group.get('profiles', []):
6870
self.profiles.append(Profile(self, profile))
6971

@@ -74,8 +76,8 @@ def validate(self) -> (bool, str):
7476
return False, f'{self.name} has no region'
7577
if not self.color:
7678
return False, f'{self.name} has no color'
77-
if len(self.profiles) == 0:
78-
return False, f'{self.name} has no profiles'
79+
if self.type == "aws" and len(self.profiles) == 0:
80+
return False, f'aws "{self.name}" has no profiles'
7981
for profile in self.profiles:
8082
valid, error = profile.validate()
8183
if not valid:
@@ -94,15 +96,15 @@ def get_default_profile(self):
9496
return next((profile for profile in self.profiles if profile.default), None)
9597

9698
def to_dict(self):
97-
profiles = []
98-
for profile in self.profiles:
99-
profiles.append(profile.to_dict())
100-
return {
99+
result_dict = {
101100
'color': self.color,
102101
'team': self.team,
103102
'region': self.region,
104-
'profiles': profiles,
103+
'profiles': [profile.to_dict() for profile in self.profiles],
105104
}
105+
if self.type != "aws":
106+
result_dict["type"] = self.type
107+
return result_dict
106108

107109

108110
class Profile:

app/core/core.py

+43
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from app.core.config import Config, ProfileGroup
77
from app.core.result import Result
88
from app.yubico import mfa
9+
from app.gcp import login, config
910

1011
logger = logging.getLogger('logsmith')
1112

@@ -49,6 +50,48 @@ def login(self, profile_group: ProfileGroup, mfa_callback: Callable) -> Result:
4950
result.set_success()
5051
return result
5152

53+
def login_gcp(self, profile_group: ProfileGroup) -> Result:
54+
55+
result = Result()
56+
self.active_profile_group = profile_group
57+
logger.info('gcp login detected')
58+
59+
# first login
60+
user_login = login.gcloud_auth_login()
61+
if user_login is None:
62+
result.error("gcloud auth login command failed")
63+
return result
64+
65+
# second login for application default credentials
66+
adc_login = login.gcloud_auth_application_login()
67+
if adc_login is None:
68+
result.error("gcloud auth application-default login command failed")
69+
return result
70+
71+
# set project
72+
config_project = config.set_default_project(project=profile_group.name)
73+
if config_project is None:
74+
result.error("config gcp project failed")
75+
return result
76+
77+
# set region
78+
config_region = config.set_default_region(region=profile_group.region)
79+
if config_region is None:
80+
result.error("config gcp region failed")
81+
return result
82+
83+
# set quota-project
84+
config_quota_project = config.set_default_quota_project(project=profile_group.name)
85+
if config_quota_project is None:
86+
result.error("config gcp quota-project failed")
87+
return result
88+
89+
logger.info('login success')
90+
self._handle_support_files(profile_group)
91+
result.set_success()
92+
93+
return result
94+
5295
def logout(self):
5396
result = Result()
5497
logger.info(f'start logout')

app/gcp/config.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import logging
2+
3+
from app.shell import shell
4+
5+
logger = logging.getLogger('logsmith')
6+
7+
8+
def set_default_project(project: str):
9+
logger.info(f'set default project to: {project}')
10+
return shell.run(f"gcloud config set project {project}", timeout=5)
11+
12+
13+
def set_default_quota_project(project: str):
14+
logger.info(f'set default quota-project to: {project}')
15+
return shell.run(f"gcloud auth application-default set-quota-project {project}", timeout=5)
16+
17+
18+
def set_default_region(region: str):
19+
20+
logger.info(f'set default region to: {region}')
21+
return shell.run(f"gcloud config set compute/region {region}", timeout=5)
22+

app/gcp/login.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import logging
2+
3+
from app.shell import shell
4+
5+
logger = logging.getLogger('logsmith')
6+
7+
8+
def gcloud_auth_login():
9+
logger.info(f'run gcloud auth login and wait for 60 seconds to complete login flow!')
10+
return shell.run("gcloud auth login", timeout=60)
11+
12+
13+
def gcloud_auth_application_login():
14+
logger.info(f'run gcloud auth application-default login and wait for 60 seconds to complete login flow!')
15+
return shell.run("gcloud auth application-default login", timeout=60)

app/gui/assets.py

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def __init__(self):
1212
self.cloud = self._resource_path('assets/cloud.svg')
1313
self.cloud_outline = self._resource_path('assets/cloud-outline.svg')
1414
self.cloud_done = self._resource_path('assets/cloud-done.svg')
15+
self.cloud_google = self._resource_path('assets/google-cloud.svg')
1516
self.bug = self._resource_path('assets/bug.svg')
1617
self.standard = self.get_icon()
1718

@@ -20,6 +21,8 @@ def get_icon(self, style='full', color_code='#FFFFFF'):
2021
return self._color_icon(self.cloud_outline, color_code)
2122
if style == 'error':
2223
return self._color_icon(self.bug, color_code)
24+
if style == 'gcp':
25+
return self._color_icon(self.cloud_google, color_code)
2326
else:
2427
return self._color_icon(self.cloud, color_code)
2528

app/gui/gui.py

+20-4
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
from app.gui.key_rotation_dialog import RotateKeyDialog
1616
from app.gui.log_dialog import LogDialog
1717
from app.gui.trayicon import SystemTrayIcon
18-
from core.core import Core
19-
from gui.mfa_dialog import MfaDialog
20-
from gui.repeater import Repeater
18+
from app.core.core import Core
19+
from app.gui.mfa_dialog import MfaDialog
20+
from app.gui.repeater import Repeater
2121

2222
logger = logging.getLogger('logsmith')
2323

@@ -55,6 +55,21 @@ def login(self, profile_group: ProfileGroup):
5555
delay_seconds=300)
5656
self._to_login_state()
5757

58+
def login_gcp(self, profile_group: ProfileGroup):
59+
self._to_reset_state()
60+
self.tray_icon.disable_actions(True)
61+
62+
result = self.core.login_gcp(profile_group=profile_group)
63+
if not self._check_and_signal_error(result):
64+
self._to_error_state()
65+
return
66+
67+
logger.info('start repeater to remind login in 8 hours')
68+
prepare_login = partial(self.login_gcp, profile_group=profile_group)
69+
self.login_repeater.start(task=prepare_login,
70+
delay_seconds=8 * 60 * 60)
71+
self._to_login_state()
72+
5873
def logout(self):
5974
result = self.core.logout()
6075
self._check_and_signal_error(result)
@@ -109,7 +124,8 @@ def show_logs(self):
109124
self.log_dialog.show_dialog(logs_as_text)
110125

111126
def _to_login_state(self):
112-
self.tray_icon.setIcon(self.assets.get_icon(color_code=self.core.get_active_profile_color()))
127+
style = "full" if self.core.active_profile_group.type == "aws" else "gcp"
128+
self.tray_icon.setIcon(self.assets.get_icon(style=style, color_code=self.core.get_active_profile_color()))
113129
self.tray_icon.disable_actions(False)
114130
self.tray_icon.update_last_login(self.get_timestamp())
115131

app/gui/trayicon.py

+15-5
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,28 @@ def populate_context_menu(self, profile_list: List[ProfileGroup]):
3636

3737
self.actions = []
3838
for profile_group in profile_list:
39-
action = menu.addAction(profile_group.name)
40-
action.triggered.connect(partial(self.gui.login,
41-
profile_group=profile_group))
42-
action.setIcon(self.assets.get_icon(style='full', color_code=profile_group.color))
43-
self.actions.append(action)
39+
if profile_group.type == "aws":
40+
action = menu.addAction(profile_group.name)
41+
action.triggered.connect(partial(self.gui.login,
42+
profile_group=profile_group))
43+
action.setIcon(self.assets.get_icon(style='full', color_code=profile_group.color))
44+
self.actions.append(action)
4445

4546
# log out
4647
action = menu.addAction('logout')
4748
action.triggered.connect(self.gui.logout)
4849
action.setIcon(self.assets.get_icon(style='outline', color_code='#FFFFFF'))
4950
self.actions.append(action)
5051

52+
menu.addSeparator()
53+
for profile_group in profile_list:
54+
if profile_group.type == "gcp":
55+
action = menu.addAction("[GCP] " + profile_group.name)
56+
action.triggered.connect(partial(self.gui.login_gcp,
57+
profile_group=profile_group))
58+
action.setIcon(self.assets.get_icon(style='gcp', color_code=profile_group.color))
59+
self.actions.append(action)
60+
5161
menu.addSeparator()
5262
# active region
5363
self.active_region = menu.addAction('not logged in')

example.png

-25.4 KB
Loading

tests/test_core/test_config.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ def test_save_to_disk(self, mock_save_accounts_file, mock_save_config_file):
6767
'role': 'readonly'
6868
}
6969
]
70+
},
71+
'gcp-project-dev': {
72+
'color': '#FF0000',
73+
'team': 'another-team',
74+
'region': 'europe-west1',
75+
'type': 'gcp',
76+
'profiles': [], # this will be automatically added
7077
}
7178
}
7279
)]
@@ -81,14 +88,15 @@ def test_save_to_disk(self, mock_save_accounts_file, mock_save_config_file):
8188
def test_set_accounts(self):
8289
self.config.set_accounts(get_test_accounts())
8390

84-
groups = ['development', 'live']
91+
groups = ['development', 'live', 'gcp-project-dev']
8592
self.assertEqual(groups, list(self.config.profile_groups.keys()))
8693

8794
development_group = self.config.get_group('development')
8895
self.assertEqual('development', development_group.name)
8996
self.assertEqual('awesome-team', development_group.team)
9097
self.assertEqual('us-east-1', development_group.region)
9198
self.assertEqual('#388E3C', development_group.color)
99+
self.assertEqual('aws', development_group.type)
92100

93101
profile = development_group.profiles[0]
94102
self.assertEqual(development_group, profile.group)

tests/test_core/test_core.py

+5
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,11 @@ def test_get_region__region_overwrite(self):
171171
region = self.core.get_region()
172172
self.assertEqual('eu-north-1', region)
173173

174+
def test_get_region__gcp(self):
175+
self.core.active_profile_group = self.config.get_group('gcp-project-dev')
176+
region = self.core.get_region()
177+
self.assertEqual('europe-west1', region)
178+
174179
@mock.patch('app.core.core.mfa')
175180
@mock.patch('app.core.core.credentials')
176181
def test__renew_session__token_from_shell(self, mock_credentials, mock_mfa_shell):

tests/test_data/test_accounts.py

+6
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ def get_test_accounts() -> dict:
3535
'role': 'readonly',
3636
}
3737
]
38+
},
39+
'gcp-project-dev': {
40+
'color': '#FF0000',
41+
'team': 'another-team',
42+
'region': 'europe-west1',
43+
'type': 'gcp',
3844
}
3945
}
4046

0 commit comments

Comments
 (0)