xpk 0.14.3__py3-none-any.whl → 0.15.0__py3-none-any.whl
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.
- integration/gcluster_a3mega_test.py +11 -0
- integration/gcluster_a3ultra_test.py +11 -0
- integration/gcluster_a4_test.py +11 -0
- xpk/commands/cluster.py +57 -21
- xpk/commands/cluster_gcluster.py +25 -5
- xpk/commands/cluster_gcluster_test.py +11 -2
- xpk/commands/cluster_test.py +233 -12
- xpk/commands/config.py +3 -5
- xpk/commands/kind.py +1 -1
- xpk/commands/storage.py +8 -10
- xpk/commands/workload.py +28 -11
- xpk/commands/workload_test.py +3 -3
- xpk/core/blueprint/blueprint_generator.py +70 -33
- xpk/core/blueprint/blueprint_test.py +9 -0
- xpk/core/capacity.py +46 -8
- xpk/core/capacity_test.py +32 -1
- xpk/core/cluster.py +37 -57
- xpk/core/cluster_test.py +95 -0
- xpk/core/commands.py +4 -10
- xpk/core/config.py +9 -2
- xpk/core/gcloud_context.py +18 -12
- xpk/core/gcloud_context_test.py +111 -1
- xpk/core/kjob.py +6 -9
- xpk/core/kueue_manager.py +192 -32
- xpk/core/kueue_manager_test.py +132 -4
- xpk/core/nodepool.py +21 -29
- xpk/core/nodepool_test.py +17 -15
- xpk/core/scheduling.py +16 -1
- xpk/core/scheduling_test.py +85 -6
- xpk/core/system_characteristics.py +77 -19
- xpk/core/system_characteristics_test.py +80 -5
- xpk/core/telemetry.py +263 -0
- xpk/core/telemetry_test.py +211 -0
- xpk/main.py +31 -13
- xpk/parser/cluster.py +48 -9
- xpk/parser/cluster_test.py +42 -3
- xpk/parser/workload.py +12 -0
- xpk/parser/workload_test.py +4 -4
- xpk/telemetry_uploader.py +29 -0
- xpk/templates/kueue_gke_default_topology.yaml.j2 +1 -1
- xpk/templates/kueue_sub_slicing_topology.yaml.j2 +3 -8
- xpk/utils/console.py +41 -10
- xpk/utils/console_test.py +106 -0
- xpk/utils/feature_flags.py +7 -1
- xpk/utils/file.py +4 -1
- xpk/utils/topology.py +4 -0
- xpk/utils/user_agent.py +35 -0
- xpk/utils/user_agent_test.py +44 -0
- xpk/utils/user_input.py +48 -0
- xpk/utils/user_input_test.py +92 -0
- xpk/utils/validation.py +0 -11
- xpk/utils/versions.py +31 -0
- {xpk-0.14.3.dist-info → xpk-0.15.0.dist-info}/METADATA +113 -92
- {xpk-0.14.3.dist-info → xpk-0.15.0.dist-info}/RECORD +58 -48
- {xpk-0.14.3.dist-info → xpk-0.15.0.dist-info}/WHEEL +0 -0
- {xpk-0.14.3.dist-info → xpk-0.15.0.dist-info}/entry_points.txt +0 -0
- {xpk-0.14.3.dist-info → xpk-0.15.0.dist-info}/licenses/LICENSE +0 -0
- {xpk-0.14.3.dist-info → xpk-0.15.0.dist-info}/top_level.txt +0 -0
xpk/utils/console.py
CHANGED
|
@@ -16,6 +16,9 @@ limitations under the License.
|
|
|
16
16
|
|
|
17
17
|
import sys
|
|
18
18
|
from typing import NoReturn
|
|
19
|
+
from typing import Literal
|
|
20
|
+
|
|
21
|
+
from .execution_context import is_quiet
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
def xpk_print(*args, **kwargs):
|
|
@@ -25,7 +28,7 @@ def xpk_print(*args, **kwargs):
|
|
|
25
28
|
*args: user provided print args.
|
|
26
29
|
**kwargs: user provided print args.
|
|
27
30
|
"""
|
|
28
|
-
sys.stdout.write(
|
|
31
|
+
sys.stdout.write("[XPK] ")
|
|
29
32
|
print(*args, **kwargs)
|
|
30
33
|
sys.stdout.flush()
|
|
31
34
|
|
|
@@ -37,20 +40,48 @@ def xpk_exit(error_code) -> NoReturn:
|
|
|
37
40
|
error_code: If the code provided is zero, then no issues occurred.
|
|
38
41
|
"""
|
|
39
42
|
if error_code == 0:
|
|
40
|
-
xpk_print(
|
|
43
|
+
xpk_print("Exiting XPK cleanly")
|
|
41
44
|
sys.exit(0)
|
|
42
45
|
else:
|
|
43
|
-
xpk_print(f
|
|
46
|
+
xpk_print(f"XPK failed, error code {error_code}")
|
|
44
47
|
sys.exit(error_code)
|
|
45
48
|
|
|
46
49
|
|
|
47
|
-
def
|
|
48
|
-
|
|
50
|
+
def ask_for_user_consent(
|
|
51
|
+
question: str, default_option: Literal["Y", "N"] = "N"
|
|
52
|
+
) -> bool:
|
|
53
|
+
"""Prompts user with the given question, asking for a yes/no answer and returns a relevant boolean.
|
|
54
|
+
Important: immediatelly returns `True` in quiet mode!
|
|
55
|
+
|
|
56
|
+
Example prompt for `question='Continue?'`: `[XPK] Continue? (y/N): `.
|
|
49
57
|
|
|
50
58
|
Args:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
question: The question to ask the user.
|
|
60
|
+
default_option: Option to use when user response is empty.
|
|
61
|
+
"""
|
|
62
|
+
if is_quiet():
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
options = "y/N" if default_option == "N" else "Y/n"
|
|
66
|
+
prompt = f"[XPK] {question} ({options}): "
|
|
67
|
+
|
|
68
|
+
while True:
|
|
69
|
+
user_input = input(prompt) or default_option
|
|
70
|
+
if user_input.lower() in ["yes", "y"]:
|
|
71
|
+
return True
|
|
72
|
+
elif user_input.lower() in ["no", "n"]:
|
|
73
|
+
return False
|
|
74
|
+
else:
|
|
75
|
+
xpk_print("Invalid input. Please enter: yes/no/y/n.")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def exit_code_to_int(exit_code: str | int | None) -> int:
|
|
79
|
+
"""
|
|
80
|
+
Converts sys._ExitCode to an int value that is used to exit the program.
|
|
81
|
+
See more: https://github.com/python/typeshed/issues/8513#issue-1333671093
|
|
54
82
|
"""
|
|
55
|
-
|
|
56
|
-
|
|
83
|
+
if isinstance(exit_code, int):
|
|
84
|
+
return int(exit_code)
|
|
85
|
+
if exit_code is None:
|
|
86
|
+
return 0
|
|
87
|
+
return 1
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2025 Google LLC
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from unittest.mock import MagicMock, patch
|
|
18
|
+
import pytest
|
|
19
|
+
from pytest_mock import MockerFixture
|
|
20
|
+
|
|
21
|
+
from .console import exit_code_to_int, ask_for_user_consent
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture(autouse=True)
|
|
25
|
+
def mock_is_quiet(mocker: MockerFixture):
|
|
26
|
+
return mocker.patch("xpk.utils.console.is_quiet", return_value=False)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.mark.parametrize(
|
|
30
|
+
"user_input,expected",
|
|
31
|
+
[
|
|
32
|
+
("yes", True),
|
|
33
|
+
("y", True),
|
|
34
|
+
("Y", True),
|
|
35
|
+
("Yes", True),
|
|
36
|
+
("YES", True),
|
|
37
|
+
("no", False),
|
|
38
|
+
("n", False),
|
|
39
|
+
("N", False),
|
|
40
|
+
("No", False),
|
|
41
|
+
("NO", False),
|
|
42
|
+
],
|
|
43
|
+
)
|
|
44
|
+
@patch("xpk.utils.console.input")
|
|
45
|
+
def test_ask_for_user_consent(mock_input: MagicMock, user_input, expected):
|
|
46
|
+
mock_input.return_value = user_input
|
|
47
|
+
|
|
48
|
+
assert ask_for_user_consent("Test question?") is expected
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def fake_input_factory(user_inputs: list[str]):
|
|
52
|
+
def fake_input(prompt: str) -> str:
|
|
53
|
+
return user_inputs.pop(0)
|
|
54
|
+
|
|
55
|
+
return fake_input
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@patch("xpk.utils.console.input", wraps=fake_input_factory(["invalid", "y"]))
|
|
59
|
+
def test_ask_for_user_consent_invalid_input(mock_input: MagicMock):
|
|
60
|
+
agreed = ask_for_user_consent("Test question?")
|
|
61
|
+
|
|
62
|
+
assert agreed is True
|
|
63
|
+
assert mock_input.call_count == 2
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@patch("xpk.utils.console.input", return_value="")
|
|
67
|
+
def test_ask_for_user_consent_default_No(mock_input: MagicMock):
|
|
68
|
+
agreed = ask_for_user_consent("Test question?", default_option="N")
|
|
69
|
+
|
|
70
|
+
assert agreed is False
|
|
71
|
+
mock_input.assert_called_once_with("[XPK] Test question? (y/N): ")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@patch("xpk.utils.console.input", return_value="")
|
|
75
|
+
def test_ask_for_user_consent_default_Yes(mock_input: MagicMock):
|
|
76
|
+
agreed = ask_for_user_consent("Test question?", default_option="Y")
|
|
77
|
+
|
|
78
|
+
assert agreed is True
|
|
79
|
+
mock_input.assert_called_once_with("[XPK] Test question? (Y/n): ")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@patch("xpk.utils.console.input")
|
|
83
|
+
def test_ask_for_user_consent_with_quiet_mode_always_agrees(
|
|
84
|
+
mock_input: MagicMock,
|
|
85
|
+
mock_is_quiet: MagicMock,
|
|
86
|
+
):
|
|
87
|
+
mock_is_quiet.return_value = True
|
|
88
|
+
|
|
89
|
+
agreed = ask_for_user_consent("Test question?", default_option="N")
|
|
90
|
+
|
|
91
|
+
assert agreed is True
|
|
92
|
+
mock_input.assert_not_called()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.mark.parametrize(
|
|
96
|
+
"exit_code,expected",
|
|
97
|
+
[
|
|
98
|
+
(0, 0),
|
|
99
|
+
(1, 1),
|
|
100
|
+
("Error", 1),
|
|
101
|
+
({"foo": "bar"}, 1),
|
|
102
|
+
(None, 0),
|
|
103
|
+
],
|
|
104
|
+
)
|
|
105
|
+
def test_exit_code_to_int_returns_correct_value(exit_code, expected):
|
|
106
|
+
assert exit_code_to_int(exit_code) == expected
|
xpk/utils/feature_flags.py
CHANGED
|
@@ -18,11 +18,17 @@ import os
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _get_boolean_flag(flag: str, default: bool) -> bool:
|
|
21
|
-
|
|
21
|
+
experiment_value = os.getenv(flag, "").lower()
|
|
22
|
+
if experiment_value in ["true", "false"]:
|
|
23
|
+
return experiment_value == "true"
|
|
24
|
+
|
|
25
|
+
xpk_tester = os.getenv("XPK_TESTER", "").lower() == "true"
|
|
26
|
+
return xpk_tester or default
|
|
22
27
|
|
|
23
28
|
|
|
24
29
|
class _FeatureFlags:
|
|
25
30
|
SUB_SLICING_ENABLED = _get_boolean_flag("SUB_SLICING_ENABLED", default=False)
|
|
31
|
+
TELEMETRY_ENABLED = _get_boolean_flag("TELEMETRY_ENABLED", default=False)
|
|
26
32
|
|
|
27
33
|
|
|
28
34
|
FeatureFlags = _FeatureFlags()
|
xpk/utils/file.py
CHANGED
|
@@ -18,6 +18,7 @@ import tempfile
|
|
|
18
18
|
import os
|
|
19
19
|
import hashlib
|
|
20
20
|
from .execution_context import is_dry_run
|
|
21
|
+
from .console import xpk_print
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def make_tmp_files(per_command_name: list[str]) -> list[str]:
|
|
@@ -51,7 +52,9 @@ def write_tmp_file(payload: str) -> str:
|
|
|
51
52
|
A file object that was written to.
|
|
52
53
|
"""
|
|
53
54
|
if is_dry_run():
|
|
54
|
-
|
|
55
|
+
name = _hash_filename(payload)
|
|
56
|
+
xpk_print(f'Temp file ({name}) content: \n{payload}')
|
|
57
|
+
return name
|
|
55
58
|
|
|
56
59
|
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
|
57
60
|
with open(file=tmp.name, mode='w', encoding='utf=8') as f:
|
xpk/utils/topology.py
CHANGED
|
@@ -44,3 +44,7 @@ def is_topology_contained(contained: str, container: str) -> bool:
|
|
|
44
44
|
contained <= container
|
|
45
45
|
for contained, container in zip(contained_parsed, container_parsed)
|
|
46
46
|
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_slice_topology_level(topology: str) -> str:
|
|
50
|
+
return f"cloud.google.com/gke-tpu-slice-{topology}-id"
|
xpk/utils/user_agent.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2025 Google LLC
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import platform
|
|
18
|
+
from ..core.config import __version__ as xpk_version
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_user_agent() -> str:
|
|
22
|
+
return f'XPK/{xpk_version} ({_get_user_agent_platform()})'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_user_agent_platform() -> str:
|
|
26
|
+
system = platform.system().lower()
|
|
27
|
+
if system == 'windows':
|
|
28
|
+
return f'Windows NT {platform.version()}'
|
|
29
|
+
elif system == 'linux':
|
|
30
|
+
return f'Linux; {platform.machine()}'
|
|
31
|
+
elif system == 'darwin':
|
|
32
|
+
version, _, arch = platform.mac_ver()
|
|
33
|
+
return f'Macintosh; {arch} Mac OS X {version}'
|
|
34
|
+
else:
|
|
35
|
+
return ''
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2025 Google LLC
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .user_agent import get_user_agent
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_get_user_agent_returns_correct_value_for_windows(mocker):
|
|
21
|
+
mocker.patch('xpk.utils.user_agent.xpk_version', 'v1.0.0')
|
|
22
|
+
mocker.patch('platform.system', return_value='Windows')
|
|
23
|
+
mocker.patch('platform.version', return_value='10.0')
|
|
24
|
+
assert get_user_agent() == 'XPK/v1.0.0 (Windows NT 10.0)'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_get_user_agent_returns_correct_value_for_linux(mocker):
|
|
28
|
+
mocker.patch('xpk.utils.user_agent.xpk_version', 'v1.0.0')
|
|
29
|
+
mocker.patch('platform.system', return_value='Linux')
|
|
30
|
+
mocker.patch('platform.machine', return_value='x86_64')
|
|
31
|
+
assert get_user_agent() == 'XPK/v1.0.0 (Linux; x86_64)'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_get_user_agent_returns_correct_value_for_darwin(mocker):
|
|
35
|
+
mocker.patch('xpk.utils.user_agent.xpk_version', 'v1.0.0')
|
|
36
|
+
mocker.patch('platform.system', return_value='Darwin')
|
|
37
|
+
mocker.patch('platform.mac_ver', return_value=('10.15', '', 'x86_64'))
|
|
38
|
+
assert get_user_agent() == 'XPK/v1.0.0 (Macintosh; x86_64 Mac OS X 10.15)'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_get_user_agent_returns_correct_value_for_unknown(mocker):
|
|
42
|
+
mocker.patch('xpk.utils.user_agent.xpk_version', 'v1.0.0')
|
|
43
|
+
mocker.patch('platform.system', return_value='Unknown')
|
|
44
|
+
assert get_user_agent() == 'XPK/v1.0.0 ()'
|
xpk/utils/user_input.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2025 Google LLC
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from typing import Literal
|
|
18
|
+
|
|
19
|
+
from .console import xpk_print
|
|
20
|
+
from .execution_context import is_quiet
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def ask_for_user_consent(
|
|
24
|
+
question: str, default_option: Literal["Y", "N"] = "N"
|
|
25
|
+
) -> bool:
|
|
26
|
+
"""Prompts user with the given question, asking for a yes/no answer and returns a relevant boolean.
|
|
27
|
+
Important: immediatelly returns `True` in quiet mode!
|
|
28
|
+
|
|
29
|
+
Example prompt for `question='Continue?'`: `[XPK] Continue? (y/N): `.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
question: The question to ask the user.
|
|
33
|
+
default_option: Option to use when user response is empty.
|
|
34
|
+
"""
|
|
35
|
+
if is_quiet():
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
options = "y/N" if default_option == "N" else "Y/n"
|
|
39
|
+
prompt = f"[XPK] {question} ({options}): "
|
|
40
|
+
|
|
41
|
+
while True:
|
|
42
|
+
user_input = input(prompt) or default_option
|
|
43
|
+
if user_input.lower() in ["yes", "y"]:
|
|
44
|
+
return True
|
|
45
|
+
elif user_input.lower() in ["no", "n"]:
|
|
46
|
+
return False
|
|
47
|
+
else:
|
|
48
|
+
xpk_print("Invalid input. Please enter: yes/no/y/n.")
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2025 Google LLC
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from unittest.mock import MagicMock, patch
|
|
18
|
+
import pytest
|
|
19
|
+
from pytest_mock import MockerFixture
|
|
20
|
+
|
|
21
|
+
from xpk.utils.user_input import ask_for_user_consent
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture(autouse=True)
|
|
25
|
+
def mock_is_quiet(mocker: MockerFixture):
|
|
26
|
+
return mocker.patch("xpk.utils.user_input.is_quiet", return_value=False)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.mark.parametrize(
|
|
30
|
+
"user_input,expected",
|
|
31
|
+
[
|
|
32
|
+
("yes", True),
|
|
33
|
+
("y", True),
|
|
34
|
+
("Y", True),
|
|
35
|
+
("Yes", True),
|
|
36
|
+
("YES", True),
|
|
37
|
+
("no", False),
|
|
38
|
+
("n", False),
|
|
39
|
+
("N", False),
|
|
40
|
+
("No", False),
|
|
41
|
+
("NO", False),
|
|
42
|
+
],
|
|
43
|
+
)
|
|
44
|
+
@patch("xpk.utils.user_input.input")
|
|
45
|
+
def test_ask_for_user_consent(mock_input: MagicMock, user_input, expected):
|
|
46
|
+
mock_input.return_value = user_input
|
|
47
|
+
|
|
48
|
+
assert ask_for_user_consent("Test question?") is expected
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def fake_input_factory(user_inputs: list[str]):
|
|
52
|
+
def fake_input(prompt: str) -> str:
|
|
53
|
+
return user_inputs.pop(0)
|
|
54
|
+
|
|
55
|
+
return fake_input
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@patch("xpk.utils.user_input.input", wraps=fake_input_factory(["invalid", "y"]))
|
|
59
|
+
def test_ask_for_user_consent_invalid_input(mock_input: MagicMock):
|
|
60
|
+
agreed = ask_for_user_consent("Test question?")
|
|
61
|
+
|
|
62
|
+
assert agreed is True
|
|
63
|
+
assert mock_input.call_count == 2
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@patch("xpk.utils.user_input.input", return_value="")
|
|
67
|
+
def test_ask_for_user_consent_default_No(mock_input: MagicMock):
|
|
68
|
+
agreed = ask_for_user_consent("Test question?", default_option="N")
|
|
69
|
+
|
|
70
|
+
assert agreed is False
|
|
71
|
+
mock_input.assert_called_once_with("[XPK] Test question? (y/N): ")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@patch("xpk.utils.user_input.input", return_value="")
|
|
75
|
+
def test_ask_for_user_consent_default_Yes(mock_input: MagicMock):
|
|
76
|
+
agreed = ask_for_user_consent("Test question?", default_option="Y")
|
|
77
|
+
|
|
78
|
+
assert agreed is True
|
|
79
|
+
mock_input.assert_called_once_with("[XPK] Test question? (Y/n): ")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@patch("xpk.utils.user_input.input")
|
|
83
|
+
def test_ask_for_user_consent_with_quiet_mode_always_agrees(
|
|
84
|
+
mock_input: MagicMock,
|
|
85
|
+
mock_is_quiet: MagicMock,
|
|
86
|
+
):
|
|
87
|
+
mock_is_quiet.return_value = True
|
|
88
|
+
|
|
89
|
+
agreed = ask_for_user_consent("Test question?", default_option="N")
|
|
90
|
+
|
|
91
|
+
assert agreed is True
|
|
92
|
+
mock_input.assert_not_called()
|
xpk/utils/validation.py
CHANGED
|
@@ -15,10 +15,7 @@ limitations under the License.
|
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
from ..core.commands import run_command_for_value
|
|
18
|
-
from ..core.config import __version__ as xpk_version
|
|
19
18
|
from .console import xpk_exit, xpk_print
|
|
20
|
-
from ..commands.config import xpk_cfg
|
|
21
|
-
from ..core.config import DEPENDENCIES_KEY
|
|
22
19
|
from enum import Enum
|
|
23
20
|
from dataclasses import dataclass
|
|
24
21
|
|
|
@@ -80,14 +77,6 @@ def should_validate_dependencies(args):
|
|
|
80
77
|
return not skip_validation and not dry_run
|
|
81
78
|
|
|
82
79
|
|
|
83
|
-
def validate_dependencies():
|
|
84
|
-
"""Validates all system dependencies if validation has not been done with current XPK version."""
|
|
85
|
-
deps_version = xpk_cfg.get(DEPENDENCIES_KEY)
|
|
86
|
-
if deps_version is None or deps_version != xpk_version:
|
|
87
|
-
validate_dependencies_list(list(SystemDependency))
|
|
88
|
-
xpk_cfg.set(DEPENDENCIES_KEY, xpk_version)
|
|
89
|
-
|
|
90
|
-
|
|
91
80
|
def validate_dependencies_list(dependencies: list[SystemDependency]):
|
|
92
81
|
"""Validates a list of system dependencies and returns none or exits with error."""
|
|
93
82
|
for dependency in dependencies:
|
xpk/utils/versions.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 Google LLC
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import enum
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ReleaseChannel(enum.Enum):
|
|
21
|
+
"""
|
|
22
|
+
Represents the GKE cluster release channels.
|
|
23
|
+
|
|
24
|
+
See: https://cloud.google.com/kubernetes-engine/docs/concepts/release-channels
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
RELEASE_CHANNEL_UNSPECIFIED = "RELEASE_CHANNEL_UNSPECIFIED"
|
|
28
|
+
RAPID = "RAPID"
|
|
29
|
+
REGULAR = "REGULAR"
|
|
30
|
+
STABLE = "STABLE"
|
|
31
|
+
EXTENDED = "EXTENDED"
|