Skip to content

Commit 65586ea

Browse files
authored
Merge pull request #1 from zemin-piao/main
Add basic auth functionality
2 parents 3e3f184 + 0a7d8df commit 65586ea

3 files changed

Lines changed: 81 additions & 4 deletions

File tree

spark_history_cli/cli.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,16 @@ def __init__(self):
2828
self.client: SparkHistoryClient | None = None
2929
self.session: Session = Session()
3030
self.json_mode: bool = False
31+
self.basic_auth_username: str | None = None
32+
self.basic_auth_password: str | None = None
3133

3234
def ensure_client(self) -> SparkHistoryClient:
3335
if self.client is None:
34-
self.client = SparkHistoryClient(self.session.server_url)
36+
self.client = SparkHistoryClient(
37+
self.session.server_url,
38+
basic_auth_username=self.basic_auth_username,
39+
basic_auth_password=self.basic_auth_password,
40+
)
3541
return self.client
3642

3743
def resolve_app_id(self, app_id: str | None) -> str:
@@ -86,17 +92,39 @@ def _fetch_sql_jobs(client, app_id: str, sql_exec: dict) -> list[dict]:
8692
@click.option("--server", "-s", default="http://localhost:18080",
8793
envvar="SPARK_HISTORY_SERVER",
8894
help="Spark History Server URL (default: http://localhost:18080)")
95+
@click.option("--basic-auth-user", default=None,
96+
envvar="SPARK_HISTORY_BASIC_AUTH_USER",
97+
help="Basic Auth username for Spark History Server")
98+
@click.option("--basic-auth-password", default=None,
99+
envvar="SPARK_HISTORY_BASIC_AUTH_PASSWORD",
100+
help="Basic Auth password for Spark History Server (omit to prompt securely)")
89101
@click.option("--json", "json_mode", is_flag=True, default=False,
90102
help="Output in JSON format for machine consumption")
91103
@click.option("--app-id", "-a", default=None,
92104
help="Application ID to use (sets context for subcommands)")
93105
@click.version_option(__version__, prog_name="spark-history-cli")
94106
@click.pass_context
95-
def cli(ctx, server: str, json_mode: bool, app_id: str | None):
107+
def cli(
108+
ctx,
109+
server: str,
110+
basic_auth_user: str | None,
111+
basic_auth_password: str | None,
112+
json_mode: bool,
113+
app_id: str | None,
114+
):
96115
"""CLI for querying the Apache Spark History Server REST API."""
116+
if basic_auth_password is not None and basic_auth_user is None:
117+
raise click.UsageError(
118+
"--basic-auth-password requires --basic-auth-user."
119+
)
120+
if basic_auth_user is not None and basic_auth_password is None:
121+
basic_auth_password = click.prompt("Basic Auth password", hide_input=True)
122+
97123
state = CliState()
98124
state.session.server_url = server
99125
state.json_mode = json_mode
126+
state.basic_auth_username = basic_auth_user
127+
state.basic_auth_password = basic_auth_password
100128
if app_id:
101129
state.session.set_app(app_id)
102130
state.ensure_client()
@@ -192,7 +220,11 @@ def repl(state: CliState):
192220
elif cmd == "server":
193221
if args:
194222
state.session.server_url = args[0]
195-
state.client = SparkHistoryClient(args[0])
223+
state.client = SparkHistoryClient(
224+
args[0],
225+
basic_auth_username=state.basic_auth_username,
226+
basic_auth_password=state.basic_auth_password,
227+
)
196228
client = state.client
197229
try:
198230
ver = client.get_version()
@@ -205,6 +237,7 @@ def repl(state: CliState):
205237
elif cmd == "status":
206238
skin.status_block({
207239
"Server": state.session.server_url,
240+
"Basic Auth": "enabled" if state.basic_auth_username else "disabled",
208241
"Current App": state.session.current_app_id or "(none)",
209242
"Attempt": state.session.current_attempt_id or "(none)",
210243
}, title="Session")

spark_history_cli/core/client.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,19 @@ def __init__(self, status_code: int, message: str, url: str = ""):
2424
class SparkHistoryClient:
2525
"""Client for the Spark History Server REST API (/api/v1)."""
2626

27-
def __init__(self, server_url: str = "http://localhost:18080", timeout: int = 30):
27+
def __init__(
28+
self,
29+
server_url: str = "http://localhost:18080",
30+
timeout: int = 30,
31+
basic_auth_username: str | None = None,
32+
basic_auth_password: str | None = None,
33+
):
2834
self.server_url = server_url.rstrip("/")
2935
self.base_url = f"{self.server_url}/api/v1"
3036
self.timeout = timeout
3137
self._session = requests.Session()
38+
if basic_auth_username is not None:
39+
self._session.auth = (basic_auth_username, basic_auth_password or "")
3240
self._attempt_cache: dict[str, str | None] = {}
3341

3442
def _resolve_attempt(self, app_id: str) -> str:

spark_history_cli/tests/test_core.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
from unittest.mock import patch, MagicMock
1414

1515
import pytest
16+
from click.testing import CliRunner
1617

1718
from spark_history_cli.core.client import SparkHistoryClient, HistoryServerError
1819
from spark_history_cli.core.session import Session
1920
from spark_history_cli.core import formatters as fmt
21+
from spark_history_cli.cli import cli
2022

2123

2224
# ── Sample API Responses ──────────────────────────────────────────────
@@ -227,6 +229,14 @@ def test_connection_error(self):
227229
client.get_version()
228230
assert "Cannot connect" in str(exc_info.value)
229231

232+
def test_basic_auth_is_configured(self):
233+
client = SparkHistoryClient(
234+
"http://test:18080",
235+
basic_auth_username="alice",
236+
basic_auth_password="secret",
237+
)
238+
assert client._session.auth == ("alice", "secret")
239+
230240
def test_http_404(self):
231241
client = self._mock_client({"message": "not found"}, status_code=404)
232242
with pytest.raises(HistoryServerError) as exc_info:
@@ -433,3 +443,29 @@ def test_install_skill(self):
433443
assert result.returncode == 0
434444
assert os.path.exists(os.path.join(target, "SKILL.md"))
435445
assert "Installed Copilot skill" in result.stdout
446+
447+
448+
class TestCLIOptions:
449+
def test_basic_auth_options_are_accepted(self):
450+
runner = CliRunner()
451+
result = runner.invoke(
452+
cli,
453+
["--basic-auth-user", "alice", "--basic-auth-password", "secret", "--help"],
454+
)
455+
assert result.exit_code == 0
456+
457+
def test_basic_auth_password_requires_user(self):
458+
runner = CliRunner()
459+
result = runner.invoke(cli, ["--basic-auth-password", "secret", "apps"])
460+
assert result.exit_code != 0
461+
assert "--basic-auth-password requires --basic-auth-user" in result.output
462+
463+
def test_basic_auth_user_prompts_for_password_hidden(self):
464+
runner = CliRunner()
465+
mock_client = MagicMock()
466+
mock_client.get_version.return_value = {"spark": "4.0.0"}
467+
with patch("spark_history_cli.cli.click.prompt", return_value="secret") as prompt_mock:
468+
with patch("spark_history_cli.cli.CliState.ensure_client", return_value=mock_client):
469+
result = runner.invoke(cli, ["--basic-auth-user", "alice", "version"])
470+
assert result.exit_code == 0
471+
prompt_mock.assert_called_once_with("Basic Auth password", hide_input=True)

0 commit comments

Comments
 (0)