Skip to content

Commit 7e63cbf

Browse files
fix(secret): add support of secret path
1 parent 21a7c95 commit 7e63cbf

5 files changed

Lines changed: 502 additions & 3 deletions

File tree

plugins/module_utils/model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class Secret(Model):
7474
id: str = ""
7575
description: str = ""
7676
tags: list[str] = field(default_factory=list)
77+
path: str = "/"
7778

7879

7980
@dataclass

plugins/module_utils/scaleway_secret.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,22 @@ def build_secret_version(parameters: dict) -> SecretVersion:
2323

2424
def get_secret(api: "SecretV1Beta1API", **kwargs) -> Secret:
2525
"""
26-
Get a secret by secret_id or name
26+
Get a secret by secret_id or name with optional path.
2727
"""
2828
if "secret_id" in kwargs:
2929
secret = api.get_secret(secret_id=kwargs["secret_id"])
3030

3131
elif "name" in kwargs:
32-
secrets = api.list_secrets(name=kwargs["name"], scheduled_for_deletion=False)
32+
list_kwargs = dict(name=kwargs["name"], scheduled_for_deletion=False)
33+
if "path" in kwargs:
34+
list_kwargs["path"] = kwargs["path"]
35+
secrets = api.list_secrets(**list_kwargs)
3336

3437
if len(secrets.secrets) == 0:
38+
if "path" in kwargs:
39+
raise SecretNotFound(
40+
f"Secret {kwargs['name']} not found at path {kwargs['path']}"
41+
)
3542
raise SecretNotFound(f"Secret {kwargs['name']} not found")
3643

3744
secret = secrets.secrets[0]
@@ -72,7 +79,10 @@ def update_secret(
7279
7380
return changed, local_model, remote_model
7481
"""
75-
remote_model = get_secret(api, name=parameters.get("name"))
82+
lookup = {"name": parameters.get("name")}
83+
if "path" in parameters:
84+
lookup["path"] = parameters["path"]
85+
remote_model = get_secret(api, **lookup)
7686

7787
# build and diff source model with the api one
7888
local_model = build_secret(parameters)

plugins/modules/scaleway_secret.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
description: name
3232
type: str
3333
required: true
34+
path:
35+
description: path
36+
type: str
37+
required: false
38+
default: /
3439
project_id:
3540
description: project_id
3641
type: str
@@ -203,6 +208,7 @@ def main() -> None:
203208
dict(
204209
state=dict(type="str", default="present", choices=["absent", "present"]),
205210
name=dict(type="str", required=True),
211+
path=dict(type="str", required=False, default="/"),
206212
project_id=dict(type="str", required=False),
207213
tags=dict(type="list", required=False, elements="str"),
208214
description=dict(type="str", required=False),

tests/unit/plugins/test_scaleway_secret.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,44 @@ def test_create_with_default_project_id(
9191
},
9292
)
9393

94+
@pytest.mark.parametrize(
95+
"set_module_args",
96+
[
97+
{
98+
"name": "test_secret",
99+
"path": "/custom/path",
100+
"tags": ["test", "secret"],
101+
"description": "test_description",
102+
"protected": False,
103+
}
104+
],
105+
indirect=True,
106+
)
107+
@patch.object(secret_api, "unmarshal_Secret")
108+
@patch.object(secret_api.SecretV1Beta1API, "_request")
109+
def test_create_with_custom_path(
110+
self,
111+
mock_request,
112+
mock_unmarshal_secret,
113+
scaleway_config_profile,
114+
set_module_args,
115+
):
116+
mock_unmarshal_secret.return_value = MagicMock()
117+
mock_request.return_value = MagicMock(status_code=201)
118+
scaleway_secret.main()
119+
mock_request.assert_called_once_with(
120+
"POST",
121+
f"/secret-manager/v1beta1/regions/{scaleway_config_profile.default_region}/secrets",
122+
body={
123+
"name": "test_secret",
124+
"path": "/custom/path",
125+
"tags": ["test", "secret"],
126+
"description": "test_description",
127+
"protected": False,
128+
"project_id": scaleway_config_profile.default_project_id,
129+
},
130+
)
131+
94132
@pytest.mark.parametrize(
95133
"set_module_args",
96134
[
@@ -130,6 +168,173 @@ def __dict__(self):
130168
f"/secret-manager/v1beta1/regions/{scaleway_config_profile.default_region}/secrets/{self.test_uuid}",
131169
)
132170

171+
@pytest.mark.parametrize(
172+
"set_module_args",
173+
[
174+
{
175+
"name": "test_secret",
176+
"path": "/custom/path",
177+
"state": "absent",
178+
}
179+
],
180+
indirect=True,
181+
)
182+
@patch.object(secret_api, "unmarshal_ListSecretsResponse")
183+
@patch.object(secret_api.SecretV1Beta1API, "_request")
184+
def test_delete_secret_with_path(
185+
self,
186+
mock_request,
187+
mock_unmarshal_list_secrets_response,
188+
scaleway_config_profile,
189+
set_module_args,
190+
):
191+
class MockedSecret(MagicMock):
192+
id = self.test_uuid
193+
name = "test_secret"
194+
path = "/custom/path"
195+
196+
def __dict__(self):
197+
return {
198+
"id": self.id,
199+
"name": self.name,
200+
"path": self.path,
201+
}
202+
203+
mock_unmarshal_list_secrets_response.return_value = MagicMock(
204+
secrets=[MockedSecret]
205+
)
206+
mock_request.side_effect = [
207+
MagicMock(status_code=200), # list secret response
208+
MagicMock(status_code=204), # delete secret response
209+
]
210+
scaleway_secret.main()
211+
mock_request.assert_any_call(
212+
"DELETE",
213+
f"/secret-manager/v1beta1/regions/{scaleway_config_profile.default_region}/secrets/{self.test_uuid}",
214+
)
215+
216+
@pytest.mark.parametrize(
217+
"set_module_args",
218+
[
219+
{
220+
"name": "test_secret",
221+
"path": "/custom/path",
222+
"tags": ["test", "secret"],
223+
"description": "test_description",
224+
"protected": False,
225+
"state": "present",
226+
"__check_mode__": True,
227+
}
228+
],
229+
indirect=True,
230+
)
231+
@patch.object(secret_api, "unmarshal_ListSecretsResponse")
232+
@patch.object(secret_api, "unmarshal_Secret")
233+
@patch.object(secret_api.SecretV1Beta1API, "_request")
234+
def test_update_secret_with_path_check_mode(
235+
self,
236+
mock_request,
237+
mock_unmarshal_secret,
238+
mock_unmarshal_list_secrets_response,
239+
scaleway_config_profile,
240+
set_module_args,
241+
):
242+
class MockedSecret(MagicMock):
243+
id = self.test_uuid
244+
name = "test_secret"
245+
path = "/custom/path"
246+
description = "old_description"
247+
tags = ["old-tag"]
248+
249+
def __dict__(self):
250+
return {
251+
"id": self.id,
252+
"name": self.name,
253+
"path": self.path,
254+
"description": self.description,
255+
"tags": self.tags,
256+
}
257+
258+
mock_unmarshal_list_secrets_response.return_value = MagicMock(
259+
secrets=[MockedSecret]
260+
)
261+
mock_unmarshal_secret.return_value = MagicMock()
262+
mock_request.side_effect = [
263+
MagicMock(status_code=200), # list secrets response
264+
]
265+
scaleway_secret.main()
266+
267+
mock_request.assert_called_once()
268+
call = mock_request.call_args_list[0]
269+
assert call[0][0] == "GET", "Only GET request should be made in check mode"
270+
271+
@pytest.mark.parametrize(
272+
"set_module_args",
273+
[
274+
{
275+
"name": "test_secret",
276+
"path": "/custom/path",
277+
"tags": ["test", "secret"],
278+
"description": "test_description",
279+
"protected": False,
280+
}
281+
],
282+
indirect=True,
283+
)
284+
@patch.object(secret_api, "unmarshal_ListSecretsResponse")
285+
@patch.object(secret_api, "unmarshal_Secret")
286+
@patch.object(secret_api.SecretV1Beta1API, "_request")
287+
def test_update_secret_with_path(
288+
self,
289+
mock_request,
290+
mock_unmarshal_secret,
291+
mock_unmarshal_list_secrets_response,
292+
scaleway_config_profile,
293+
set_module_args,
294+
):
295+
class MockedSecret(MagicMock):
296+
id = self.test_uuid
297+
name = "test_secret"
298+
path = "/custom/path"
299+
description = "old_description"
300+
301+
def __dict__(self):
302+
return {
303+
"id": self.id,
304+
"name": self.name,
305+
"path": self.path,
306+
"description": self.description,
307+
}
308+
309+
mock_unmarshal_list_secrets_response.return_value = MagicMock(
310+
secrets=[MockedSecret]
311+
)
312+
mock_unmarshal_secret.return_value = MagicMock()
313+
mock_request.side_effect = [
314+
MagicMock(status_code=200), # list secrets response
315+
MagicMock(status_code=200), # update secret response
316+
]
317+
scaleway_secret.main()
318+
319+
list_call = None
320+
update_call = None
321+
for call in mock_request.call_args_list:
322+
if call[0][0] == "GET":
323+
list_call = call
324+
elif call[0][0] == "PATCH":
325+
update_call = call
326+
327+
assert list_call is not None, "List secrets call should be made"
328+
assert update_call is not None, "Update secret call should be made"
329+
330+
update_body = update_call[1]["body"]
331+
assert "path" in update_body, "Update should include path parameter"
332+
assert update_body["path"] == "/custom/path", "Path should be /custom/path"
333+
assert update_body["description"] == "test_description", (
334+
"Description should be updated"
335+
)
336+
assert update_body["tags"] == ["test", "secret"], "Tags should be updated"
337+
133338

134339
@patch.object(basic.AnsibleModule, "exit_json", MagicMock())
135340
class TestScalewaySecretVersion:

0 commit comments

Comments
 (0)