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

Add support for Object Storage Gen 2 #503

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
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
40 changes: 40 additions & 0 deletions linode_api4/groups/object_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@

from deprecated import deprecated

from linode_api4 import (
ObjectStorageEndpoint,
ObjectStorageEndpointType,
PaginatedList,
)
from linode_api4.errors import UnexpectedResponseError
from linode_api4.groups import Group
from linode_api4.objects import (
Expand Down Expand Up @@ -272,6 +277,30 @@ def transfer(self):

return MappedObject(**result)

def endpoints(self, *filters) -> PaginatedList:
"""
Returns a paginated list of all Object Storage endpoints available in your account.

This is intended to be called from the :any:`LinodeClient`
class, like this::

endpoints = client.object_storage.endpoints()

API Documentation: https://techdocs.akamai.com/linode-api/reference/get-object-storage-endpoints

:param filters: Any number of filters to apply to this query.
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
for more details on filtering.

:returns: A list of Object Storage Endpoints that matched the query.
:rtype: PaginatedList of ObjectStorageEndpoint
"""
return self.client._get_and_filter(
ObjectStorageEndpoint,
*filters,
endpoint="/object-storage/endpoints",
)

def buckets(self, *filters):
"""
Returns a paginated list of all Object Storage Buckets that you own.
Expand Down Expand Up @@ -299,6 +328,8 @@ def bucket_create(
label: str,
acl: ObjectStorageACL = ObjectStorageACL.PRIVATE,
cors_enabled=False,
s3_endpoint: str = None,
endpoint_type: ObjectStorageEndpointType = None,
):
"""
Creates an Object Storage Bucket in the specified cluster. Accounts with
Expand All @@ -320,6 +351,13 @@ def bucket_create(
should be created.
:type cluster: str

:param endpoint_type: The type of s3_endpoint available to the active user in this region.
:type endpoint_type: str
Enum: E0,E1,E2,E3

:param s3_endpoint: The active user's s3 endpoint URL, based on the endpoint_type and region.
:type s3_endpoint: str

:param cors_enabled: If true, the bucket will be created with CORS enabled for
all origins. For more fine-grained controls of CORS, use
the S3 API directly.
Expand All @@ -346,6 +384,8 @@ def bucket_create(
"label": label,
"acl": acl,
"cors_enabled": cors_enabled,
"s3_endpoint": s3_endpoint,
"endpoint_type": endpoint_type,
}

if self.is_cluster(cluster_or_region_id):
Expand Down
2 changes: 1 addition & 1 deletion linode_api4/objects/linode.py
Original file line number Diff line number Diff line change
Expand Up @@ -1936,7 +1936,7 @@ def _serialize(self):
def _expand_placement_group_assignment(
pg: Union[
InstancePlacementGroupAssignment, "PlacementGroup", Dict[str, Any], int
]
],
) -> Optional[Dict[str, Any]]:
"""
Expands the placement group argument into a dict for use in an API request body.
Expand Down
62 changes: 54 additions & 8 deletions linode_api4/objects/object_storage.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dataclasses import dataclass
from typing import Optional
from urllib import parse

Expand All @@ -11,7 +12,7 @@
Property,
Region,
)
from linode_api4.objects.serializable import StrEnum
from linode_api4.objects.serializable import JSONObject, StrEnum
from linode_api4.util import drop_null_keys


Expand All @@ -28,6 +29,27 @@ class ObjectStorageKeyPermission(StrEnum):
READ_WRITE = "read_write"


class ObjectStorageEndpointType(StrEnum):
E0 = "E0"
E1 = "E1"
E2 = "E2"
E3 = "E3"


@dataclass
class ObjectStorageEndpoint(JSONObject):
"""
ObjectStorageEndpoint contains the core fields of an object storage endpoint object.

NOTE: This is not implemented as a typical API object (Base) because Object Storage Endpoints
cannot be refreshed, as there is no singular GET endpoint.
"""

region: str = ""
endpoint_type: str = ""
s3_endpoint: Optional[str] = None


class ObjectStorageBucket(DerivedBase):
"""
A bucket where objects are stored in.
Expand All @@ -47,6 +69,8 @@ class ObjectStorageBucket(DerivedBase):
"label": Property(identifier=True),
"objects": Property(),
"size": Property(),
"endpoint_type": Property(),
"s3_endpoint": Property(),
}

@classmethod
Expand All @@ -63,13 +87,10 @@ def make_instance(cls, id, client, parent_id=None, json=None):
Override this method to pass in the parent_id from the _raw_json object
when it's available.
"""
if json is None:
return None

cluster_or_region = json.get("region") or json.get("cluster")

if parent_id is None and cluster_or_region:
parent_id = cluster_or_region
if json is not None:
cluster_or_region = json.get("region") or json.get("cluster")
if parent_id is None and cluster_or_region:
parent_id = cluster_or_region

if parent_id:
return super().make(id, client, cls, parent_id=parent_id, json=json)
Expand All @@ -78,6 +99,31 @@ def make_instance(cls, id, client, parent_id=None, json=None):
"Unexpected json response when making a new Object Storage Bucket instance."
)

def access_get(self):
"""
Returns a result object which wraps the current access config for this ObjectStorageBucket.

API Documentation: TODO

:returns: A result object which wraps the access that this ObjectStorageBucket is currently configured with.
:rtype: MappedObject
"""
result = self._client.get(
"{}/access".format(self.api_endpoint),
model=self,
)

if not any(
key in result
for key in ["acl", "acl_xml", "cors_enabled", "cors_xml"]
):
raise UnexpectedResponseError(
"Unexpected response when getting the bucket access config of a bucket!",
json=result,
)

return MappedObject(**result)

def access_modify(
self,
acl: Optional[ObjectStorageACL] = None,
Expand Down
4 changes: 3 additions & 1 deletion test/fixtures/object-storage_buckets_us-east-1.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"hostname": "example-bucket.us-east-1.linodeobjects.com",
"label": "example-bucket",
"objects": 4,
"size": 188318981
"size": 188318981,
"endpoint_type": "E1",
"s3_endpoint": "us-east-12.linodeobjects.com"
}
],
"page": 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@
"hostname": "example-bucket.us-east-1.linodeobjects.com",
"label": "example-bucket",
"objects": 4,
"size": 188318981
"size": 188318981,
"endpoint_type": "E1",
"s3_endpoint": "us-east-12.linodeobjects.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"acl": "authenticated-read",
"acl_xml": "<AccessControlPolicy...",
"cors_enabled": true,
"cors_xml": "<CORSConfiguration>..."
}
4 changes: 2 additions & 2 deletions test/integration/models/image/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ def image_upload_url(test_linode_client):
@pytest.fixture(scope="session")
def test_uploaded_image(test_linode_client):
test_image_content = (
b"\x1F\x8B\x08\x08\xBD\x5C\x91\x60\x00\x03\x74\x65\x73\x74\x2E\x69"
b"\x6D\x67\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x1f\x8b\x08\x08\xbd\x5c\x91\x60\x00\x03\x74\x65\x73\x74\x2e\x69"
b"\x6d\x67\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00"
)

label = get_test_label() + "_image"
Expand Down
75 changes: 68 additions & 7 deletions test/integration/models/object_storage/test_obj.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
ObjectStorageACL,
ObjectStorageBucket,
ObjectStorageCluster,
ObjectStorageEndpointType,
ObjectStorageKeyPermission,
ObjectStorageKeys,
)
Expand All @@ -19,7 +20,14 @@ def region(test_linode_client: LinodeClient):


@pytest.fixture(scope="session")
def bucket(test_linode_client: LinodeClient, region: str):
def endpoints(test_linode_client: LinodeClient):
return test_linode_client.object_storage.endpoints()


@pytest.fixture(scope="session")
def bucket(
test_linode_client: LinodeClient, region: str
) -> ObjectStorageBucket:
bucket = test_linode_client.object_storage.bucket_create(
cluster_or_region=region,
label="bucket-" + str(time.time_ns()),
Expand All @@ -31,6 +39,31 @@ def bucket(test_linode_client: LinodeClient, region: str):
bucket.delete()


@pytest.fixture(scope="session")
def bucket_with_endpoint(
test_linode_client: LinodeClient, endpoints
) -> ObjectStorageBucket:
selected_endpoint = next(
(
e
for e in endpoints
if e.endpoint_type == ObjectStorageEndpointType.E1
),
None,
)

bucket = test_linode_client.object_storage.bucket_create(
cluster_or_region=selected_endpoint.region,
label="bucket-" + str(time.time_ns()),
acl=ObjectStorageACL.PRIVATE,
cors_enabled=False,
endpoint_type=selected_endpoint.endpoint_type,
)

yield bucket
bucket.delete()


@pytest.fixture(scope="session")
def obj_key(test_linode_client: LinodeClient):
key = test_linode_client.object_storage.keys_create(
Expand Down Expand Up @@ -71,19 +104,39 @@ def test_keys(

assert loaded_key.label == obj_key.label
assert loaded_limited_key.label == obj_limited_key.label
assert (
loaded_limited_key.regions[0].endpoint_type
in ObjectStorageEndpointType.__members__.values()
)


def test_bucket(
test_linode_client: LinodeClient,
bucket: ObjectStorageBucket,
):
loaded_bucket = test_linode_client.load(ObjectStorageBucket, bucket.label)
def test_bucket(test_linode_client: LinodeClient, bucket: ObjectStorageBucket):
loaded_bucket = test_linode_client.load(
ObjectStorageBucket,
target_id=bucket.label,
target_parent_id=bucket.region,
)

assert loaded_bucket.label == bucket.label
assert loaded_bucket.region == bucket.region


def test_bucket(
def test_bucket_with_endpoint(
test_linode_client: LinodeClient, bucket_with_endpoint: ObjectStorageBucket
):
loaded_bucket = test_linode_client.load(
ObjectStorageBucket,
target_id=bucket_with_endpoint.label,
target_parent_id=bucket_with_endpoint.region,
)

assert loaded_bucket.label == bucket_with_endpoint.label
assert loaded_bucket.region == bucket_with_endpoint.region
assert loaded_bucket.s3_endpoint is not None
assert loaded_bucket.endpoint_type == "E1"


def test_buckets_in_region(
test_linode_client: LinodeClient,
bucket: ObjectStorageBucket,
region: str,
Expand All @@ -103,6 +156,14 @@ def test_list_obj_storage_bucket(
assert any(target_bucket_id == b.id for b in buckets)


def test_bucket_access_get(bucket: ObjectStorageBucket):
access = bucket.access_get()

assert access.acl is not None
assert access.acl_xml is not None
assert access.cors_enabled is not None


def test_bucket_access_modify(bucket: ObjectStorageBucket):
bucket.access_modify(ObjectStorageACL.PRIVATE, cors_enabled=True)

Expand Down
4 changes: 2 additions & 2 deletions test/unit/objects/image_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

# A minimal gzipped image that will be accepted by the API
TEST_IMAGE_CONTENT = (
b"\x1F\x8B\x08\x08\xBD\x5C\x91\x60\x00\x03\x74\x65\x73\x74\x2E\x69"
b"\x6D\x67\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x1f\x8b\x08\x08\xbd\x5c\x91\x60\x00\x03\x74\x65\x73\x74\x2e\x69"
b"\x6d\x67\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00"
)


Expand Down
Loading
Loading