Skip to content

Add Akeyless Secret Store/SSH Plugin#147

Open
kgal-akl wants to merge 12 commits intoansible:develfrom
akeyless-community:add-akeyless-credential
Open

Add Akeyless Secret Store/SSH Plugin#147
kgal-akl wants to merge 12 commits intoansible:develfrom
akeyless-community:add-akeyless-credential

Conversation

@kgal-akl
Copy link
Copy Markdown

@kgal-akl kgal-akl commented Jan 20, 2026

PR created as part of ansible/awx#16151 (comment).

Summary by CodeRabbit

  • New Features

    • Added Akeyless credential plugins (secret retrieval and SSH certificate issuance).
  • Tests

    • Added comprehensive unit tests for secret formats, SSH issuance, TTL handling, and error paths.
  • Chores

    • Added entry points and optional dependency groups to enable Akeyless integrations.
    • Relaxed type-checking for the Akeyless SDK to suppress import/type errors.
  • Style

    • Updated linting and pre-commit tooling to include the new plugin.
  • Documentation

    • Updated docs and spelling list to account for Akeyless names.

@kgal-akl
Copy link
Copy Markdown
Author

@jessicamack - PR is ready for review.

@webknjaz
Copy link
Copy Markdown
Member

The CI didn't start for some reason (or the runs got outdated and GH cleared them?). Re-opening the PR to trigger the CI.

@webknjaz webknjaz closed this Mar 18, 2026
@webknjaz webknjaz reopened this Mar 18, 2026
@webknjaz
Copy link
Copy Markdown
Member

Tip

I wonder if this needs to be a part of awx-plugins-core. I don't see why it can't be its own project that end-users would opt-in to use via pip-install.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 18, 2026

Codecov Report

❌ Patch coverage is 92.27468% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.96%. Comparing base (6f0a4de) to head (ae8022e).
⚠️ Report is 2 commits behind head on devel.
✅ All tests successful. No failed tests found.

❌ Your patch check has failed because the patch coverage (92.27%) is below the target coverage (100.00%). You can increase the patch coverage or adjust the target coverage.
❌ Your project check has failed because the head coverage (99.04%) is below the target coverage (100.00%). You can increase the head coverage or adjust the target coverage.

@coderabbitai

This comment was marked as outdated.

coderabbitai[bot]

This comment was marked as outdated.

@kgal-akl
Copy link
Copy Markdown
Author

Re the tip about whether this needs to be part of awx-plugins-core: we'd prefer to keep it here to stay consistent with how other third-party integrations (Azure Key Vault, HashiVault, CyberArk AIM, Conjur, etc.) are packaged. Being in the core repo gives it the same CI, linting, and test infrastructure — and makes it discoverable to AWX users without an additional install step. Happy to discuss further if there's a strong reason to break it out.

@kgal-akl kgal-akl requested a review from webknjaz March 18, 2026 14:23
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
tests/akeyless_test.py (2)

205-207: Minor: En-dash character in comment.

Line 206 uses an en-dash (–) instead of a hyphen (-).

Suggested fix
-# akeyless_ssh_backend – SSH certificate plugin
+# akeyless_ssh_backend - SSH certificate plugin
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/akeyless_test.py` around lines 205 - 207, The comment header line
containing "akeyless_ssh_backend – SSH certificate plugin" uses an en-dash;
replace the en-dash (–) with a standard hyphen (-) so the line reads
"akeyless_ssh_backend - SSH certificate plugin" to keep ASCII characters
consistent in tests/akeyless_test.py.

89-91: Minor: En-dash character in comment.

Line 90 uses an en-dash (–) instead of a hyphen (-). This is a minor formatting inconsistency.

Suggested fix
-# akeyless_backend – secret store plugin
+# akeyless_backend - secret store plugin
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/akeyless_test.py` around lines 89 - 91, The comment header contains an
en-dash in the string "akeyless_backend – secret store plugin"; replace the
en-dash (–) with a standard hyphen (-) so it reads "akeyless_backend - secret
store plugin" to keep consistent ASCII punctuation in the comment.
src/awx_plugins/credentials/akeyless.py (1)

267-280: Consider defensive handling for malformed JSON structures.

The _t.cast on line 274 assumes the parsed JSON is dict[str, str]. If the secret contains a nested object or non-string value for the requested key, this could surface as a confusing runtime error downstream.

The current implementation is acceptable given that Akeyless secret formats are well-defined, but consider whether additional validation would improve error messages.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/awx_plugins/credentials/akeyless.py` around lines 267 - 280, The function
_extract_structured_secret assumes secret_data JSON decodes to dict[str,str];
instead, after parsing secret_data into secret_dict, validate that it's a
mapping and that secret_key exists and maps to a string (or coerce to string),
and raise clear ValueError messages referencing secret_key and secret_path when
the JSON is not an object or the value is missing/invalid; update uses of
secret_dict, secret_key, and secret_path in the error messages so malformed JSON
(e.g., nested objects or non-string values) produces a descriptive error rather
than a confusing downstream exception.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/awx_plugins/credentials/akeyless.py`:
- Around line 267-280: The function _extract_structured_secret assumes
secret_data JSON decodes to dict[str,str]; instead, after parsing secret_data
into secret_dict, validate that it's a mapping and that secret_key exists and
maps to a string (or coerce to string), and raise clear ValueError messages
referencing secret_key and secret_path when the JSON is not an object or the
value is missing/invalid; update uses of secret_dict, secret_key, and
secret_path in the error messages so malformed JSON (e.g., nested objects or
non-string values) produces a descriptive error rather than a confusing
downstream exception.

In `@tests/akeyless_test.py`:
- Around line 205-207: The comment header line containing "akeyless_ssh_backend
– SSH certificate plugin" uses an en-dash; replace the en-dash (–) with a
standard hyphen (-) so the line reads "akeyless_ssh_backend - SSH certificate
plugin" to keep ASCII characters consistent in tests/akeyless_test.py.
- Around line 89-91: The comment header contains an en-dash in the string
"akeyless_backend – secret store plugin"; replace the en-dash (–) with a
standard hyphen (-) so it reads "akeyless_backend - secret store plugin" to keep
consistent ASCII punctuation in the comment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ade9d49a-bf1d-453c-9098-e5a832469aa2

📥 Commits

Reviewing files that changed from the base of the PR and between 98271f6 and 0858c70.

📒 Files selected for processing (3)
  • .mypy.ini
  • src/awx_plugins/credentials/akeyless.py
  • tests/akeyless_test.py

@webknjaz
Copy link
Copy Markdown
Member

Well, the maintenance resources (human-wise) are quite limited here. And so the advantages can easily end up being downsides. As for the infra — it can be copied to whatever place necessary 🤷‍♂️

@webknjaz webknjaz requested a review from jessicamack March 18, 2026 16:06
@webknjaz
Copy link
Copy Markdown
Member

@kgal-akl I think I've posted all the feedback I could. But the ultimate decision on this belonging here would be by the respective team. I'm just helping out with the repo infra occasionally.

@webknjaz webknjaz linked an issue Mar 18, 2026 that may be closed by this pull request
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/akeyless_test.py (1)

192-199: Consider adding a test for ApiException during authentication.

The current test covers token=None (auth success but no token), but there's no test for ApiException raised by api_instance.auth(). This would help verify the behavior flagged in the implementation review.

Suggested test case
def test_backend_auth_api_exc_raises(
    patch_setup_client: _MockApiFactory,
) -> None:
    """An ApiException from auth should surface as RuntimeError."""
    mock_api = patch_setup_client()
    mock_api.auth.side_effect = ApiException(
        status=401,
        reason='Unauthorized',
    )

    with pytest.raises(
        RuntimeError,
        match=r'Akeyless API error: Unauthorized \(Status: 401\)',
    ):
        akeyless_mod.akeyless_backend(**_backend_kwargs())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/akeyless_test.py` around lines 192 - 199, Add a new test that verifies
an ApiException raised by the API auth call surfaces as a RuntimeError from
akeyless_mod.akeyless_backend: use the existing fixture patch_setup_client to
get mock_api, set mock_api.auth.side_effect = ApiException(status=401,
reason='Unauthorized'), and assert that calling
akeyless_mod.akeyless_backend(**_backend_kwargs()) raises RuntimeError with a
message matching "Akeyless API error: Unauthorized (Status: 401)"; place the
test alongside test_backend_auth_failure_raises and name it
test_backend_auth_api_exc_raises.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/awx_plugins/credentials/akeyless.py`:
- Around line 356-363: The try/except around calling _authenticate only catches
ValueError so ApiException from api_instance.auth() escapes; update the except
to catch both ApiException and ValueError, for example: except (ApiException,
ValueError) as exc: raise RuntimeError(str(exc)) from exc, and apply the same
change in the akeyless_ssh_backend authentication block to match how
_fetch_secret_value and _fetch_ssh_certificate wrap ApiException into
RuntimeError.

---

Nitpick comments:
In `@tests/akeyless_test.py`:
- Around line 192-199: Add a new test that verifies an ApiException raised by
the API auth call surfaces as a RuntimeError from akeyless_mod.akeyless_backend:
use the existing fixture patch_setup_client to get mock_api, set
mock_api.auth.side_effect = ApiException(status=401, reason='Unauthorized'), and
assert that calling akeyless_mod.akeyless_backend(**_backend_kwargs()) raises
RuntimeError with a message matching "Akeyless API error: Unauthorized (Status:
401)"; place the test alongside test_backend_auth_failure_raises and name it
test_backend_auth_api_exc_raises.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d8dbeb7d-a989-4f6f-a2aa-9f14592d21ed

📥 Commits

Reviewing files that changed from the base of the PR and between 0858c70 and 2ee9014.

📒 Files selected for processing (8)
  • .flake8
  • .mypy.ini
  • .pre-commit-config.yaml
  • .pylintrc.toml
  • .ruff.toml
  • pytest.ini
  • src/awx_plugins/credentials/akeyless.py
  • tests/akeyless_test.py

@kgal-akl kgal-akl requested a review from webknjaz March 18, 2026 19:19
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/awx_plugins/credentials/akeyless.py (2)

430-437: ⚠️ Potential issue | 🟠 Major

Same issue: ApiException from authentication escapes unwrapped.

Apply the same fix as suggested for akeyless_backend to maintain consistent error handling.

Proposed fix
         try:
             token = _authenticate(
                 api_instance,
                 kwargs['access_id'],
                 kwargs['access_key'],
             )
-        except ValueError as val_err:
+        except _ApiException as api_exc:
+            raise RuntimeError(
+                f'Akeyless API error: {api_exc.reason}'
+                f' (Status: {api_exc.status})',
+            ) from api_exc
+        except ValueError as val_err:
             raise RuntimeError(str(val_err)) from val_err
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/awx_plugins/credentials/akeyless.py` around lines 430 - 437, The
authentication call to _authenticate in the akeyless plugin can raise
ApiException which is currently not wrapped; add an except ApiException as
api_err handler (in addition to the existing ValueError handler) around the
token = _authenticate(...) call and re-raise a RuntimeError from api_err (e.g.,
raise RuntimeError(str(api_err)) from api_err) to mirror the akeyless_backend
error handling and ensure consistent wrapped errors.

369-376: ⚠️ Potential issue | 🟠 Major

ApiException from api_instance.auth() escapes unwrapped.

The try/except block catches ValueError from _authenticate, but api_instance.auth() (line 244) can also raise ApiException. This creates inconsistent error handling: API errors during secret retrieval are wrapped in RuntimeError, but identical API errors during authentication escape as-is.

Proposed fix: catch both exception types from authentication
         try:
             token = _authenticate(
                 api_instance,
                 kwargs['access_id'],
                 kwargs['access_key'],
             )
-        except ValueError as val_err:
+        except _ApiException as api_exc:
+            raise RuntimeError(
+                f'Akeyless API error: {api_exc.reason}'
+                f' (Status: {api_exc.status})',
+            ) from api_exc
+        except ValueError as val_err:
             raise RuntimeError(str(val_err)) from val_err
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/awx_plugins/credentials/akeyless.py` around lines 369 - 376, The
authentication try/except around _authenticate currently only catches ValueError
so ApiException thrown by api_instance.auth() (called inside _authenticate)
escapes; update the except to catch both ValueError and ApiException and
re-raise them as a RuntimeError (e.g., except (ValueError, ApiException) as err:
raise RuntimeError(str(err)) from err) so authentication failures are handled
consistently with secret retrieval; locate the logic around _authenticate(...)
and api_instance.auth() and apply the dual-exception catch using the
ApiException symbol.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/awx_plugins/credentials/akeyless.py`:
- Around line 430-437: The authentication call to _authenticate in the akeyless
plugin can raise ApiException which is currently not wrapped; add an except
ApiException as api_err handler (in addition to the existing ValueError handler)
around the token = _authenticate(...) call and re-raise a RuntimeError from
api_err (e.g., raise RuntimeError(str(api_err)) from api_err) to mirror the
akeyless_backend error handling and ensure consistent wrapped errors.
- Around line 369-376: The authentication try/except around _authenticate
currently only catches ValueError so ApiException thrown by api_instance.auth()
(called inside _authenticate) escapes; update the except to catch both
ValueError and ApiException and re-raise them as a RuntimeError (e.g., except
(ValueError, ApiException) as err: raise RuntimeError(str(err)) from err) so
authentication failures are handled consistently with secret retrieval; locate
the logic around _authenticate(...) and api_instance.auth() and apply the
dual-exception catch using the ApiException symbol.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 308cebc2-e120-4fff-a205-2a2fd9498764

📥 Commits

Reviewing files that changed from the base of the PR and between 2ee9014 and ae8022e.

📒 Files selected for processing (4)
  • docs/conf.py
  • docs/spelling_wordlist.txt
  • src/awx_plugins/credentials/akeyless.py
  • tests/akeyless_test.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/akeyless_test.py

mock_api.describe_item.return_value = mock_describe
mock_api.get_secret_value.return_value = {secret_path: secret_data}
mock_api.get_ssh_certificate.return_value.data = ssh_cert_data
return mock_api
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, wouldn't it be easier to go with stubs instead of mocks? It seems like half of these would be just fine with a dumb dataclass-based implementation instead of mocks (let alone magic ones).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.mypy.ini:
- Around line 65-72: The mypy config for [mypy-awx_plugins.credentials.akeyless]
currently disables multiple strict checks; instead create explicit type stubs
for the akeyless SDK in the _type_stubs directory (e.g., _type_stubs/akeyless/
with .pyi files defining the public classes/functions you use), remove the broad
suppressions (revert disallow_any_unimported, disallow_any_explicit,
disallow_untyped_calls, warn_return_any back to true) and keep only the minimal
suppression used by other plugins (disallow_any_expr = false) so mypy uses your
surgical stubs to type-check awx_plugins.credentials.akeyless correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b4c44264-39a8-4a4e-94c5-1ab0ba75ee0d

📥 Commits

Reviewing files that changed from the base of the PR and between ae8022e and 8a841d9.

📒 Files selected for processing (4)
  • .flake8
  • .mypy.ini
  • .pre-commit-config.yaml
  • docs/conf.py
✅ Files skipped from review due to trivial changes (3)
  • .flake8
  • .pre-commit-config.yaml
  • docs/conf.py

@webknjaz
Copy link
Copy Markdown
Member

webknjaz commented Apr 2, 2026

@kgal-akl plz avoid adding merge commits in favor of rebasing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Akeyless Secrets and SSH Plugin

2 participants