Skip to content

Commit 0900301

Browse files
authored
Merge pull request #987 from cderici/update-remove-grant-revoke-secrets
#987 #### Description This is the continuation of #986, completing the support for user secrets on pylibjuju by adding `update_secret`, `remove_secret`, `grant_secret` and `revoke_secret`. #### QA Steps Added integration tests for all of these so they can be individually ran: ``` tox -e integration -- tests/integration/test_secrets.py::test_update_secret ``` ``` tox -e integration -- tests/integration/test_secrets.py::test_remove_secret ``` ``` tox -e integration -- tests/integration/test_secrets.py::test_grant_secret ``` ``` tox -e integration -- tests/integration/test_secrets.py::test_revoke_secret ``` All CI tests need to pass. #### Notes & Discussion JUJU-5093
2 parents 93b0718 + cea1ac5 commit 0900301

2 files changed

Lines changed: 147 additions & 5 deletions

File tree

juju/model.py

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2614,18 +2614,18 @@ async def export_bundle(self, filename=None):
26142614
except IOError:
26152615
raise
26162616

2617-
async def add_secret(self, name, dataArgs, file="", info=""):
2617+
async def add_secret(self, name, data_args, file="", info=""):
26182618
"""Adds a secret with a list of key values
26192619
26202620
Equivalent to the cli command:
26212621
juju add-secret [options] <name> [key[#base64|#file]=value...]
26222622
26232623
:param name str: The name of the secret to be added.
2624-
:param dataArgs []str: The key value pairs to be added into the secret.
2624+
:param data_args []str: The key value pairs to be added into the secret.
26252625
:param file str: A path to a yaml file containing secret key values.
26262626
:param info str: The secret description.
26272627
"""
2628-
data = create_secret_data(dataArgs)
2628+
data = create_secret_data(data_args)
26292629

26302630
if file:
26312631
data_from_file = read_secret_data(file)
@@ -2649,6 +2649,39 @@ async def add_secret(self, name, dataArgs, file="", info=""):
26492649
raise JujuAPIError(result.error.message)
26502650
return result.result
26512651

2652+
async def update_secret(self, name, data_args=[], new_name="", file="", info=""):
2653+
"""Update a secret with a list of key values, or info.
2654+
2655+
Equivalent to the cli command:
2656+
juju add-secret [options] <name> [key[#base64|#file]=value...]
2657+
2658+
:param name str: The name of the secret to be added.
2659+
:param data_args []str: The key value pairs to be added into the secret.
2660+
:param file str: A path to a yaml file containing secret key values.
2661+
:param info str: The secret description.
2662+
"""
2663+
data = create_secret_data(data_args)
2664+
if file:
2665+
data_from_file = read_secret_data(file)
2666+
for k, v in data_from_file.items():
2667+
# Caution: key/value pairs in files overwrite the ones in the args.
2668+
data[k] = v
2669+
2670+
if client.SecretsFacade.best_facade_version(self.connection()) < 2:
2671+
raise JujuNotSupportedError("user secrets")
2672+
secretsFacade = client.SecretsFacade.from_connection(self.connection())
2673+
results = await secretsFacade.UpdateSecrets([{
2674+
'content': {'data': data},
2675+
'description': info,
2676+
'existing-label': name,
2677+
'label': new_name,
2678+
}])
2679+
if len(results.results) != 1:
2680+
raise JujuAPIError(f"expected 1 result, got {len(results.results)}")
2681+
result_error = results.results[0]
2682+
if result_error.error is not None:
2683+
raise JujuAPIError(result_error.error)
2684+
26522685
async def list_secrets(self, filter="", show_secrets=False):
26532686
"""
26542687
Returns the list of available secrets.
@@ -2660,6 +2693,68 @@ async def list_secrets(self, filter="", show_secrets=False):
26602693
})
26612694
return results.results
26622695

2696+
async def remove_secret(self, secret_name, revision=-1):
2697+
"""Remove an existing secret.
2698+
2699+
:param secret_name str: ID|name of the secret to remove.
2700+
:param revision int: remove the specified revision.
2701+
"""
2702+
if client.SecretsFacade.best_facade_version(self.connection()) < 2:
2703+
raise JujuNotSupportedError("user secrets")
2704+
remove_secret_arg = {
2705+
'label': secret_name,
2706+
}
2707+
if revision >= 0:
2708+
remove_secret_arg['revisions'] = [revision]
2709+
2710+
secretsFacade = client.SecretsFacade.from_connection(self.connection())
2711+
results = await secretsFacade.RemoveSecrets([remove_secret_arg])
2712+
if len(results.results) != 1:
2713+
raise JujuAPIError(f"expected 1 result, got {len(results.results)}")
2714+
result_error = results.results[0]
2715+
if result_error.error is not None:
2716+
raise JujuAPIError(result_error.error)
2717+
2718+
async def grant_secret(self, secret_name, application, *applications):
2719+
"""Grants access to a secret to the specified applications.
2720+
2721+
:param secret_name str: ID|name of the secret.
2722+
:param application str: name of an application for which the access is granted
2723+
:param applications []str: names of more applications to associate the secret with
2724+
"""
2725+
if client.SecretsFacade.best_facade_version(self.connection()) < 2:
2726+
raise JujuNotSupportedError("user secrets")
2727+
secretsFacade = client.SecretsFacade.from_connection(self.connection())
2728+
results = await secretsFacade.GrantSecret(
2729+
applications=[application] + list(applications),
2730+
label=secret_name)
2731+
if len(results.results) != 1:
2732+
raise JujuAPIError(f"expected 1 result, got {len(results.results)}")
2733+
result_error = results.results[0]
2734+
if result_error.error is not None:
2735+
raise JujuAPIError(result_error.error)
2736+
2737+
async def revoke_secret(self, secret_name, application, *applications):
2738+
"""Revoke access to a secret.
2739+
2740+
Revoke applications' access to view the value of a specified secret.
2741+
2742+
:param secret_name str: ID|name of the secret.
2743+
:param application str: name of an application for which the access to the secret is revoked
2744+
:param applications []str: names of more applications to disassociate the secret with
2745+
"""
2746+
if client.SecretsFacade.best_facade_version(self.connection()) < 2:
2747+
raise JujuNotSupportedError("user secrets")
2748+
secretsFacade = client.SecretsFacade.from_connection(self.connection())
2749+
results = await secretsFacade.RevokeSecret(
2750+
applications=[application] + list(applications),
2751+
label=secret_name)
2752+
if len(results.results) != 1:
2753+
raise JujuAPIError(f"expected 1 result, got {len(results.results)}")
2754+
result_error = results.results[0]
2755+
if result_error.error is not None:
2756+
raise JujuAPIError(result_error.error)
2757+
26632758
async def _get_source_api(self, url, controller_name=None):
26642759
controller = Controller()
26652760
if url.has_empty_source():

tests/integration/test_secrets.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99
@base.bootstrapped
1010
@pytest.mark.bundle
1111
async def test_add_secret(event_loop):
12-
1312
async with base.CleanModel() as model:
14-
secret = await model.add_secret(name='my-apitoken', dataArgs=['token=34ae35facd4'])
13+
secret = await model.add_secret(name='my-apitoken', data_args=['token=34ae35facd4'])
1514
assert secret.startswith('secret:')
1615

1716
secrets = await model.list_secrets()
@@ -37,3 +36,51 @@ async def test_list_secrets(event_loop):
3736
secrets = await model.list_secrets(show_secrets=True)
3837
assert secrets is not None
3938
assert len(secrets) == 1
39+
40+
41+
@base.bootstrapped
42+
@pytest.mark.bundle
43+
async def test_update_secret(event_loop):
44+
async with base.CleanModel() as model:
45+
secret = await model.add_secret(name='my-apitoken', data_args=['token=34ae35facd4'])
46+
assert secret.startswith('secret:')
47+
48+
await model.update_secret(name='my-apitoken', new_name='new-token')
49+
50+
secrets = await model.list_secrets()
51+
assert len(secrets) == 1
52+
assert secrets[0].label == 'new-token'
53+
54+
55+
@base.bootstrapped
56+
@pytest.mark.bundle
57+
async def test_remove_secret(event_loop):
58+
async with base.CleanModel() as model:
59+
secret = await model.add_secret(name='my-apitoken', data_args=['token=34ae35facd4'])
60+
assert secret.startswith('secret:')
61+
62+
await model.remove_secret('my-apitoken')
63+
64+
secrets = await model.list_secrets()
65+
assert len(secrets) == 0
66+
67+
68+
@base.bootstrapped
69+
@pytest.mark.bundle
70+
async def test_grant_secret(event_loop):
71+
async with base.CleanModel() as model:
72+
secret = await model.add_secret(name='my-apitoken', data_args=['token=34ae35facd4'])
73+
assert secret.startswith('secret:')
74+
75+
await model.deploy('ubuntu')
76+
77+
await model.grant_secret('my-apitoken', 'ubuntu')
78+
79+
80+
@base.bootstrapped
81+
@pytest.mark.bundle
82+
async def test_revoke_secret(event_loop):
83+
async with base.CleanModel() as model:
84+
secret = await model.add_secret(name='my-apitoken', data_args=['token=34ae35facd4'])
85+
assert secret.startswith('secret:')
86+
await model.revoke_secret('my-apitoken', 'ubuntu')

0 commit comments

Comments
 (0)