Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new: Support for Multicluster OBJ in object_keys module #531

Merged
Merged
Show file tree
Hide file tree
Changes from 9 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
45 changes: 36 additions & 9 deletions docs/modules/object_keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,21 @@ Manage Linode Object Storage Keys.
```

```yaml
- name: Create a limited Object Storage key
- name: Create an Object Storage key limited to specific regions
linode.cloud.object_keys:
label: 'my-region-limited-key'
regions:
- us-mia
- us-ord
state: present
```

```yaml
- name: Create an Object Storage key limited to specific buckets
linode.cloud.object_keys:
label: 'my-limited-key'
access:
- cluster: us-east-1
- cluster: us-mia
bucket_name: my-bucket
permissions: read_write
state: present
Expand All @@ -47,14 +57,16 @@ Manage Linode Object Storage Keys.
| `state` | <center>`str`</center> | <center>**Required**</center> | The desired state of the target. **(Choices: `present`, `absent`)** |
| `label` | <center>`str`</center> | <center>Optional</center> | The unique label to give this key. |
| [`access` (sub-options)](#access) | <center>`list`</center> | <center>Optional</center> | A list of access permissions to give the key. |
| `regions` | <center>`list`</center> | <center>Optional</center> | A list of regions to scope this key to. **(Updatable)** |

### access

| Field | Type | Required | Description |
|-----------|------|----------|------------------------------------------------------------------------------|
| `cluster` | <center>`str`</center> | <center>**Required**</center> | The id of the cluster that the provided bucket exists under. **NOTE: This field has been deprecated because it relies on deprecated API endpoints. Going forward, `region` will be the preferred way to designate where Object Storage resources should be created.** |
| `bucket_name` | <center>`str`</center> | <center>**Required**</center> | The name of the bucket to set the key's permissions for. |
| `permissions` | <center>`str`</center> | <center>**Required**</center> | The permissions to give the key. **(Choices: `read_only`, `write_only`, `read_write`)** |
| `region` | <center>`str`</center> | <center>Optional</center> | The region of the cluster that the provided bucket exists under. **(Conflicts With: `cluster`)** |
| `cluster` | <center>`str`</center> | <center>Optional</center> | The id of the cluster that the provided bucket exists under. **NOTE: This field has been deprecated because it relies on deprecated API endpoints. Going forward, `region` will be the preferred way to designate where Object Storage resources should be created.** **(Conflicts With: `region`)** |

## Return Values

Expand All @@ -63,18 +75,33 @@ Manage Linode Object Storage Keys.
- Sample Response:
```json
{
"access_key": "ACCESSKEY",
"access_key": "redacted",
"bucket_access": [
{
"bucket_name": "example-bucket",
"cluster": "ap-south-1",
"permissions": "read_only"
"bucket_name": "my-bucket",
"cluster": "us-iad-1",
"permissions": "read_write",
"region": "us-iad"
}
],
"id": 123,
"id": 12345,
"label": "my-key",
"limited": true,
"secret_key": "SECRETKEY"
"regions": [
{
"id": "us-iad",
"s3_endpoint": "us-iad-1.linodeobjects.com"
},
{
"id": "us-ord",
"s3_endpoint": "us-ord-1.linodeobjects.com"
},
{
"id": "us-sea",
"s3_endpoint": "us-sea-1.linodeobjects.com"
}
],
"secret_key": "[REDACTED]"
}
```
- See the [Linode API response documentation](https://www.linode.com/docs/api/object-storage/#object-storage-key-view__responses) for a list of returned fields
Expand Down
38 changes: 30 additions & 8 deletions plugins/module_utils/doc_fragments/object_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@
linode.cloud.object_keys:
label: 'my-fullaccess-key'
state: present''', '''
- name: Create a limited Object Storage key
- name: Create an Object Storage key limited to specific regions
linode.cloud.object_keys:
label: 'my-region-limited-key'
regions:
- us-mia
- us-ord
state: present''', '''
- name: Create an Object Storage key limited to specific buckets
linode.cloud.object_keys:
label: 'my-limited-key'
access:
- cluster: us-east-1
- cluster: us-mia
bucket_name: my-bucket
permissions: read_write
state: present''', '''
Expand All @@ -19,16 +26,31 @@
state: absent''']

result_key_samples = ['''{
"access_key": "ACCESSKEY",
"access_key": "redacted",
"bucket_access": [
{
"bucket_name": "example-bucket",
"cluster": "ap-south-1",
"permissions": "read_only"
"bucket_name": "my-bucket",
"cluster": "us-iad-1",
"permissions": "read_write",
"region": "us-iad"
}
],
"id": 123,
"id": 12345,
"label": "my-key",
"limited": true,
"secret_key": "SECRETKEY"
"regions": [
{
"id": "us-iad",
"s3_endpoint": "us-iad-1.linodeobjects.com"
},
{
"id": "us-ord",
"s3_endpoint": "us-ord-1.linodeobjects.com"
},
{
"id": "us-sea",
"s3_endpoint": "us-sea-1.linodeobjects.com"
}
],
"secret_key": "[REDACTED]"
}''']
136 changes: 129 additions & 7 deletions plugins/modules/object_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from __future__ import absolute_import, division, print_function

from typing import Any, List, Optional, Union
from typing import Any, Dict, List, Optional

import ansible_collections.linode.cloud.plugins.module_utils.doc_fragments.object_keys as docs
from ansible_collections.linode.cloud.plugins.module_utils.linode_common import (
Expand All @@ -15,6 +15,9 @@
global_authors,
global_requirements,
)
from ansible_collections.linode.cloud.plugins.module_utils.linode_helper import (
filter_null_values,
)
from ansible_specdoc.objects import (
FieldType,
SpecDocMeta,
Expand All @@ -24,16 +27,23 @@
from linode_api4 import ObjectStorageKeys

linode_access_spec = {
"region": SpecField(
type=FieldType.string,
description=[
"The region of the cluster that the provided bucket exists under."
],
conflicts_with=["cluster"],
),
"cluster": SpecField(
type=FieldType.string,
required=True,
description=[
"The id of the cluster that the provided bucket exists under.",
"**NOTE: This field has been deprecated because it "
+ "relies on deprecated API endpoints. Going forward, `region` will "
+ "be the preferred way to designate where Object Storage resources "
+ "should be created.**",
],
conflicts_with=["region"],
),
"bucket_name": SpecField(
type=FieldType.string,
Expand Down Expand Up @@ -61,6 +71,12 @@
suboptions=linode_access_spec,
description=["A list of access permissions to give the key."],
),
"regions": SpecField(
type=FieldType.list,
element_type=FieldType.string,
description=["A list of regions to scope this key to."],
editable=True,
),
"state": SpecField(
type=FieldType.string,
description=["The desired state of the target."],
Expand Down Expand Up @@ -128,32 +144,138 @@ def _get_key_by_label(self, label: str) -> Optional[ObjectStorageKeys]:
)

def _create_key(
self, label: str, bucket_access: Union[dict, List[dict]]
self,
label: str,
bucket_access: Optional[List[Dict[str, Any]]],
regions: Optional[List[str]],
) -> Optional[ObjectStorageKeys]:
"""Creates an Object Storage key with the given label and access"""

# The API will reject explicit null values for `bucket_access.region`
if bucket_access is not None:
bucket_access = [
filter_null_values(grant) for grant in bucket_access
]

try:
return self.client.object_storage.keys_create(
label, bucket_access=bucket_access
label, bucket_access=bucket_access, regions=regions
)
except Exception as exception:
return self.fail(
msg="failed to create object storage key: {0}".format(exception)
)

@staticmethod
def _access_changed(key: ObjectStorageKeys, params: Dict[str, Any]) -> bool:
Copy link
Contributor Author

@lgarber-akamai lgarber-akamai Jun 27, 2024

Choose a reason for hiding this comment

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

This method contains a lot of logic just to raise an error, but I really wanted to make sure users would be informed if they attempted to update the bucket access configuration of a key

"""
Returns whether the user has made any effective changes to the `access` field.

NOTE: This requires special logic to maintain backwards compatibility
with the `cluster` field.
"""

configured_access = params.get("access")
if configured_access is None:
return False

# Map the region and bucket name to a grant
access = {
(grant.region, grant.bucket_name): grant
for grant in key.bucket_access
}

for configured_grant in configured_access:
configured_region = configured_grant.get("region")
configured_permissions = configured_grant.get("permissions")

# Hack to extract the region from a configured cluster
if configured_region is None:
configured_region = configured_grant.get("cluster").split("-1")[
0
]

grant_key = (configured_region, configured_grant.get("bucket_name"))

grant = access.get(grant_key)
if grant is None or configured_permissions != grant.permissions:
return True

del access[grant_key]

# If true, the user attempted to remove a grant
return len(access) > 0

def _validate_updates(
self, key: ObjectStorageKeys, params: Dict[str, Any]
) -> None:
"""
Raises an error if any invalid update operations are attempted.
"""

if self._access_changed(key, params):
self.fail("`access` is not an updatable field")

def _attempt_update_key(
self, key: ObjectStorageKeys, params: Dict[str, Any]
) -> None:
"""
Attempts to update the given OBJ key.
"""

self._validate_updates(key, params)

put_body = {}

# We can't use handle_updates here because the structure under `regions`
# differs between the request and response
configured_regions = params.get("regions") or []
flattened_regions = set(v.id for v in key.regions)

# Regions from bucket_access will implicitly be added to the
# `regions` attribute, so we should account for that here
for grant in key.bucket_access or []:
configured_regions.append(grant.region)

if (
configured_regions is not None
and set(configured_regions) != flattened_regions
):
put_body["regions"] = configured_regions
self.register_action(
f"Updated regions from {list(flattened_regions)} to {configured_regions}"
)

# Apply changes
if len(put_body) > 0:
self._client.put(
ObjectStorageKeys.api_endpoint, model=key, data=put_body
)

# Refresh the key object
self._key._api_get()

def _handle_key(self) -> None:
"""Updates the key defined in kwargs"""

params = self.module.params
label: str = params.pop("label")
access: dict = params.get("access")
label: str = params.get("label")
access: List[Dict[str, Any]] = params.get("access")
regions: List[str] = params.get("regions")

self._key = self._get_key_by_label(label)

if self._key is None:
self._key = self._create_key(label, bucket_access=access)
self._key = self._create_key(
label, bucket_access=access, regions=regions
)
self.register_action("Created key {0}".format(label))

# NOTE: If the key is refreshed at all after creation,
# make sure you preserve the secret_key :)
else:
self._attempt_update_key(self._key, params)

self.results["key"] = self._key._raw_json

def _handle_key_absent(self) -> None:
Expand Down
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
linode_api4>=5.17.0
# TODO(Multicluster OBJ): Revert before merging to dev
# linode-api4>=5.17.0
git+https://github.com/zliang-akamai/linode_api4-python@zhiwei/bucket-key-update

polling>=0.3.2
types-requests==2.32.0.20240622
ansible-specdoc>=0.0.14
Loading
Loading