nate2-scripts 1.0.0__tar.gz → 1.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nate2_scripts-1.0.3/.github/workflows/publish.yml +64 -0
- nate2_scripts-1.0.3/.gitignore +3 -0
- {nate2_scripts-1.0.0 → nate2_scripts-1.0.3}/PKG-INFO +2 -2
- {nate2_scripts-1.0.0 → nate2_scripts-1.0.3}/README.md +18 -11
- {nate2_scripts-1.0.0 → nate2_scripts-1.0.3}/pyproject.toml +4 -1
- nate2_scripts-1.0.0/src/nate2_scripts/bing_wrapper.py → nate2_scripts-1.0.3/src/nate2_scripts/bing_wrapper/__init__.py +41 -0
- nate2_scripts-1.0.3/src/nate2_scripts/bing_wrapper/autocomplete.sh +30 -0
- {nate2_scripts-1.0.0 → nate2_scripts-1.0.3}/src/nate2_scripts/console/__init__.py +51 -14
- {nate2_scripts-1.0.0 → nate2_scripts-1.0.3}/src/nate2_scripts/console/destitationResolver.py +17 -5
- nate2_scripts-1.0.3/src/nate2_scripts/console/nameResolver.py +106 -0
- {nate2_scripts-1.0.0 → nate2_scripts-1.0.3}/src/nate2_scripts/console/resourceIdResolver.py +10 -8
- {nate2_scripts-1.0.0 → nate2_scripts-1.0.3}/src/nate2_scripts/console/sessionCache.py +5 -1
- nate2_scripts-1.0.0/.gitignore +0 -1
- nate2_scripts-1.0.0/testing.txt +0 -9
- {nate2_scripts-1.0.0 → nate2_scripts-1.0.3}/CONSOLE-NOTES.md +0 -0
- {nate2_scripts-1.0.0 → nate2_scripts-1.0.3}/LICENSE +0 -0
- {nate2_scripts-1.0.0 → nate2_scripts-1.0.3}/src/nate2_scripts/__init__.py +0 -0
- {nate2_scripts-1.0.0 → nate2_scripts-1.0.3}/src/nate2_scripts/console/arnResolver.py +0 -0
- {nate2_scripts-1.0.0 → nate2_scripts-1.0.3}/src/nate2_scripts/console/hashGrabber.py +0 -0
- {nate2_scripts-1.0.0 → nate2_scripts-1.0.3}/src/nate2_scripts/console/regionCache.py +0 -0
- {nate2_scripts-1.0.0 → nate2_scripts-1.0.3}/src/nate2_scripts/test.py +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Trigger this workflow on every push to the main branch.
|
|
4
|
+
# To prevent unauthorized publishes, protect main with branch protection rules
|
|
5
|
+
# (require PRs + approvals) and add required reviewers to the "pypi" environment.
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: Build distribution
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
# GitHub Actions run on fresh virtual machines that have no knowledge of your code.
|
|
18
|
+
# This step clones your repository into the runner so subsequent steps can access your files.
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.x"
|
|
25
|
+
|
|
26
|
+
- name: Install Hatch
|
|
27
|
+
run: pip install hatch
|
|
28
|
+
|
|
29
|
+
# Produces a source distribution (sdist) and a wheel in the dist/ directory.
|
|
30
|
+
# The version published is whatever is set in pyproject.toml — bump it before
|
|
31
|
+
# merging to main, otherwise PyPI will reject a duplicate version.
|
|
32
|
+
- name: Build package
|
|
33
|
+
run: hatch build
|
|
34
|
+
|
|
35
|
+
# Pass the built artifacts to the publish job via GitHub's artifact storage.
|
|
36
|
+
- name: Upload distribution artifacts
|
|
37
|
+
uses: actions/upload-artifact@v4
|
|
38
|
+
with:
|
|
39
|
+
name: dist
|
|
40
|
+
path: dist/
|
|
41
|
+
|
|
42
|
+
publish:
|
|
43
|
+
name: Publish to PyPI
|
|
44
|
+
needs: build # only runs after the build job succeeds
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
# The "pypi" environment can be configured in GitHub (Settings → Environments)
|
|
47
|
+
# to require manual approval before this job runs, adding an extra gate on publishing.
|
|
48
|
+
environment:
|
|
49
|
+
name: pypi
|
|
50
|
+
url: https://pypi.org/p/nate2-scripts
|
|
51
|
+
permissions:
|
|
52
|
+
id-token: write # required for Trusted Publishing (OIDC) — no API token needed
|
|
53
|
+
|
|
54
|
+
steps:
|
|
55
|
+
- name: Download distribution artifacts
|
|
56
|
+
uses: actions/download-artifact@v4
|
|
57
|
+
with:
|
|
58
|
+
name: dist
|
|
59
|
+
path: dist/
|
|
60
|
+
|
|
61
|
+
# Uses PyPA's official action with Trusted Publishing (OIDC).
|
|
62
|
+
# PyPI must be configured with a Trusted Publisher pointing at this repo/workflow.
|
|
63
|
+
- name: Publish to PyPI
|
|
64
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
[](https://pypi.org/project/nate2-scripts/)
|
|
2
|
+
[](https://github.com/tennex-nwestfall/nate2-scripts/blob/main/LICENSE)
|
|
3
|
+
|
|
1
4
|
# Nate2's Scripts
|
|
2
5
|
|
|
3
6
|
Utility scripts for AWS development on macOS.
|
|
@@ -5,7 +8,7 @@ Utility scripts for AWS development on macOS.
|
|
|
5
8
|
To install run:
|
|
6
9
|
|
|
7
10
|
```bash
|
|
8
|
-
pip install
|
|
11
|
+
pip install nate2-scripts
|
|
9
12
|
```
|
|
10
13
|
|
|
11
14
|
## Scripts
|
|
@@ -31,7 +34,7 @@ eval "$(register-python-argcomplete console)"
|
|
|
31
34
|
**Usage:**
|
|
32
35
|
|
|
33
36
|
```bash
|
|
34
|
-
# change to an account/role
|
|
37
|
+
# change to an account/role
|
|
35
38
|
asp my-aws-role
|
|
36
39
|
|
|
37
40
|
# open main page of the role/account
|
|
@@ -42,8 +45,8 @@ console ec2
|
|
|
42
45
|
console s3
|
|
43
46
|
# open lambda page of the role/account in us-west-2
|
|
44
47
|
console lambda -r us-west-2
|
|
45
|
-
# open instance id page of the role/account in
|
|
46
|
-
# whatever region that instance is in
|
|
48
|
+
# open instance id page of the role/account in
|
|
49
|
+
# whatever region that instance is in
|
|
47
50
|
console i-0123456789abcdef
|
|
48
51
|
# open the console page for the resource the ARN points to
|
|
49
52
|
console arn:aws:ec2:us-east-1:012345678967:instance/i-0123456789abcdef
|
|
@@ -56,7 +59,7 @@ console arn:aws:ec2:us-east-1:012345678967:instance/i-0123456789abcdef
|
|
|
56
59
|
**Supported ARNs:** `ec2`, `iam` (role, user), `lambda`, `logs` (CloudWatch Logs), `rds`, `s3`, `secretsmanager`, `ecs`, `eks`, `sns`, `sqs`, `batch`, `dynamodb`, `states` (Step Functions), `elasticloadbalancing`
|
|
57
60
|
|
|
58
61
|
**All Regions Supported:**
|
|
59
|
-
Edit
|
|
62
|
+
Edit `~/.local/state/console-regions.json` to change which regions are evaluated and to set the default one
|
|
60
63
|
|
|
61
64
|
---
|
|
62
65
|
|
|
@@ -74,13 +77,17 @@ alias cdk="bing-wrapper.py --end --pattern \"(y/n)\" --sound-end /System/Library
|
|
|
74
77
|
|
|
75
78
|
# wraps nextflow to play a sound when done
|
|
76
79
|
alias nextflow="bing-wrapper.py --end -- nextflow"
|
|
80
|
+
|
|
81
|
+
# needed for autocomplete of bing-wrapper and the inner command
|
|
82
|
+
eval "$(bing-wrapper --autocomplete)"
|
|
77
83
|
```
|
|
78
84
|
|
|
79
85
|
**Options:**
|
|
80
86
|
|
|
81
|
-
| Flag
|
|
82
|
-
|
|
|
83
|
-
| `--end`
|
|
84
|
-
| `--pattern <text>`
|
|
85
|
-
| `--sound-end <path>`
|
|
86
|
-
| `--sound-pattern <path>` | Sound file to play on pattern match
|
|
87
|
+
| Flag | Description | Default |
|
|
88
|
+
| ------------------------ | --------------------------------------------------------- | ------------ |
|
|
89
|
+
| `--end` | Play a sound when the command exits | off |
|
|
90
|
+
| `--pattern <text>` | Play a sound when `<text>` appears in output (repeatable) | none |
|
|
91
|
+
| `--sound-end <path>` | Sound file to play on exit | `Glass.aiff` |
|
|
92
|
+
| `--sound-pattern <path>` | Sound file to play on pattern match | `Glass.aiff` |
|
|
93
|
+
| `--sound-while <path>` | Sound file to play on while the command runs | none |
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nate2-scripts"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.3"
|
|
8
8
|
description = "Utility scripts for AWS development on macOS"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
dependencies = [
|
|
@@ -19,5 +19,8 @@ bing-wrapper = "nate2_scripts.bing_wrapper:main"
|
|
|
19
19
|
[tool.hatch.build.targets.wheel]
|
|
20
20
|
packages = ["src/nate2_scripts"]
|
|
21
21
|
|
|
22
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
23
|
+
"src/nate2_scripts/bing_wrapper/autocomplete.sh" = "nate2_scripts/bing_wrapper/autocomplete.sh"
|
|
24
|
+
|
|
22
25
|
[project.urls]
|
|
23
26
|
Homepage = "https://github.com/tennex-nwestfall/nate2-scripts"
|
|
@@ -8,6 +8,7 @@ import struct
|
|
|
8
8
|
import subprocess
|
|
9
9
|
import sys
|
|
10
10
|
import termios
|
|
11
|
+
import threading
|
|
11
12
|
import tty
|
|
12
13
|
import argparse
|
|
13
14
|
|
|
@@ -55,6 +56,26 @@ def run(args: argparse.Namespace) -> None:
|
|
|
55
56
|
# ---- parent ----
|
|
56
57
|
os.close(slave_fd)
|
|
57
58
|
|
|
59
|
+
# Loop sound-while for the lifetime of the child process.
|
|
60
|
+
_stop_sound_while = threading.Event()
|
|
61
|
+
_sound_while_thread = None
|
|
62
|
+
|
|
63
|
+
def _play_sound_while_loop() -> None:
|
|
64
|
+
while not _stop_sound_while.is_set():
|
|
65
|
+
proc = subprocess.Popen(["afplay", args.sound_while])
|
|
66
|
+
while proc.poll() is None:
|
|
67
|
+
if _stop_sound_while.wait(timeout=0.05):
|
|
68
|
+
proc.terminate()
|
|
69
|
+
try:
|
|
70
|
+
proc.wait(timeout=1)
|
|
71
|
+
except subprocess.TimeoutExpired:
|
|
72
|
+
proc.kill()
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
if args.sound_while:
|
|
76
|
+
_sound_while_thread = threading.Thread(target=_play_sound_while_loop, daemon=True)
|
|
77
|
+
_sound_while_thread.start()
|
|
78
|
+
|
|
58
79
|
# Propagate terminal resizes to the child (also sends SIGWINCH to it).
|
|
59
80
|
def _sigwinch(signum: int, frame) -> None:
|
|
60
81
|
try:
|
|
@@ -119,6 +140,10 @@ def run(args: argparse.Namespace) -> None:
|
|
|
119
140
|
|
|
120
141
|
_, status = os.waitpid(pid, 0)
|
|
121
142
|
|
|
143
|
+
_stop_sound_while.set()
|
|
144
|
+
if _sound_while_thread is not None:
|
|
145
|
+
_sound_while_thread.join(timeout=2)
|
|
146
|
+
|
|
122
147
|
if args.end:
|
|
123
148
|
subprocess.Popen(["afplay", args.sound_end])
|
|
124
149
|
if os.WIFEXITED(status):
|
|
@@ -156,9 +181,25 @@ def main() -> None:
|
|
|
156
181
|
type=str,
|
|
157
182
|
default="/System/Library/Sounds/Glass.aiff",
|
|
158
183
|
)
|
|
184
|
+
parser.add_argument(
|
|
185
|
+
"--sound-while",
|
|
186
|
+
help="play sound while command is running",
|
|
187
|
+
type=str,
|
|
188
|
+
default="",
|
|
189
|
+
)
|
|
190
|
+
parser.add_argument(
|
|
191
|
+
"--autocomplete",
|
|
192
|
+
help="return autocomplete scripts",
|
|
193
|
+
action="store_true",
|
|
194
|
+
)
|
|
159
195
|
|
|
160
196
|
args = parser.parse_args()
|
|
161
197
|
|
|
198
|
+
if args.autocomplete:
|
|
199
|
+
script = os.path.join(os.path.dirname(__file__), "autocomplete.sh")
|
|
200
|
+
print(open(script).read())
|
|
201
|
+
sys.exit(0)
|
|
202
|
+
|
|
162
203
|
if not args.command:
|
|
163
204
|
parser.print_help()
|
|
164
205
|
sys.exit(1)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
_bing_wrapper() {
|
|
2
|
+
local -a bing_args
|
|
3
|
+
local -i double_dash_pos=0
|
|
4
|
+
|
|
5
|
+
# Find position of "--"
|
|
6
|
+
for (( i=1; i <= $#words; i++ )); do
|
|
7
|
+
if [[ "${words[$i]}" == "--" ]]; then
|
|
8
|
+
double_dash_pos=$i
|
|
9
|
+
break
|
|
10
|
+
fi
|
|
11
|
+
done
|
|
12
|
+
|
|
13
|
+
if (( double_dash_pos > 0 && CURRENT > double_dash_pos )); then
|
|
14
|
+
# Delegate to the wrapped command's completion
|
|
15
|
+
words=("${words[@]:$double_dash_pos}")
|
|
16
|
+
CURRENT=$(( CURRENT - double_dash_pos ))
|
|
17
|
+
_normal # let zsh look up the right completion for inner_cmd
|
|
18
|
+
else
|
|
19
|
+
# Complete bing-wrapper's own options
|
|
20
|
+
_arguments \
|
|
21
|
+
'--end[play sound at end]' \
|
|
22
|
+
'*--pattern[pattern to listen for]:pattern:' \
|
|
23
|
+
'--sound-end[sound file at end]:file:_files' \
|
|
24
|
+
'--sound-pattern[sound file for pattern]:file:_files' \
|
|
25
|
+
'--sound-while[sound file for duration of command]:file:_files' \
|
|
26
|
+
'--[inner command]:command:_normal' \
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
compdef _bing_wrapper bing-wrapper
|
|
@@ -21,21 +21,55 @@ from nate2_scripts.console.hashGrabber import HashGrabber
|
|
|
21
21
|
from nate2_scripts.console.regionCache import RegionCache
|
|
22
22
|
from nate2_scripts.console.sessionCache import SessionCache
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
def get_account_id() -> str:
|
|
26
|
-
return boto3.client("sts").get_caller_identity()["Account"]
|
|
24
|
+
sts = boto3.client("sts")
|
|
27
25
|
|
|
28
26
|
|
|
29
|
-
def
|
|
27
|
+
def get_email() -> str:
|
|
30
28
|
result = subprocess.run(
|
|
31
|
-
["
|
|
32
|
-
capture_output=True,
|
|
33
|
-
text=True,
|
|
29
|
+
["git", "config", "user.email"], capture_output=True, text=True
|
|
34
30
|
)
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
return result.stdout.strip()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_account_id() -> str:
|
|
35
|
+
return sts.get_caller_identity()["Account"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_credentials(account_id: str) -> dict:
|
|
39
|
+
# Check if already in an assumed role with the correct session name
|
|
40
|
+
identity = sts.get_caller_identity()
|
|
41
|
+
arn = identity.get("Arn", "")
|
|
42
|
+
email = get_email()
|
|
43
|
+
if f"assumed-role/" in arn and arn.endswith(f"/{email}"):
|
|
44
|
+
result = subprocess.run(
|
|
45
|
+
["aws", "configure", "export-credentials"],
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
)
|
|
49
|
+
if result.returncode == 0:
|
|
50
|
+
return json.loads(result.stdout)
|
|
51
|
+
|
|
52
|
+
profile = os.environ.get("AWS_PROFILE", "default")
|
|
53
|
+
session = boto3.Session(profile_name=profile)
|
|
54
|
+
config = session._session.get_scoped_config()
|
|
55
|
+
|
|
56
|
+
role_arn = config.get("role_arn", f"arn:aws:iam::{account_id}:role/OrganizationAccountAccessRole")
|
|
57
|
+
source_profile = config.get("source_profile")
|
|
58
|
+
|
|
59
|
+
if source_profile:
|
|
60
|
+
source_sts = boto3.Session(profile_name=source_profile).client("sts")
|
|
61
|
+
else:
|
|
62
|
+
source_sts = sts
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
response = source_sts.assume_role(
|
|
66
|
+
RoleArn=role_arn,
|
|
67
|
+
RoleSessionName=email,
|
|
68
|
+
)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
print(f"Error: Failed to assume role {role_arn}.\n{e}", file=sys.stderr)
|
|
37
71
|
sys.exit(1)
|
|
38
|
-
return
|
|
72
|
+
return response["Credentials"]
|
|
39
73
|
|
|
40
74
|
|
|
41
75
|
def get_signin_token(creds: dict) -> str:
|
|
@@ -115,15 +149,15 @@ def parse_args(region_cache: RegionCache) -> argparse.Namespace:
|
|
|
115
149
|
|
|
116
150
|
|
|
117
151
|
def main() -> None:
|
|
118
|
-
profile_name = get_profile()
|
|
119
152
|
region_cache = RegionCache()
|
|
153
|
+
args = parse_args(region_cache)
|
|
154
|
+
|
|
155
|
+
profile_name = get_profile()
|
|
120
156
|
session_cache = SessionCache()
|
|
121
157
|
|
|
122
158
|
print("Loading regions from cache:", region_cache.get_regions())
|
|
123
159
|
print("Loading default region from cache:", region_cache.get_default_region())
|
|
124
160
|
|
|
125
|
-
args = parse_args(region_cache)
|
|
126
|
-
|
|
127
161
|
if args.force:
|
|
128
162
|
print("Force passed. Reseting session cache.")
|
|
129
163
|
session_cache.reset()
|
|
@@ -131,8 +165,8 @@ def main() -> None:
|
|
|
131
165
|
if args.region:
|
|
132
166
|
print("Default region is now:", args.region)
|
|
133
167
|
|
|
134
|
-
creds = get_credentials()
|
|
135
168
|
account_id = get_account_id()
|
|
169
|
+
creds = get_credentials(account_id)
|
|
136
170
|
hash = session_cache.get_session_hash(profile_name, creds)
|
|
137
171
|
|
|
138
172
|
multisession_domain_name = ""
|
|
@@ -172,3 +206,6 @@ def main() -> None:
|
|
|
172
206
|
|
|
173
207
|
if __name__ == "__main__":
|
|
174
208
|
main()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# set user to OrganizationAccountAccessRole/nwestfall@tennex.io
|
{nate2_scripts-1.0.0 → nate2_scripts-1.0.3}/src/nate2_scripts/console/destitationResolver.py
RENAMED
|
@@ -4,7 +4,8 @@ import sys
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
import boto3
|
|
6
6
|
from nate2_scripts.console.arnResolver import ArnResolver
|
|
7
|
-
from nate2_scripts.console.
|
|
7
|
+
from nate2_scripts.console.nameResolver import NameResolver
|
|
8
|
+
from nate2_scripts.console.regionCache import RegionCache
|
|
8
9
|
from botocore.exceptions import ClientError
|
|
9
10
|
import urllib.parse
|
|
10
11
|
|
|
@@ -36,6 +37,9 @@ class DestinationResolver:
|
|
|
36
37
|
if not service:
|
|
37
38
|
return f"https://{self.mdn}{default_region}.console.aws.amazon.com/"
|
|
38
39
|
|
|
40
|
+
if service.lower() in service_urls:
|
|
41
|
+
return service_urls[service.lower()]
|
|
42
|
+
|
|
39
43
|
arn_dest = ArnResolver(self.account,self.mdn).try_parse_arn(service)
|
|
40
44
|
if arn_dest:
|
|
41
45
|
return arn_dest
|
|
@@ -44,10 +48,13 @@ class DestinationResolver:
|
|
|
44
48
|
if id_dest:
|
|
45
49
|
return id_dest
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
name_dest = NameResolver(self.region_cache, self.mdn).resolve_name(service)
|
|
52
|
+
if name_dest:
|
|
53
|
+
return name_dest
|
|
54
|
+
|
|
55
|
+
print(f"Error: Unknown service '{service}'.", file=sys.stderr)
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
51
58
|
|
|
52
59
|
@staticmethod
|
|
53
60
|
def _get_services(region: str, mdn: str = "") -> dict[str, str]:
|
|
@@ -98,6 +105,11 @@ class DestinationResolver:
|
|
|
98
105
|
"athena": f"https://{mdn}{region}.console.aws.amazon.com/athena/home#/query-editor",
|
|
99
106
|
"cloudtrail": f"https://{mdn}{region}.console.aws.amazon.com/cloudtrailv2/",
|
|
100
107
|
"ct": f"https://{mdn}{region}.console.aws.amazon.com/cloudtrailv2/",
|
|
108
|
+
"fsx": f"https://{mdn}{region}.console.aws.amazon.com/fsx/",
|
|
109
|
+
"subnet": f"https://{mdn}{region}.console.aws.amazon.com/vpcconsole/home#subnets:",
|
|
110
|
+
"ssm": f"https://{mdn}{region}.console.aws.amazon.com/systems-manager/",
|
|
111
|
+
"parameter": f"https://{mdn}{region}.console.aws.amazon.com/systems-manager/parameters/",
|
|
112
|
+
"ps": f"https://{mdn}{region}.console.aws.amazon.com/systems-manager/parameters/",
|
|
101
113
|
# unchecked
|
|
102
114
|
}
|
|
103
115
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import urllib.parse
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
4
|
+
|
|
5
|
+
import boto3
|
|
6
|
+
from botocore.exceptions import ClientError
|
|
7
|
+
|
|
8
|
+
from nate2_scripts.console.regionCache import RegionCache
|
|
9
|
+
|
|
10
|
+
MAX_THREADS = 10
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NameResolver:
|
|
14
|
+
region_cache: RegionCache
|
|
15
|
+
mdn: str
|
|
16
|
+
|
|
17
|
+
def __init__(self, region_cache: RegionCache, multisession_domain_name: str):
|
|
18
|
+
self.region_cache = region_cache
|
|
19
|
+
self.mdn = multisession_domain_name
|
|
20
|
+
|
|
21
|
+
def resolve_name(self, name: str) -> str | None:
|
|
22
|
+
regions = self.region_cache.get_regions()
|
|
23
|
+
|
|
24
|
+
# Priority: 0=logs, 1=ec2, 2=s3, 3=cloudformation, 4=lambda
|
|
25
|
+
tasks: list[tuple] = []
|
|
26
|
+
for region in regions:
|
|
27
|
+
tasks.append((0, self._resolve_log_group, name, region))
|
|
28
|
+
tasks.append((2, self._resolve_ec2_instance, name, region))
|
|
29
|
+
tasks.append((3, self._resolve_cloudformation_stack, name, region))
|
|
30
|
+
tasks.append((4, self._resolve_lambda_function, name, region))
|
|
31
|
+
tasks.append((1, self._resolve_s3_bucket, name))
|
|
32
|
+
|
|
33
|
+
results: dict[int, str] = {}
|
|
34
|
+
|
|
35
|
+
with ThreadPoolExecutor(max_workers=MAX_THREADS) as executor:
|
|
36
|
+
future_to_priority = {
|
|
37
|
+
executor.submit(fn, *args): priority
|
|
38
|
+
for priority, fn, *args in tasks
|
|
39
|
+
}
|
|
40
|
+
for future in as_completed(future_to_priority):
|
|
41
|
+
priority = future_to_priority[future]
|
|
42
|
+
url = future.result()
|
|
43
|
+
if url is not None and priority not in results:
|
|
44
|
+
results[priority] = url
|
|
45
|
+
|
|
46
|
+
for priority in sorted(results):
|
|
47
|
+
return results[priority]
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
def _resolve_log_group(self, name: str, region: str) -> str | None:
|
|
51
|
+
try:
|
|
52
|
+
result = boto3.client("logs", region_name=region).describe_log_groups(
|
|
53
|
+
logGroupNamePrefix=name
|
|
54
|
+
)
|
|
55
|
+
if any(lg["logGroupName"] == name for lg in result.get("logGroups", [])):
|
|
56
|
+
base = f"https://{self.mdn}{region}.console.aws.amazon.com"
|
|
57
|
+
encoded = urllib.parse.quote(name, safe="")
|
|
58
|
+
return f"{base}/cloudwatch/home?region={region}#logsV2:log-groups/log-group/{encoded}"
|
|
59
|
+
except ClientError:
|
|
60
|
+
pass
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def _resolve_ec2_instance(self, name: str, region: str) -> str | None:
|
|
64
|
+
try:
|
|
65
|
+
result = boto3.client("ec2", region_name=region).describe_instances(
|
|
66
|
+
Filters=[{"Name": "tag:Name", "Values": [name]}]
|
|
67
|
+
)
|
|
68
|
+
reservations = result.get("Reservations", [])
|
|
69
|
+
if reservations:
|
|
70
|
+
base = f"https://{self.mdn}{region}.console.aws.amazon.com"
|
|
71
|
+
instance_id = reservations[0]["Instances"][0]["InstanceId"]
|
|
72
|
+
return f"{base}/ec2/v2/home?region={region}#InstanceDetails:instanceId={instance_id}"
|
|
73
|
+
except ClientError:
|
|
74
|
+
pass
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def _resolve_cloudformation_stack(self, name: str, region: str) -> str | None:
|
|
78
|
+
try:
|
|
79
|
+
result = boto3.client("cloudformation", region_name=region).describe_stacks(
|
|
80
|
+
StackName=name
|
|
81
|
+
)
|
|
82
|
+
stacks = result.get("Stacks", [])
|
|
83
|
+
if stacks:
|
|
84
|
+
base = f"https://{self.mdn}{region}.console.aws.amazon.com"
|
|
85
|
+
stack_id = urllib.parse.quote(stacks[0]["StackId"], safe="")
|
|
86
|
+
return f"{base}/cloudformation/home?region={region}#/stacks/stackinfo?stackId={stack_id}"
|
|
87
|
+
except ClientError:
|
|
88
|
+
pass
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def _resolve_s3_bucket(self, name: str) -> str | None:
|
|
92
|
+
try:
|
|
93
|
+
boto3.client("s3").head_bucket(Bucket=name)
|
|
94
|
+
return f"https://{self.mdn}us-east-1.console.aws.amazon.com/s3/buckets/{name}"
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def _resolve_lambda_function(self, name: str, region: str) -> str | None:
|
|
100
|
+
try:
|
|
101
|
+
boto3.client("lambda", region_name=region).get_function(FunctionName=name)
|
|
102
|
+
base = f"https://{self.mdn}{region}.console.aws.amazon.com"
|
|
103
|
+
return f"{base}/lambda/home?region={region}#/functions/{name}"
|
|
104
|
+
except ClientError:
|
|
105
|
+
pass
|
|
106
|
+
return None
|
|
@@ -77,14 +77,16 @@ class ResourceIdResolver:
|
|
|
77
77
|
# keeping it here in case I figure out later
|
|
78
78
|
# if re.match(r"^fl-[0-9a-f]{8,17}$", resource_id):
|
|
79
79
|
# return "fl"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if resource_id.
|
|
83
|
-
|
|
84
|
-
if resource_id.startswith("
|
|
85
|
-
|
|
86
|
-
if resource_id.startswith("
|
|
87
|
-
|
|
80
|
+
|
|
81
|
+
# not tested
|
|
82
|
+
# if self._KMS_UUID.match(resource_id) or self._KMS_MRK.match(resource_id):
|
|
83
|
+
# return "kms"
|
|
84
|
+
# if resource_id.startswith("db/"):
|
|
85
|
+
# return "rds"
|
|
86
|
+
# if resource_id.startswith("role/"):
|
|
87
|
+
# return "role"
|
|
88
|
+
# if resource_id.startswith("usr/"):
|
|
89
|
+
# return "user"
|
|
88
90
|
return None
|
|
89
91
|
|
|
90
92
|
def _resolve_in_region(
|
|
@@ -59,9 +59,13 @@ class SessionCache:
|
|
|
59
59
|
|
|
60
60
|
# Honour the STS credential expiration if present
|
|
61
61
|
expiration = creds.get("Expiration")
|
|
62
|
+
print(expiration)
|
|
62
63
|
if expiration:
|
|
63
64
|
try:
|
|
64
|
-
|
|
65
|
+
if isinstance(expiration, datetime):
|
|
66
|
+
exp_time = expiration
|
|
67
|
+
else:
|
|
68
|
+
exp_time = datetime.fromisoformat(expiration.replace("Z", "+00:00"))
|
|
65
69
|
if datetime.now(timezone.utc) >= exp_time:
|
|
66
70
|
return None
|
|
67
71
|
except ValueError:
|
nate2_scripts-1.0.0/.gitignore
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__pycache__
|
nate2_scripts-1.0.0/testing.txt
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
ec2
|
|
2
|
-
|
|
3
|
-
i-04338f521d58918dd
|
|
4
|
-
|
|
5
|
-
arn:aws:ec2:us-east-1:905502466771:instance/i-04338f521d58918dd
|
|
6
|
-
|
|
7
|
-
arn:aws:logs:us-east-1:905502466771:log-group:/aws/parallelcluster/pcluster-lab-nate2-202607062002:*
|
|
8
|
-
|
|
9
|
-
arn:aws:logs:us-west-2:905502466771:log-group:/aws/lambda/InternalTestingStack-1-TagBucketLambda1DCC2FF9-dwonHDanL8Xf:*
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|