tft-cli 0.0.24__py3-none-any.whl → 0.0.26__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.
tft/cli/commands.py CHANGED
@@ -2,6 +2,8 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import base64
5
+ import codecs
6
+ import importlib.metadata
5
7
  import ipaddress
6
8
  import json
7
9
  import os
@@ -16,7 +18,6 @@ import xml.etree.ElementTree as ET
16
18
  from enum import Enum
17
19
  from typing import Any, Dict, List, Optional
18
20
 
19
- import pkg_resources
20
21
  import requests
21
22
  import typer
22
23
  from click.core import ParameterSource # pyre-ignore[21]
@@ -27,6 +28,7 @@ from rich.table import Table
27
28
  from tft.cli.config import settings
28
29
  from tft.cli.utils import (
29
30
  artifacts,
31
+ check_unexpected_arguments,
30
32
  cmd_output_or_exit,
31
33
  console,
32
34
  console_stderr,
@@ -39,7 +41,7 @@ from tft.cli.utils import (
39
41
  uuid_valid,
40
42
  )
41
43
 
42
- cli_version: str = pkg_resources.get_distribution("tft-cli").version
44
+ cli_version: str = importlib.metadata.version("tft-cli")
43
45
 
44
46
  TestingFarmRequestV1: Dict[str, Any] = {'test': {}, 'environments': None}
45
47
  Environment: Dict[str, Any] = {'arch': None, 'os': None, 'pool': None, 'artifacts': None, 'variables': {}}
@@ -65,6 +67,11 @@ RESERVE_TMT_DISCOVER_EXTRA_ARGS = f"--insert --how fmf --url {RESERVE_URL} --ref
65
67
 
66
68
  DEFAULT_PIPELINE_TIMEOUT = 60 * 12
67
69
 
70
+ # SSH command options for reservation connections
71
+ SSH_RESERVATION_OPTIONS = (
72
+ "ssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -oServerAliveInterval=60 -oServerAliveCountMax=3"
73
+ )
74
+
68
75
  # Won't be validating CIDR and 65535 max port range with regex here, not worth it
69
76
  SECURITY_GROUP_RULE_FORMAT = re.compile(r"(tcp|ip|icmp|udp|-1|[0-255]):(.*):(\d{1,5}-\d{1,5}|\d{1,5}|-1)")
70
77
 
@@ -82,6 +89,9 @@ class PipelineType(str, Enum):
82
89
  ARGUMENT_API_URL: str = typer.Argument(
83
90
  settings.API_URL, envvar="TESTING_FARM_API_URL", metavar='', rich_help_panel='Environment variables'
84
91
  )
92
+ OPTION_API_URL: str = typer.Option(
93
+ settings.API_URL, envvar="TESTING_FARM_API_URL", metavar='', rich_help_panel='Environment variables'
94
+ )
85
95
  ARGUMENT_API_TOKEN: str = typer.Argument(
86
96
  settings.API_TOKEN,
87
97
  envvar="TESTING_FARM_API_TOKEN",
@@ -89,6 +99,13 @@ ARGUMENT_API_TOKEN: str = typer.Argument(
89
99
  metavar='',
90
100
  rich_help_panel='Environment variables',
91
101
  )
102
+ OPTION_API_TOKEN: str = typer.Option(
103
+ settings.API_TOKEN,
104
+ envvar="TESTING_FARM_API_TOKEN",
105
+ show_default=False,
106
+ metavar='',
107
+ rich_help_panel='Environment variables',
108
+ )
92
109
  OPTION_TMT_PLAN_NAME: Optional[str] = typer.Option(
93
110
  None,
94
111
  "--plan",
@@ -254,6 +271,13 @@ OPTION_RESERVE: bool = typer.Option(
254
271
  help="Reserve machine after testing, similarly to the `reserve` command.",
255
272
  rich_help_panel=REQUEST_PANEL_RESERVE,
256
273
  )
274
+ OPTION_TMT_CONTEXT: Optional[List[str]] = typer.Option(
275
+ None,
276
+ "-c",
277
+ "--context",
278
+ metavar="key=value|@file",
279
+ help="Context variables to pass to `tmt`. The @ prefix marks a yaml file to load.",
280
+ )
257
281
 
258
282
 
259
283
  def _option_autoconnect(panel: str) -> bool:
@@ -326,12 +350,12 @@ def _sanity_reserve() -> None:
326
350
  exit_error("No SSH identities found in the SSH agent. Please run `ssh-add`.")
327
351
 
328
352
 
329
- def _handle_reservation(session, request_id: str, autoconnect: bool = False) -> None:
353
+ def _handle_reservation(session, api_url: str, request_id: str, autoconnect: bool = False) -> None:
330
354
  """
331
355
  Handle the reservation for :py:func:``request`` and :py:func:``restart`` commands.
332
356
  """
333
357
  # Get artifacts url
334
- request_url = urllib.parse.urljoin(settings.API_URL, f"/v0.1/requests/{request_id}")
358
+ request_url = urllib.parse.urljoin(api_url, f"/v0.1/requests/{request_id}")
335
359
  response = session.get(request_url)
336
360
  artifacts_url = response.json()['run']['artifacts']
337
361
 
@@ -386,7 +410,7 @@ def _handle_reservation(session, request_id: str, autoconnect: bool = False) ->
386
410
  console.print(f"🌎 ssh root@{guests[0]}")
387
411
 
388
412
  if autoconnect:
389
- os.system(f"ssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null root@{guests[0]}") # noqa: E501
413
+ os.system(f"{SSH_RESERVATION_OPTIONS} root@{guests[0]}") # noqa: E501
390
414
 
391
415
 
392
416
  def _localhost_ingress_rule(session: requests.Session) -> str:
@@ -397,7 +421,7 @@ def _localhost_ingress_rule(session: requests.Session) -> str:
397
421
 
398
422
  if get_ip.ok:
399
423
  ip = get_ip.text.strip()
400
- return f"-1:{ip}:-1"
424
+ return f"-1:{ip}:-1" # noqa: E231
401
425
 
402
426
  else:
403
427
  exit_error(f"Got {get_ip.status_code} while checking {settings.PUBLIC_IP_CHECKER_URL}")
@@ -666,21 +690,25 @@ def _print_summary_table(summary: dict, format: Optional[WatchFormat], show_deta
666
690
 
667
691
 
668
692
  def watch(
669
- api_url: str = typer.Option(settings.API_URL, help="Testing Farm API URL."),
693
+ context: typer.Context,
694
+ api_url: str = ARGUMENT_API_URL,
670
695
  id: str = typer.Option(..., help="Request ID to watch"),
671
696
  no_wait: bool = typer.Option(False, help="Skip waiting for request completion."),
672
697
  format: Optional[WatchFormat] = typer.Option(WatchFormat.text, help="Output format"),
673
698
  autoconnect: bool = typer.Option(True, hidden=True),
674
699
  reserve: bool = typer.Option(False, hidden=True),
675
700
  ):
701
+ """Watch request for completion."""
702
+
703
+ # Accept these arguments only via environment variables
704
+ check_unexpected_arguments(context, "api_url")
705
+
676
706
  def _console_print(*args, **kwargs):
677
707
  """A helper function that will skip printing to console if output format is json"""
678
708
  if format == WatchFormat.json:
679
709
  return
680
710
  console.print(*args, **kwargs)
681
711
 
682
- """Watch request for completion."""
683
-
684
712
  if not uuid_valid(id):
685
713
  exit_error("invalid request id")
686
714
 
@@ -737,10 +765,10 @@ def watch(
737
765
  if state == current_state:
738
766
  # check for reservation status and finish early if reserved
739
767
  if reserve and _is_reserved(session, request):
740
- _handle_reservation(session, request["id"], autoconnect)
768
+ _handle_reservation(session, api_url, request["id"], autoconnect)
741
769
  return
742
770
 
743
- time.sleep(1)
771
+ time.sleep(settings.WATCH_TICK)
744
772
  continue
745
773
 
746
774
  current_state = state
@@ -804,6 +832,7 @@ def version():
804
832
 
805
833
 
806
834
  def request(
835
+ context: typer.Context,
807
836
  api_url: str = ARGUMENT_API_URL,
808
837
  api_token: str = ARGUMENT_API_TOKEN,
809
838
  timeout: int = typer.Option(
@@ -839,13 +868,7 @@ def request(
839
868
  hardware: List[str] = OPTION_HARDWARE,
840
869
  kickstart: Optional[List[str]] = OPTION_KICKSTART,
841
870
  pool: Optional[str] = OPTION_POOL,
842
- cli_tmt_context: Optional[List[str]] = typer.Option(
843
- None,
844
- "-c",
845
- "--context",
846
- metavar="key=value|@file",
847
- help="Context variables to pass to `tmt`. The @ prefix marks a yaml file to load.",
848
- ),
871
+ cli_tmt_context: Optional[List[str]] = OPTION_TMT_CONTEXT,
849
872
  variables: Optional[List[str]] = OPTION_VARIABLES,
850
873
  secrets: Optional[List[str]] = OPTION_SECRETS,
851
874
  tmt_environment: Optional[List[str]] = typer.Option(
@@ -903,6 +926,10 @@ def request(
903
926
  """
904
927
  Request testing from Testing Farm.
905
928
  """
929
+
930
+ # Accept these arguments only via environment variables
931
+ check_unexpected_arguments(context, "api_url", "api_token")
932
+
906
933
  # Split comma separated arches
907
934
  arches = normalize_multistring_option(arches)
908
935
 
@@ -1038,6 +1065,8 @@ def request(
1038
1065
  environment["hardware"] = hw_constraints(hardware)
1039
1066
 
1040
1067
  if kickstart:
1068
+ # Typer escapes newlines in options, we need to unescape them
1069
+ kickstart = [codecs.decode(value, 'unicode_escape') for value in kickstart] # pyre-ignore[6]
1041
1070
  environment["kickstart"] = options_to_dict("environment kickstart", kickstart)
1042
1071
 
1043
1072
  if redhat_brew_build:
@@ -1204,7 +1233,7 @@ def request(
1204
1233
  request_id = response.json()['id']
1205
1234
 
1206
1235
  # Watch the request and handle reservation
1207
- watch(api_url, request_id, no_wait, reserve=reserve, autoconnect=autoconnect, format=WatchFormat.text)
1236
+ watch(context, api_url, request_id, no_wait, reserve=reserve, autoconnect=autoconnect, format=WatchFormat.text)
1208
1237
 
1209
1238
 
1210
1239
  def restart(
@@ -1226,6 +1255,8 @@ def restart(
1226
1255
  None,
1227
1256
  help="Force pool to provision.",
1228
1257
  ),
1258
+ cli_tmt_context: Optional[List[str]] = OPTION_TMT_CONTEXT,
1259
+ variables: Optional[List[str]] = OPTION_VARIABLES,
1229
1260
  git_url: Optional[str] = typer.Option(None, help="Force URL of the GIT repository to test."),
1230
1261
  git_ref: Optional[str] = typer.Option(None, help="Force GIT ref or branch to test."),
1231
1262
  git_merge_sha: Optional[str] = typer.Option(None, help="Force GIT ref or branch into which --ref will be merged."),
@@ -1256,6 +1287,9 @@ def restart(
1256
1287
  Just pass a request ID or an URL with a request ID to restart it.
1257
1288
  """
1258
1289
 
1290
+ # Accept these arguments only via environment variables
1291
+ check_unexpected_arguments(context, "api_url", "api_token", "internal_api_url")
1292
+
1259
1293
  # UUID pattern
1260
1294
  uuid_pattern = re.compile('[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}')
1261
1295
 
@@ -1301,9 +1335,9 @@ def restart(
1301
1335
  # Transform to a request
1302
1336
  request['environments'] = request['environments_requested']
1303
1337
 
1304
- # Remove all keys except test and environments
1338
+ # Remove all keys except test, environments and settings
1305
1339
  for key in list(request):
1306
- if key not in ['test', 'environments']:
1340
+ if key not in ['test', 'environments', 'settings']:
1307
1341
  del request[key]
1308
1342
 
1309
1343
  test = request['test']
@@ -1375,6 +1409,14 @@ def restart(
1375
1409
  for environment in request["environments"]:
1376
1410
  environment["tmt"]["extra_args"]["finish"] = tmt_finish
1377
1411
 
1412
+ if cli_tmt_context:
1413
+ for environment in request["environments"]:
1414
+ environment["tmt"]["context"] = options_to_dict("tmt context", cli_tmt_context)
1415
+
1416
+ if variables:
1417
+ for environment in request["environments"]:
1418
+ environment["variables"] = options_to_dict("environment variables", variables)
1419
+
1378
1420
  test_type = "fmf" if "fmf" in request["test"] else "sti"
1379
1421
 
1380
1422
  if tmt_plan_name:
@@ -1473,11 +1515,18 @@ def restart(
1473
1515
 
1474
1516
  # watch
1475
1517
  watch(
1476
- str(api_url), response.json()['id'], no_wait, reserve=reserve, autoconnect=autoconnect, format=WatchFormat.text
1518
+ context,
1519
+ str(api_url),
1520
+ response.json()['id'],
1521
+ no_wait,
1522
+ reserve=reserve,
1523
+ autoconnect=autoconnect,
1524
+ format=WatchFormat.text,
1477
1525
  )
1478
1526
 
1479
1527
 
1480
1528
  def run(
1529
+ context: typer.Context,
1481
1530
  arch: str = typer.Option("x86_64", "--arch", help="Hardware platform of the target machine."),
1482
1531
  compose: Optional[str] = typer.Option(
1483
1532
  None,
@@ -1489,6 +1538,10 @@ def run(
1489
1538
  secrets: Optional[List[str]] = OPTION_SECRETS,
1490
1539
  dry_run: bool = OPTION_DRY_RUN,
1491
1540
  verbose: bool = typer.Option(False, help="Be verbose."),
1541
+ # NOTE: we cannot use ARGUMENT_API_* because it would collide with command,
1542
+ # so use rather OPTION variants for this command
1543
+ api_url: str = OPTION_API_URL,
1544
+ api_token: str = OPTION_API_TOKEN,
1492
1545
  command: List[str] = typer.Argument(..., help="Command to run. Use `--` to separate COMMAND from CLI options."),
1493
1546
  ):
1494
1547
  """
@@ -1496,7 +1549,7 @@ def run(
1496
1549
  """
1497
1550
 
1498
1551
  # check for token
1499
- if not settings.API_TOKEN:
1552
+ if not api_token:
1500
1553
  exit_error("No API token found, export `TESTING_FARM_API_TOKEN` environment variable.")
1501
1554
 
1502
1555
  # create request
@@ -1530,7 +1583,7 @@ def run(
1530
1583
  request["environments"] = [environment]
1531
1584
 
1532
1585
  # submit request to Testing Farm
1533
- post_url = urllib.parse.urljoin(str(settings.API_URL), "v0.1/requests")
1586
+ post_url = urllib.parse.urljoin(api_url, "v0.1/requests")
1534
1587
 
1535
1588
  # Setting up retries
1536
1589
  session = requests.Session()
@@ -1544,7 +1597,7 @@ def run(
1544
1597
  raise typer.Exit()
1545
1598
 
1546
1599
  # handle errors
1547
- response = session.post(post_url, json=request, headers=_get_headers(settings.API_TOKEN))
1600
+ response = session.post(post_url, json=request, headers=_get_headers(api_token))
1548
1601
  if response.status_code == 401:
1549
1602
  exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
1550
1603
 
@@ -1556,7 +1609,7 @@ def run(
1556
1609
  exit_error(f"Unexpected error. Please file an issue to {settings.ISSUE_TRACKER}.")
1557
1610
 
1558
1611
  id = response.json()['id']
1559
- get_url = urllib.parse.urljoin(str(settings.API_URL), f"/v0.1/requests/{id}")
1612
+ get_url = urllib.parse.urljoin(api_url, f"/v0.1/requests/{id}")
1560
1613
 
1561
1614
  if verbose:
1562
1615
  console.print(f"🔎 api [blue]{get_url}[/blue]")
@@ -1588,7 +1641,7 @@ def run(
1588
1641
  state = request["state"]
1589
1642
 
1590
1643
  if state == current_state:
1591
- time.sleep(1)
1644
+ time.sleep(settings.WATCH_TICK)
1592
1645
  continue
1593
1646
 
1594
1647
  current_state = state
@@ -1600,7 +1653,7 @@ def run(
1600
1653
  progress.stop()
1601
1654
  exit_error("Request canceled.")
1602
1655
 
1603
- time.sleep(1)
1656
+ time.sleep(settings.WATCH_TICK)
1604
1657
 
1605
1658
  # workaround TFT-1690
1606
1659
  install_http_retries(session, status_forcelist_extend=[404], timeout=60, retry_backoff_factor=0.1)
@@ -1640,6 +1693,9 @@ def run(
1640
1693
 
1641
1694
 
1642
1695
  def reserve(
1696
+ context: typer.Context,
1697
+ api_url: str = ARGUMENT_API_URL,
1698
+ api_token: str = ARGUMENT_API_TOKEN,
1643
1699
  ssh_public_keys: List[str] = _option_ssh_public_keys(RESERVE_PANEL_GENERAL),
1644
1700
  reservation_duration: int = _option_reservation_duration(RESERVE_PANEL_GENERAL),
1645
1701
  arch: str = typer.Option(
@@ -1659,6 +1715,9 @@ def reserve(
1659
1715
  repository: List[str] = OPTION_REPOSITORY,
1660
1716
  repository_file: List[str] = OPTION_REPOSITORY_FILE,
1661
1717
  redhat_brew_build: List[str] = OPTION_REDHAT_BREW_BUILD,
1718
+ tmt_discover: Optional[List[str]] = _generate_tmt_extra_args("discover"),
1719
+ tmt_prepare: Optional[List[str]] = _generate_tmt_extra_args("prepare"),
1720
+ tmt_finish: Optional[List[str]] = _generate_tmt_extra_args("finish"),
1662
1721
  dry_run: bool = OPTION_DRY_RUN,
1663
1722
  post_install_script: Optional[str] = OPTION_POST_INSTALL_SCRIPT,
1664
1723
  print_only_request_id: bool = typer.Option(
@@ -1688,6 +1747,9 @@ def reserve(
1688
1747
 
1689
1748
  _sanity_reserve()
1690
1749
 
1750
+ # Accept these arguments only via environment variables
1751
+ check_unexpected_arguments(context, "api_url", "api_token")
1752
+
1691
1753
  # check for token
1692
1754
  if not settings.API_TOKEN:
1693
1755
  exit_error("No API token found, export `TESTING_FARM_API_TOKEN` environment variable.")
@@ -1734,6 +1796,8 @@ def reserve(
1734
1796
  environment["settings"]["provisioning"]["tags"] = options_to_dict("tags", tags)
1735
1797
 
1736
1798
  if kickstart:
1799
+ # Typer escapes newlines in options, we need to unescape them
1800
+ kickstart = [codecs.decode(value, 'unicode_escape') for value in kickstart] # pyre-ignore[6]
1737
1801
  environment["kickstart"] = options_to_dict("environment kickstart", kickstart)
1738
1802
 
1739
1803
  if redhat_brew_build:
@@ -1754,6 +1818,18 @@ def reserve(
1754
1818
  if post_install_script:
1755
1819
  environment["settings"]["provisioning"]["post_install_script"] = post_install_script
1756
1820
 
1821
+ if tmt_discover or tmt_prepare or tmt_finish:
1822
+ environment["tmt"] = {"extra_args": {}}
1823
+
1824
+ if tmt_discover:
1825
+ environment["tmt"]["extra_args"]["discover"] = tmt_discover
1826
+
1827
+ if tmt_prepare:
1828
+ environment["tmt"]["extra_args"]["prepare"] = tmt_prepare
1829
+
1830
+ if tmt_finish:
1831
+ environment["tmt"]["extra_args"]["finish"] = tmt_finish
1832
+
1757
1833
  # Setting up retries
1758
1834
  session = requests.Session()
1759
1835
  install_http_retries(session)
@@ -1803,7 +1879,7 @@ def reserve(
1803
1879
  console.print(f"⏳ Maximum reservation time is {DEFAULT_PIPELINE_TIMEOUT} minutes")
1804
1880
 
1805
1881
  # submit request to Testing Farm
1806
- post_url = urllib.parse.urljoin(str(settings.API_URL), "v0.1/requests")
1882
+ post_url = urllib.parse.urljoin(api_url, "v0.1/requests")
1807
1883
 
1808
1884
  # dry run
1809
1885
  if dry_run:
@@ -1815,7 +1891,7 @@ def reserve(
1815
1891
  raise typer.Exit()
1816
1892
 
1817
1893
  # handle errors
1818
- response = session.post(post_url, json=request, headers=_get_headers(settings.API_TOKEN))
1894
+ response = session.post(post_url, json=request, headers=_get_headers(api_token))
1819
1895
  if response.status_code == 401:
1820
1896
  exit_error(f"API token is invalid. See {settings.ONBOARDING_DOCS} for more information.")
1821
1897
 
@@ -1830,7 +1906,7 @@ def reserve(
1830
1906
  exit_error(f"Unexpected error. Please file an issue to {settings.ISSUE_TRACKER}.")
1831
1907
 
1832
1908
  id = response.json()['id']
1833
- get_url = urllib.parse.urljoin(str(settings.API_URL), f"/v0.1/requests/{id}")
1909
+ get_url = urllib.parse.urljoin(api_url, f"/v0.1/requests/{id}")
1834
1910
 
1835
1911
  if not print_only_request_id:
1836
1912
  console.print(f"🔎 [blue]{get_url}[/blue]")
@@ -1868,7 +1944,7 @@ def reserve(
1868
1944
  state = request["state"]
1869
1945
 
1870
1946
  if state == current_state:
1871
- time.sleep(1)
1947
+ time.sleep(settings.WATCH_TICK)
1872
1948
  continue
1873
1949
 
1874
1950
  current_state = state
@@ -1883,7 +1959,7 @@ def reserve(
1883
1959
  if not print_only_request_id and task_id is not None:
1884
1960
  progress.update(task_id, description=f"Reservation job is [yellow]{current_state}[/yellow]")
1885
1961
 
1886
- time.sleep(1)
1962
+ time.sleep(settings.WATCH_TICK)
1887
1963
 
1888
1964
  while current_state != "ready":
1889
1965
  if not print_only_request_id and task_id:
@@ -1950,12 +2026,12 @@ def reserve(
1950
2026
  current_state = "ready"
1951
2027
  guest = search.group(1)
1952
2028
 
1953
- time.sleep(1)
2029
+ time.sleep(settings.WATCH_TICK)
1954
2030
 
1955
2031
  console.print(f"🌎 ssh root@{guest}")
1956
2032
 
1957
2033
  if autoconnect:
1958
- os.system(f"ssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null root@{guest}") # noqa: E501
2034
+ os.system(f"{SSH_RESERVATION_OPTIONS} root@{guest}") # noqa: E501
1959
2035
 
1960
2036
 
1961
2037
  def update():
@@ -1967,6 +2043,7 @@ def update():
1967
2043
 
1968
2044
 
1969
2045
  def cancel(
2046
+ context: typer.Context,
1970
2047
  request_id: str = typer.Argument(
1971
2048
  ..., help="Testing Farm request to cancel. Specified by a request ID or a string containing it."
1972
2049
  ),
@@ -1977,6 +2054,9 @@ def cancel(
1977
2054
  Cancel a Testing Farm request.
1978
2055
  """
1979
2056
 
2057
+ # Accept these arguments only via environment variables
2058
+ check_unexpected_arguments(context, "api_url", "api_token")
2059
+
1980
2060
  # UUID pattern
1981
2061
  uuid_pattern = re.compile('[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}')
1982
2062
 
@@ -2023,6 +2103,7 @@ def cancel(
2023
2103
 
2024
2104
 
2025
2105
  def encrypt(
2106
+ context: typer.Context,
2026
2107
  message: str = typer.Argument(..., help="Message to be encrypted."),
2027
2108
  api_url: str = ARGUMENT_API_URL,
2028
2109
  api_token: str = ARGUMENT_API_TOKEN,
@@ -2040,6 +2121,9 @@ def encrypt(
2040
2121
  Create secrets for use in in-repository configuration.
2041
2122
  """
2042
2123
 
2124
+ # Accept these arguments only via environment variables
2125
+ check_unexpected_arguments(context, "api_url", "api_token")
2126
+
2043
2127
  # check for token
2044
2128
  if not api_token:
2045
2129
  exit_error("No API token found, export `TESTING_FARM_API_TOKEN` environment variable")
tft/cli/config.py CHANGED
@@ -12,9 +12,9 @@ settings = LazySettings(
12
12
  API_TOKEN=None,
13
13
  ISSUE_TRACKER="https://gitlab.com/testing-farm/general/-/issues/new",
14
14
  STATUS_PAGE="https://status.testing-farm.io",
15
- ONBOARDING_DOCS="https://docs.testing-farm.io/general/0.1/onboarding.html",
15
+ ONBOARDING_DOCS="https://docs.testing-farm.io/Testing%20Farm/0.1/onboarding.html",
16
16
  CONTAINER_SIGN="/.testing-farm-container",
17
- WATCH_TICK=3,
17
+ WATCH_TICK=30,
18
18
  DEFAULT_API_TIMEOUT=10,
19
19
  DEFAULT_API_RETRIES=7,
20
20
  # default reservation duration in minutes
tft/cli/utils.py CHANGED
@@ -13,6 +13,7 @@ from typing import Any, Dict, List, NoReturn, Optional, Union
13
13
  import requests
14
14
  import requests.adapters
15
15
  import typer
16
+ from click.core import ParameterSource # pyre-ignore[21]
16
17
  from rich.console import Console
17
18
  from ruamel.yaml import YAML
18
19
  from urllib3 import Retry
@@ -89,6 +90,20 @@ def hw_constraints(hardware: List[str]) -> Dict[Any, Any]:
89
90
  constraints[first_key].append(new_dict)
90
91
  continue
91
92
 
93
+ # Special handling for CPU flags as they are also a list
94
+ if first_key == 'cpu' and len(path_splitted) == 2 and path_splitted[1] == 'flag':
95
+ second_key = 'flag'
96
+
97
+ if first_key not in constraints:
98
+ constraints[first_key] = {}
99
+
100
+ if second_key not in constraints[first_key]:
101
+ constraints[first_key][second_key] = []
102
+
103
+ constraints[first_key][second_key].append(value)
104
+
105
+ continue
106
+
92
107
  # Walk the path, step by step, and initialize containers along the way. The last step is not
93
108
  # a name of another nested container, but actually a name in the last container.
94
109
  container: Any = constraints
@@ -244,3 +259,12 @@ def read_glob_paths(glob_paths: List[str]) -> str:
244
259
  contents.append(file.read())
245
260
 
246
261
  return ''.join(contents)
262
+
263
+
264
+ def check_unexpected_arguments(context: typer.Context, *args: str) -> Union[None, NoReturn]:
265
+ for argument in args:
266
+ if context.get_parameter_source(argument) == ParameterSource.COMMANDLINE: # pyre-ignore[16]
267
+ exit_error(
268
+ f"Unexpected argument '{context.params.get(argument)}'. "
269
+ "Please make sure you are passing the parameters correctly."
270
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tft-cli
3
- Version: 0.0.24
3
+ Version: 0.0.26
4
4
  Summary: Testing Farm CLI tool
5
5
  License: Apache-2.0
6
6
  Author: Miroslav Vadkerti
@@ -0,0 +1,10 @@
1
+ tft/cli/__init__.py,sha256=uEJkNJbqC583PBtNI30kxWdeOr3Wj6zJzIYKf0AD72I,92
2
+ tft/cli/commands.py,sha256=AijL207LsDBXU7nNqWTPDFuc2QiRgFjTN6zU9sGVCFU,82931
3
+ tft/cli/config.py,sha256=ge-uJUnjzbMlp1haRcd50PCYWwNxaEvXjH-fhrA0wW4,1262
4
+ tft/cli/tool.py,sha256=nuz57u3yE4fKgdMgNtAFM7PCTcF6PSNFBQbCYDzxBOw,897
5
+ tft/cli/utils.py,sha256=t3ZSnviGxVbBQ4u4ltwQwRgN647BvuyL1QrSlQAAT1Q,9136
6
+ tft_cli-0.0.26.dist-info/LICENSE,sha256=YpVAQfXkIyzQAdm5LZkI6L5UWqLppa6O8_tgDSdoabQ,574
7
+ tft_cli-0.0.26.dist-info/METADATA,sha256=fA7fuCqiRS-17UOI7ij1avPNLlttFKMnjEiTnyoO8XI,789
8
+ tft_cli-0.0.26.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
9
+ tft_cli-0.0.26.dist-info/entry_points.txt,sha256=xzdebHkH5Bx-YRf-XPMsIoVpvgfUqqcRQGuo8DFkiao,49
10
+ tft_cli-0.0.26.dist-info/RECORD,,