cloudx-proxy 0.17.4__tar.gz → 0.17.6__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.
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.github/workflows/claude.yml +1 -1
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/CHANGELOG.md +10 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/PKG-INFO +1 -1
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy/_version.py +2 -2
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy/setup.py +32 -14
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy.egg-info/PKG-INFO +1 -1
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy.egg-info/SOURCES.txt +1 -0
- cloudx_proxy-0.17.6/tests/test_instance_tags.py +126 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/README.md +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/context/README.md +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/context/architecture.md +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/context/commit-conventions.md +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/context/development.md +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/context/project-overview.md +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/context/ssh-config.md +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/roadmap/architecture-review.md +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/roadmap/documentation-findings.md +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.ai/roadmap/improvement-roadmap.md +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.clinerules +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.envrc +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.github/dependabot.yml +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.github/workflows/codeql.yml +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.github/workflows/release.yml +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.github/workflows/test.yml +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.gitignore +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/.releaserc +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/CLAUDE.md +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/CONTRIBUTING.md +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/LICENSE +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/NOTICE +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/README.md +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy/__init__.py +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy/_op.py +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy/cli.py +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy/colors.py +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy/core.py +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy.egg-info/dependency_links.txt +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy.egg-info/entry_points.txt +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy.egg-info/requires.txt +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/cloudx_proxy.egg-info/top_level.txt +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/pyproject.toml +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/setup.cfg +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/__init__.py +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_backward_compat.py +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_cli_options.py +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_config_safety.py +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_core.py +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_non_interactive.py +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_runtime_safety.py +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_setup.py +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_ssh_config.py +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/tests/test_version.py +0 -0
- {cloudx_proxy-0.17.4 → cloudx_proxy-0.17.6}/uv.lock +0 -0
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## [0.17.6](https://github.com/easytocloud/cloudX-proxy/compare/v0.17.5...v0.17.6) (2026-09-16)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* resolve hostname/environment from plain Name tag, not just legacy prefix ([8f157a5](https://github.com/easytocloud/cloudX-proxy/commit/8f157a587a24c1cbb587162fc4e11ed8945b0715))
|
|
7
|
+
* use warning styling, not the failure symbol, for env tag conflicts ([7866ddc](https://github.com/easytocloud/cloudX-proxy/commit/7866ddcd35248d1124c66491f5d8ce7b6b489ba3))
|
|
8
|
+
|
|
9
|
+
## [0.17.5](https://github.com/easytocloud/cloudX-proxy/compare/v0.17.4...v0.17.5) (2026-09-10)
|
|
10
|
+
|
|
1
11
|
## [0.17.4](https://github.com/easytocloud/cloudX-proxy/compare/v0.17.3...v0.17.4) (2026-09-06)
|
|
2
12
|
|
|
3
13
|
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.17.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 17,
|
|
21
|
+
__version__ = version = '0.17.6'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 17, 6)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -135,8 +135,12 @@ class CloudXSetup:
|
|
|
135
135
|
"""Fetch instance tags and extract environment and hostname.
|
|
136
136
|
|
|
137
137
|
Queries EC2 for the instance tags and extracts:
|
|
138
|
-
-
|
|
139
|
-
|
|
138
|
+
- Hostname from the 'Name' tag: legacy cloudX-{env}-{hostname} (optionally
|
|
139
|
+
" | {username}") format if present, otherwise Name is used as-is (current
|
|
140
|
+
cloudX-instance.yaml sets Name to the plain hostname with no prefix).
|
|
141
|
+
- Environment from the cloudX:environment/cloudx:environment/Environment tag,
|
|
142
|
+
preferred over {env} parsed from a legacy Name tag. A mismatch between the
|
|
143
|
+
two is logged as a warning; the tag always wins.
|
|
140
144
|
|
|
141
145
|
Args:
|
|
142
146
|
instance_id: The EC2 instance ID
|
|
@@ -173,8 +177,12 @@ class CloudXSetup:
|
|
|
173
177
|
instance = response['Reservations'][0]['Instances'][0]
|
|
174
178
|
tags = {tag['Key']: tag['Value'] for tag in instance.get('Tags', [])}
|
|
175
179
|
|
|
176
|
-
#
|
|
177
|
-
#
|
|
180
|
+
# Hostname comes from the Name tag. Older cloudX-instance.yaml versions
|
|
181
|
+
# prefixed it as cloudX-{env}-{hostname} (optionally " | {username}");
|
|
182
|
+
# current versions set Name to the plain hostname with no prefix. Try the
|
|
183
|
+
# legacy pattern first for backward compatibility, and fall back to using
|
|
184
|
+
# Name as-is - either way we get a hostname straight from the tag, so this
|
|
185
|
+
# is never itself a reason to flag anything.
|
|
178
186
|
hostname = None
|
|
179
187
|
env_from_name = None
|
|
180
188
|
name_tag = tags.get('Name', '')
|
|
@@ -184,20 +192,30 @@ class CloudXSetup:
|
|
|
184
192
|
if match:
|
|
185
193
|
env_from_name = match.group(1)
|
|
186
194
|
hostname = match.group(2)
|
|
187
|
-
self.print_status(f"Found hostname from Name tag: {hostname}", True, 2)
|
|
188
195
|
else:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
#
|
|
194
|
-
#
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
hostname = ssh_hostname
|
|
197
|
+
if hostname:
|
|
198
|
+
self.print_status(f"Found hostname from Name tag: {hostname}", True, 2)
|
|
199
|
+
|
|
200
|
+
# Environment: prefer the explicit tag over one parsed from a legacy Name
|
|
201
|
+
# tag, since the tag is the current, authoritative source. Only speak up
|
|
202
|
+
# if the two genuinely disagree - that's a real inconsistency worth a
|
|
203
|
+
# human's attention, not the routine case.
|
|
204
|
+
env_from_tag = (
|
|
205
|
+
tags.get('cloudX:environment')
|
|
198
206
|
or tags.get('cloudx:environment')
|
|
199
207
|
or tags.get('Environment')
|
|
200
208
|
)
|
|
209
|
+
if env_from_name and env_from_tag and env_from_name != env_from_tag:
|
|
210
|
+
self.print_status(
|
|
211
|
+
warning(
|
|
212
|
+
f"Name tag implies environment '{env_from_name}' but the environment tag says "
|
|
213
|
+
f"'{env_from_tag}' - using '{env_from_tag}'"
|
|
214
|
+
),
|
|
215
|
+
None, 2
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
environment = env_from_tag or env_from_name
|
|
201
219
|
if environment:
|
|
202
220
|
self.print_status(f"Found environment: {environment}", True, 2)
|
|
203
221
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""get_instance_tags() hostname/environment resolution.
|
|
2
|
+
|
|
3
|
+
Older cloudX-instance.yaml versions set the Name tag to
|
|
4
|
+
cloudX-{env}-{hostname} (optionally " | {username}"); current versions set
|
|
5
|
+
Name to the plain hostname with no prefix. Both must resolve a hostname
|
|
6
|
+
without complaint - the absence of the legacy prefix is not itself a
|
|
7
|
+
problem. Environment must prefer the explicit cloudX:environment tag over
|
|
8
|
+
one parsed out of a legacy Name tag, and only speak up when the two
|
|
9
|
+
sources actively disagree.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from cloudx_proxy.setup import CloudXSetup
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FakeEC2Client:
|
|
18
|
+
def __init__(self, tags):
|
|
19
|
+
self._tags = tags
|
|
20
|
+
|
|
21
|
+
def describe_instances(self, InstanceIds):
|
|
22
|
+
return {
|
|
23
|
+
"Reservations": [
|
|
24
|
+
{"Instances": [{"Tags": [{"Key": k, "Value": v} for k, v in self._tags.items()]}]}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FakeSession:
|
|
30
|
+
def __init__(self, tags):
|
|
31
|
+
self._tags = tags
|
|
32
|
+
|
|
33
|
+
def client(self, service_name):
|
|
34
|
+
assert service_name == "ec2"
|
|
35
|
+
return FakeEC2Client(self._tags)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture
|
|
39
|
+
def setup(tmp_path):
|
|
40
|
+
return CloudXSetup(ssh_config=str(tmp_path / "config"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_tags(setup, monkeypatch, tags):
|
|
44
|
+
monkeypatch.setattr(
|
|
45
|
+
"cloudx_proxy.setup.boto3.Session", lambda *a, **k: FakeSession(tags)
|
|
46
|
+
)
|
|
47
|
+
return setup.get_instance_tags("i-0123456789abcdef0")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestPlainNameTag:
|
|
51
|
+
"""Current cloudX-instance.yaml: Name is the bare hostname, no prefix."""
|
|
52
|
+
|
|
53
|
+
def test_hostname_from_plain_name_no_warning(self, setup, monkeypatch, capsys):
|
|
54
|
+
environment, hostname = get_tags(
|
|
55
|
+
setup, monkeypatch, {"Name": "erix", "cloudX:environment": "DTA"}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
assert (environment, hostname) == ("DTA", "erix")
|
|
59
|
+
out = capsys.readouterr().out
|
|
60
|
+
assert "does not match" not in out
|
|
61
|
+
assert "✗" not in out
|
|
62
|
+
|
|
63
|
+
def test_hyphenated_plain_hostname_is_kept_whole(self, setup, monkeypatch):
|
|
64
|
+
environment, hostname = get_tags(
|
|
65
|
+
setup, monkeypatch, {"Name": "my-dev-box", "cloudX:environment": "OTA"}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
assert (environment, hostname) == ("OTA", "my-dev-box")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TestLegacyNameTag:
|
|
72
|
+
"""Older cloudX-instance.yaml: Name is cloudX-{env}-{hostname}."""
|
|
73
|
+
|
|
74
|
+
def test_env_and_hostname_parsed_when_tag_absent(self, setup, monkeypatch):
|
|
75
|
+
environment, hostname = get_tags(setup, monkeypatch, {"Name": "cloudX-OTA-web1"})
|
|
76
|
+
|
|
77
|
+
assert (environment, hostname) == ("OTA", "web1")
|
|
78
|
+
|
|
79
|
+
def test_username_suffix_is_stripped(self, setup, monkeypatch):
|
|
80
|
+
environment, hostname = get_tags(
|
|
81
|
+
setup, monkeypatch, {"Name": "cloudX-OTA-web1 | erik"}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
assert (environment, hostname) == ("OTA", "web1")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestEnvironmentPrecedence:
|
|
88
|
+
def test_tag_wins_and_conflict_is_flagged(self, setup, monkeypatch, capsys):
|
|
89
|
+
environment, hostname = get_tags(
|
|
90
|
+
setup, monkeypatch, {"Name": "cloudX-FOO-erik", "cloudX:environment": "BAR"}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
assert environment == "BAR"
|
|
94
|
+
assert hostname == "erik"
|
|
95
|
+
out = capsys.readouterr().out
|
|
96
|
+
assert "FOO" in out and "BAR" in out
|
|
97
|
+
assert "✗" not in out, "a resolved conflict is a warning, not a failure"
|
|
98
|
+
|
|
99
|
+
def test_agreement_is_not_flagged(self, setup, monkeypatch, capsys):
|
|
100
|
+
environment, hostname = get_tags(
|
|
101
|
+
setup, monkeypatch, {"Name": "cloudX-OTA-erik", "cloudX:environment": "OTA"}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
assert (environment, hostname) == ("OTA", "erik")
|
|
105
|
+
assert "✗" not in capsys.readouterr().out
|
|
106
|
+
|
|
107
|
+
def test_environment_tag_alone_is_sufficient(self, setup, monkeypatch):
|
|
108
|
+
environment, hostname = get_tags(
|
|
109
|
+
setup, monkeypatch, {"Name": "erix", "cloudx:environment": "dta"}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
assert (environment, hostname) == ("dta", "erix")
|
|
113
|
+
|
|
114
|
+
def test_generic_environment_tag_is_last_resort(self, setup, monkeypatch):
|
|
115
|
+
environment, hostname = get_tags(
|
|
116
|
+
setup, monkeypatch, {"Name": "erix", "Environment": "legacy"}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
assert (environment, hostname) == ("legacy", "erix")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class TestNothingToGoOn:
|
|
123
|
+
def test_no_name_and_no_environment_tag(self, setup, monkeypatch):
|
|
124
|
+
environment, hostname = get_tags(setup, monkeypatch, {})
|
|
125
|
+
|
|
126
|
+
assert (environment, hostname) == (None, None)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|