Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
opensearch_version: "3.6.0"
- fess_version: "15.7.0"
opensearch_version: "3.7.0"
- fess_version: "15.8.0"
opensearch_version: "3.8.0"

steps:
- uses: actions/checkout@v4
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ uv tool install fessctl
```bash
export FESS_ACCESS_TOKEN=your_access_token_here
export FESS_ENDPOINT=https://your-fess-server
export FESS_VERSION=15.7.0
export FESS_VERSION=15.8.0

fessctl --help
fessctl ping
Expand All @@ -54,7 +54,7 @@ If you prefer a container-based workflow, use the pre-built Docker image:
docker run --rm \
-e FESS_ENDPOINT=https://your-fess-server \
-e FESS_ACCESS_TOKEN=your_access_token_here \
-e FESS_VERSION=15.7.0 \
-e FESS_VERSION=15.8.0 \
ghcr.io/codelibs/fessctl:0.2.0 --help
```

Expand All @@ -64,13 +64,13 @@ Run actual commands:
docker run --rm \
-e FESS_ENDPOINT=https://your-fess-server \
-e FESS_ACCESS_TOKEN=your_access_token_here \
-e FESS_VERSION=15.7.0 \
-e FESS_VERSION=15.8.0 \
ghcr.io/codelibs/fessctl:0.2.0 ping

docker run --rm \
-e FESS_ENDPOINT=https://your-fess-server \
-e FESS_ACCESS_TOKEN=your_access_token_here \
-e FESS_VERSION=15.7.0 \
-e FESS_VERSION=15.8.0 \
ghcr.io/codelibs/fessctl:0.2.0 user list
```

Expand All @@ -90,7 +90,7 @@ Then run with your custom image:
docker run --rm \
-e FESS_ENDPOINT=https://your-fess-server \
-e FESS_ACCESS_TOKEN=your_access_token_here \
-e FESS_VERSION=15.7.0 \
-e FESS_VERSION=15.8.0 \
fessctl:latest --help
```

Expand All @@ -115,7 +115,7 @@ uv pip install -e src
```bash
export FESS_ACCESS_TOKEN=your_access_token_here
export FESS_ENDPOINT=https://your-fess-server
export FESS_VERSION=15.7.0
export FESS_VERSION=15.8.0

fessctl --help
fessctl ping
Expand All @@ -129,7 +129,7 @@ All four methods require the following environment variables:

- `FESS_ENDPOINT`: The URL of your Fess server's API endpoint (default: `http://localhost:8080`)
- `FESS_ACCESS_TOKEN`: Bearer token for API authentication (required)
- `FESS_VERSION`: Target Fess version for API compatibility (default: `15.7.0`). Set this to match your Fess server. Fess 14.x and 15.x are supported; the value controls version-specific behavior such as HTTP methods for CRUD operations and the health-check endpoint (`/api/v1/health` for versions before 15.7, `/api/v2/health` for 15.7 and later).
- `FESS_VERSION`: Target Fess version for API compatibility (default: `15.8.0`). Set this to match your Fess server. Fess 14.x and 15.x are supported; the value controls version-specific behavior such as HTTP methods for CRUD operations and the health-check endpoint (`/api/v1/health` for versions before 15.7, `/api/v2/health` for 15.7 and later).

## License

Expand Down
2 changes: 1 addition & 1 deletion skills/fessctl/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ See `references/installation.md` for the exact wrappers.

- `FESS_ENDPOINT` (default `http://localhost:8080`)
- `FESS_ACCESS_TOKEN` (required for any non-`ping` call)
- `FESS_VERSION` (e.g. `15.6.0`; must match the target Fess server)
- `FESS_VERSION` (e.g. `15.8.0`; must match the target Fess server)

See `references/authentication.md` for token issuance.

Expand Down
4 changes: 2 additions & 2 deletions skills/fessctl/references/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Every fessctl call other than `ping` is authenticated with a Fess access token s
|----------|----------|---------|-------|
| `FESS_ENDPOINT` | optional | `http://localhost:8080` | Base URL of the target Fess server. Include scheme; do not include a trailing `/`. |
| `FESS_ACCESS_TOKEN` | **yes** for any non-`ping` call | none | Bearer token issued from the Fess admin UI or via `fessctl accesstoken create`. |
| `FESS_VERSION` | optional | `15.4.0` (as of fessctl 0.1.0) | Must match the major.minor of the target Fess server so request shapes line up. Set it explicitly — do not rely on the default. |
| `FESS_VERSION` | optional | `15.8.0` | Must match the major.minor of the target Fess server so request shapes line up. Set it explicitly — do not rely on the default. |

Defaults live in `src/fessctl/config/settings.py`. The defaults are conservative and may lag the latest Fess release; for any non-trivial work, set `FESS_ENDPOINT` and `FESS_VERSION` explicitly.

Expand Down Expand Up @@ -39,7 +39,7 @@ Pick the option that matches how you run fessctl.
```bash
export FESS_ENDPOINT=http://localhost:8080
export FESS_ACCESS_TOKEN=eyJhbGciOi...
export FESS_VERSION=15.6.0
export FESS_VERSION=15.8.0
```
Add `.envrc` to `.gitignore`. Run `direnv allow` to activate.

Expand Down
2 changes: 1 addition & 1 deletion skills/fessctl/references/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Two networking notes for reaching a Fess server running on the **host**:

## Choosing the Docker tag

The Docker image is published at `ghcr.io/codelibs/fessctl`. Pin a tag rather than `latest` for reproducible runs. The convention is to keep the image tag close to the Fess version it has been validated against — if you are talking to a Fess 15.6 server, prefer the tag whose `FESS_VERSION` default matches. Inspect available tags at <https://github.com/codelibs/fessctl/pkgs/container/fessctl> if unsure.
The Docker image is published at `ghcr.io/codelibs/fessctl`. Pin a tag rather than `latest` for reproducible runs. The convention is to keep the image tag close to the Fess version it has been validated against — if you are talking to a Fess 15.8 server, prefer the tag whose `FESS_VERSION` default matches. Inspect available tags at <https://github.com/codelibs/fessctl/pkgs/container/fessctl> if unsure.

## Verifying the install

Expand Down
15 changes: 10 additions & 5 deletions src/fessctl/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ def ping(
result = client.ping()
if client.is_api_v2:
# Fess 15.7+ /api/v2/health envelope:
# healthy: {"response": {"status": 0, "engine": {"status": "green", "ping_status": 0}}}
# healthy: {"response": {"status": 0, "engine": {"status": "GREEN", "ping_status": 0}}}
# red: {"response": {"status": 9, "error": {"message": "...",
# "details": {"engine": {"status": "red", "ping_status": N}}}}}
# "details": {"engine": {"status": "RED", "ping_status": N}}}}}
response = result.get("response", {})
engine = response.get("engine")
if engine is None:
Expand All @@ -107,14 +107,19 @@ def ping(
timed_out = result.get("data", {}).get("timed_out", True)
message = result.get("response", {}).get("message", "")

# The /api/v2 envelope carries the OpenSearch enum name ("GREEN"/"YELLOW"/"RED"), while
# the legacy /api/v1 body spelled the same value in lower case. Compare case-insensitively
# so a healthy 15.7+ server is not reported as an error.
normalized_status = str(status).lower()

if output == "json":
typer.echo(json.dumps(result, indent=2))
elif output == "yaml":
typer.echo(yaml.dump(result))
else:
if status == "green" and not timed_out:
typer.echo(format_result_markdown(True, "Fess server is healthy (status: green).", "Server", "ping"))
elif status == "yellow":
if normalized_status == "green" and not timed_out:
typer.echo(format_result_markdown(True, f"Fess server is healthy (status: {status}).", "Server", "ping"))
elif normalized_status == "yellow":
typer.echo(format_result_markdown(True, f"Fess server status: {status} (timed_out: {timed_out})", "Server", "ping"))
else:
typer.echo(format_result_markdown(False, f"Fess server status: {status} (timed_out: {timed_out}) {message}", "Server", "ping"))
Expand Down
2 changes: 1 addition & 1 deletion src/fessctl/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ class Settings:
access_token: str | None = field(
default_factory=lambda: os.getenv("FESS_ACCESS_TOKEN", None))
fess_version: str = field(
default_factory=lambda: os.getenv("FESS_VERSION", "15.7.0"))
default_factory=lambda: os.getenv("FESS_VERSION", "15.8.0"))
41 changes: 41 additions & 0 deletions tests/commands/test_ping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import json
import pytest
from typer.testing import CliRunner

from fessctl.api.client import FessAPIClient
from fessctl.cli import app
from fessctl.config.settings import Settings


@pytest.fixture(scope="module")
def runner():
"""
Provides a CliRunner instance for invoking commands.
"""
return CliRunner()


def test_ping_reports_a_healthy_server(runner, fess_service):
"""
A server the test harness already waited for must be reported as healthy,
whatever spelling of the cluster status the running Fess version uses.
"""
result = runner.invoke(app, ["ping"])
assert result.exit_code == 0, f"Ping failed: {result.stdout}"
assert "error" not in result.stdout.lower(), result.stdout


def test_ping_json_reports_a_usable_cluster(runner, fess_service):
"""
The raw health payload must carry a green or yellow cluster status.
"""
result = runner.invoke(app, ["ping", "--output", "json"])
assert result.exit_code == 0, f"Ping failed: {result.stdout}"
payload = json.loads(result.stdout)

if FessAPIClient(Settings()).is_api_v2:
status = payload["response"]["engine"]["status"]
else:
status = payload["data"]["status"]

assert status.lower() in ("green", "yellow"), f"Unexpected cluster status: {status}"
6 changes: 3 additions & 3 deletions tests/compose-fess15.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
services:
fessctl_fess01:
image: ghcr.io/codelibs/fess:${FESS_VERSION:-15.7.0}
image: ghcr.io/codelibs/fess:${FESS_VERSION:-15.8.0}
container_name: fessctl_fess01
environment:
- "SEARCH_ENGINE_HTTP_URL=http://fessctl_search01:9200"
- "FESS_DICTIONARY_PATH=${FESS_DICTIONARY_PATH:-/usr/share/opensearch/config/dictionary/}"
# - "FESS_PLUGINS=fess-ds-csv:${FESS_VERSION:-15.7.0}"
# - "FESS_PLUGINS=fess-ds-csv:${FESS_VERSION:-15.8.0}"
volumes:
# - fessctl_fess01_plugin:/usr/share/fess/app/WEB-INF/plugin
- ./resources/access_token.bulk:/usr/share/fess/app/WEB-INF/classes/fess_indices/fess_config.access_token/access_token.bulk
Expand All @@ -23,7 +23,7 @@ services:
restart: unless-stopped

fessctl_search01:
image: ghcr.io/codelibs/fess-opensearch:${OPENSEARCH_VERSION:-3.6.0}
image: ghcr.io/codelibs/fess-opensearch:${OPENSEARCH_VERSION:-3.8.0}
container_name: fessctl_search01
environment:
- node.name=fessctl_search01
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def fess_service():
print(f"Project root: {project_root}")

# Determine which compose file to use based on FESS_VERSION
fess_version = os.getenv("FESS_VERSION", "15.7.0")
fess_version = os.getenv("FESS_VERSION", "15.8.0")
if fess_version.startswith("15."):
compose_file = "compose-fess15.yaml"
else:
Expand Down
53 changes: 53 additions & 0 deletions tests/unit/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,59 @@ def test_ping_red_status(self, mock_client_class, runner):
assert "red" in result.stdout.lower()
assert "search engine cluster is red" in result.stdout

@patch("fessctl.cli.FessAPIClient")
def test_ping_uppercase_green_status(self, mock_client_class, runner):
"""A real server spells the cluster status with the OpenSearch enum name (GREEN)."""
mock_client = Mock()
mock_client.is_api_v2 = True
mock_client.ping.return_value = {
"response": {"status": 0, "engine": {"status": "GREEN", "ping_status": 0}}
}
mock_client_class.return_value = mock_client

result = runner.invoke(app, ["ping"])

assert result.exit_code == 0
assert "healthy" in result.stdout.lower()
assert "GREEN" in result.stdout

@patch("fessctl.cli.FessAPIClient")
def test_ping_uppercase_yellow_status(self, mock_client_class, runner):
"""An upper-case YELLOW is a warning, not an error."""
mock_client = Mock()
mock_client.is_api_v2 = True
mock_client.ping.return_value = {
"response": {"status": 0, "engine": {"status": "YELLOW", "ping_status": 0}}
}
mock_client_class.return_value = mock_client

result = runner.invoke(app, ["ping"])

assert result.exit_code == 0
assert "YELLOW" in result.stdout

@patch("fessctl.cli.FessAPIClient")
def test_ping_uppercase_red_status(self, mock_client_class, runner):
"""An upper-case RED still fails the command."""
mock_client = Mock()
mock_client.is_api_v2 = True
mock_client.ping.return_value = {
"response": {
"status": 9,
"error": {
"code": "service_unavailable",
"message": "search engine cluster is red",
"details": {"engine": {"status": "RED", "ping_status": 2}},
},
}
}
mock_client_class.return_value = mock_client

result = runner.invoke(app, ["ping"])

assert result.exit_code == 1
assert "RED" in result.stdout

@patch("fessctl.cli.FessAPIClient")
def test_ping_json_output(self, mock_client_class, runner):
"""Test v2 ping JSON output echoes the raw v2 envelope."""
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def test_default_values(self, monkeypatch):

assert settings.fess_endpoint == "http://localhost:8080"
assert settings.access_token is None
assert settings.fess_version == "15.7.0"
assert settings.fess_version == "15.8.0"

def test_endpoint_from_environment(self, monkeypatch):
"""Test that FESS_ENDPOINT environment variable is used."""
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading