From df2d8c1f221952fd6b58c3d468708ec1df89c34c Mon Sep 17 00:00:00 2001 From: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:08:56 -0400 Subject: [PATCH 1/8] new: Add support for Linode Disk Encryption (#413) --- linode_api4/groups/linode.py | 18 ++++++- linode_api4/objects/linode.py | 49 +++++++++++++++++- linode_api4/objects/lke.py | 1 + linode_api4/objects/serializable.py | 20 ++++++++ test/fixtures/linode_instances.json | 8 ++- test/fixtures/linode_instances_123_disks.json | 6 ++- ...inode_instances_123_disks_12345_clone.json | 3 +- .../lke_clusters_18881_nodes_123456.json | 2 +- .../lke_clusters_18881_pools_456.json | 3 +- test/integration/helpers.py | 6 ++- test/integration/models/linode/test_linode.py | 50 +++++++++++++++++-- test/integration/models/lke/test_lke.py | 24 +++++++-- test/unit/objects/linode_test.py | 25 ++++++++-- test/unit/objects/lke_test.py | 6 ++- 14 files changed, 197 insertions(+), 24 deletions(-) diff --git a/linode_api4/groups/linode.py b/linode_api4/groups/linode.py index 982eede81..e61edcc9c 100644 --- a/linode_api4/groups/linode.py +++ b/linode_api4/groups/linode.py @@ -1,8 +1,9 @@ import base64 import os from collections.abc import Iterable +from typing import Optional, Union -from linode_api4 import Profile +from linode_api4 import InstanceDiskEncryptionType, Profile from linode_api4.common import SSH_KEY_TYPES, load_and_validate_keys from linode_api4.errors import UnexpectedResponseError from linode_api4.groups import Group @@ -131,7 +132,15 @@ def kernels(self, *filters): # create things def instance_create( - self, ltype, region, image=None, authorized_keys=None, **kwargs + self, + ltype, + region, + image=None, + authorized_keys=None, + disk_encryption: Optional[ + Union[InstanceDiskEncryptionType, str] + ] = None, + **kwargs, ): """ Creates a new Linode Instance. This function has several modes of operation: @@ -266,6 +275,8 @@ def instance_create( :type metadata: dict :param firewall: The firewall to attach this Linode to. :type firewall: int or Firewall + :param disk_encryption: The disk encryption policy for this Linode. + :type disk_encryption: InstanceDiskEncryptionType or str :param interfaces: An array of Network Interfaces to add to this Linode’s Configuration Profile. At least one and up to three Interface objects can exist in this array. :type interfaces: list[ConfigInterface] or list[dict[str, Any]] @@ -326,6 +337,9 @@ def instance_create( "authorized_keys": authorized_keys, } + if disk_encryption is not None: + params["disk_encryption"] = str(disk_encryption) + params.update(kwargs) result = self.client.post("/linode/instances", data=params) diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index f459f5918..323b295b1 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -22,12 +22,25 @@ from linode_api4.objects.base import MappedObject from linode_api4.objects.filtering import FilterableAttribute from linode_api4.objects.networking import IPAddress, IPv6Range, VPCIPAddress +from linode_api4.objects.serializable import StrEnum from linode_api4.objects.vpc import VPC, VPCSubnet from linode_api4.paginated_list import PaginatedList PASSWORD_CHARS = string.ascii_letters + string.digits + string.punctuation +class InstanceDiskEncryptionType(StrEnum): + """ + InstanceDiskEncryptionType defines valid values for the + Instance(...).disk_encryption field. + + API Documentation: TODO + """ + + enabled = "enabled" + disabled = "disabled" + + class Backup(DerivedBase): """ A Backup of a Linode Instance. @@ -114,6 +127,7 @@ class Disk(DerivedBase): "filesystem": Property(), "updated": Property(is_datetime=True), "linode_id": Property(identifier=True), + "disk_encryption": Property(), } def duplicate(self): @@ -650,6 +664,8 @@ class Instance(Base): "host_uuid": Property(), "watchdog_enabled": Property(mutable=True), "has_user_data": Property(), + "disk_encryption": Property(), + "lke_cluster_id": Property(), } @property @@ -1343,7 +1359,16 @@ def ip_allocate(self, public=False): i = IPAddress(self._client, result["address"], result) return i - def rebuild(self, image, root_pass=None, authorized_keys=None, **kwargs): + def rebuild( + self, + image, + root_pass=None, + authorized_keys=None, + disk_encryption: Optional[ + Union[InstanceDiskEncryptionType, str] + ] = None, + **kwargs, + ): """ Rebuilding an Instance deletes all existing Disks and Configs and deploys a new :any:`Image` to it. This can be used to reset an existing @@ -1361,6 +1386,8 @@ def rebuild(self, image, root_pass=None, authorized_keys=None, **kwargs): be a single key, or a path to a file containing the key. :type authorized_keys: list or str + :param disk_encryption: The disk encryption policy for this Linode. + :type disk_encryption: InstanceDiskEncryptionType or str :returns: The newly generated password, if one was not provided (otherwise True) @@ -1378,6 +1405,10 @@ def rebuild(self, image, root_pass=None, authorized_keys=None, **kwargs): "root_pass": root_pass, "authorized_keys": authorized_keys, } + + if disk_encryption is not None: + params["disk_encryption"] = str(disk_encryption) + params.update(kwargs) result = self._client.post( @@ -1683,6 +1714,22 @@ def stats(self): "{}/stats".format(Instance.api_endpoint), model=self ) + @property + def lke_cluster(self) -> Optional["LKECluster"]: + """ + Returns the LKE Cluster this Instance is a node of. + + :returns: The LKE Cluster this Instance is a node of. + :rtype: Optional[LKECluster] + """ + + # Local import to prevent circular dependency + from linode_api4.objects.lke import ( # pylint: disable=import-outside-toplevel + LKECluster, + ) + + return LKECluster(self._client, self.lke_cluster_id) + def stats_for(self, dt): """ Returns stats for the month containing the given datetime diff --git a/linode_api4/objects/lke.py b/linode_api4/objects/lke.py index 55dd0372e..6c21dbf1d 100644 --- a/linode_api4/objects/lke.py +++ b/linode_api4/objects/lke.py @@ -127,6 +127,7 @@ class LKENodePool(DerivedBase): "cluster_id": Property(identifier=True), "type": Property(slug_relationship=Type), "disks": Property(), + "disk_encryption": Property(), "count": Property(mutable=True), "nodes": Property( volatile=True diff --git a/linode_api4/objects/serializable.py b/linode_api4/objects/serializable.py index 15494cdce..b0e7a2503 100644 --- a/linode_api4/objects/serializable.py +++ b/linode_api4/objects/serializable.py @@ -1,5 +1,6 @@ import inspect from dataclasses import dataclass +from enum import Enum from types import SimpleNamespace from typing import ( Any, @@ -223,3 +224,22 @@ def __delitem__(self, key): def __len__(self): return len(vars(self)) + + +class StrEnum(str, Enum): + """ + Used for enums that are of type string, which is necessary + for implicit JSON serialization. + + NOTE: Replace this with StrEnum once Python 3.10 has been EOL'd. + See: https://docs.python.org/3/library/enum.html#enum.StrEnum + """ + + def __new__(cls, *values): + value = str(*values) + member = str.__new__(cls, value) + member._value_ = value + return member + + def __str__(self): + return self._value_ diff --git a/test/fixtures/linode_instances.json b/test/fixtures/linode_instances.json index 3d257938d..a809d8926 100644 --- a/test/fixtures/linode_instances.json +++ b/test/fixtures/linode_instances.json @@ -40,7 +40,9 @@ "image": "linode/ubuntu17.04", "tags": ["something"], "host_uuid": "3a3ddd59d9a78bb8de041391075df44de62bfec8", - "watchdog_enabled": true + "watchdog_enabled": true, + "disk_encryption": "disabled", + "lke_cluster_id": null }, { "group": "test", @@ -79,7 +81,9 @@ "image": "linode/debian9", "tags": [], "host_uuid": "3a3ddd59d9a78bb8de041391075df44de62bfec8", - "watchdog_enabled": false + "watchdog_enabled": false, + "disk_encryption": "enabled", + "lke_cluster_id": 18881 } ] } diff --git a/test/fixtures/linode_instances_123_disks.json b/test/fixtures/linode_instances_123_disks.json index eca5079e5..ddfe7f313 100644 --- a/test/fixtures/linode_instances_123_disks.json +++ b/test/fixtures/linode_instances_123_disks.json @@ -10,7 +10,8 @@ "id": 12345, "updated": "2017-01-01T00:00:00", "label": "Ubuntu 17.04 Disk", - "created": "2017-01-01T00:00:00" + "created": "2017-01-01T00:00:00", + "disk_encryption": "disabled" }, { "size": 512, @@ -19,7 +20,8 @@ "id": 12346, "updated": "2017-01-01T00:00:00", "label": "512 MB Swap Image", - "created": "2017-01-01T00:00:00" + "created": "2017-01-01T00:00:00", + "disk_encryption": "disabled" } ] } diff --git a/test/fixtures/linode_instances_123_disks_12345_clone.json b/test/fixtures/linode_instances_123_disks_12345_clone.json index 2d378edca..899833e56 100644 --- a/test/fixtures/linode_instances_123_disks_12345_clone.json +++ b/test/fixtures/linode_instances_123_disks_12345_clone.json @@ -5,6 +5,7 @@ "id": 12345, "updated": "2017-01-01T00:00:00", "label": "Ubuntu 17.04 Disk", - "created": "2017-01-01T00:00:00" + "created": "2017-01-01T00:00:00", + "disk_encryption": "disabled" } \ No newline at end of file diff --git a/test/fixtures/lke_clusters_18881_nodes_123456.json b/test/fixtures/lke_clusters_18881_nodes_123456.json index 311ef3878..646b62f5d 100644 --- a/test/fixtures/lke_clusters_18881_nodes_123456.json +++ b/test/fixtures/lke_clusters_18881_nodes_123456.json @@ -1,5 +1,5 @@ { "id": "123456", - "instance_id": 123458, + "instance_id": 456, "status": "ready" } \ No newline at end of file diff --git a/test/fixtures/lke_clusters_18881_pools_456.json b/test/fixtures/lke_clusters_18881_pools_456.json index ec6b570ac..225023d5d 100644 --- a/test/fixtures/lke_clusters_18881_pools_456.json +++ b/test/fixtures/lke_clusters_18881_pools_456.json @@ -23,5 +23,6 @@ "example tag", "another example" ], - "type": "g6-standard-4" + "type": "g6-standard-4", + "disk_encryption": "enabled" } \ No newline at end of file diff --git a/test/integration/helpers.py b/test/integration/helpers.py index 5e9d1c441..e0aab06c4 100644 --- a/test/integration/helpers.py +++ b/test/integration/helpers.py @@ -79,12 +79,14 @@ def wait_for_condition( # Retry function to help in case of requests sending too quickly before instance is ready -def retry_sending_request(retries: int, condition: Callable, *args) -> object: +def retry_sending_request( + retries: int, condition: Callable, *args, **kwargs +) -> object: curr_t = 0 while curr_t < retries: try: curr_t += 1 - res = condition(*args) + res = condition(*args, **kwargs) return res except ApiError: if curr_t >= retries: diff --git a/test/integration/models/linode/test_linode.py b/test/integration/models/linode/test_linode.py index a749baad4..1621b9f07 100644 --- a/test/integration/models/linode/test_linode.py +++ b/test/integration/models/linode/test_linode.py @@ -1,4 +1,5 @@ import time +from test.integration.conftest import get_region from test.integration.helpers import ( get_test_label, retry_sending_request, @@ -19,7 +20,7 @@ Instance, Type, ) -from linode_api4.objects.linode import MigrationType +from linode_api4.objects.linode import InstanceDiskEncryptionType, MigrationType @pytest.fixture(scope="session") @@ -137,6 +138,30 @@ def create_linode_for_long_running_tests(test_linode_client): linode_instance.delete() +@pytest.fixture(scope="function") +def linode_with_disk_encryption(test_linode_client, request): + client = test_linode_client + + target_region = get_region(client, {"Disk Encryption"}) + timestamp = str(time.time_ns()) + label = "TestSDK-" + timestamp + + disk_encryption = request.param + + linode_instance, password = client.linode.instance_create( + "g6-nanode-1", + target_region, + image="linode/ubuntu23.04", + label=label, + booted=False, + disk_encryption=disk_encryption, + ) + + yield linode_instance + + linode_instance.delete() + + # Test helper def get_status(linode: Instance, status: str): return linode.status == status @@ -165,8 +190,7 @@ def test_linode_transfer(test_linode_client, linode_with_volume_firewall): def test_linode_rebuild(test_linode_client): client = test_linode_client - available_regions = client.regions() - chosen_region = available_regions[4] + chosen_region = get_region(client, {"Disk Encryption"}) label = get_test_label() + "_rebuild" linode, password = client.linode.instance_create( @@ -175,12 +199,18 @@ def test_linode_rebuild(test_linode_client): wait_for_condition(10, 100, get_status, linode, "running") - retry_sending_request(3, linode.rebuild, "linode/debian10") + retry_sending_request( + 3, + linode.rebuild, + "linode/debian10", + disk_encryption=InstanceDiskEncryptionType.disabled, + ) wait_for_condition(10, 100, get_status, linode, "rebuilding") assert linode.status == "rebuilding" assert linode.image.id == "linode/debian10" + assert linode.disk_encryption == InstanceDiskEncryptionType.disabled wait_for_condition(10, 300, get_status, linode, "running") @@ -383,6 +413,18 @@ def test_linode_volumes(linode_with_volume_firewall): assert "test" in volumes[0].label +@pytest.mark.parametrize( + "linode_with_disk_encryption", ["disabled"], indirect=True +) +def test_linode_with_disk_encryption_disabled(linode_with_disk_encryption): + linode = linode_with_disk_encryption + + assert linode.disk_encryption == InstanceDiskEncryptionType.disabled + assert ( + linode.disks[0].disk_encryption == InstanceDiskEncryptionType.disabled + ) + + def wait_for_disk_status(disk: Disk, timeout): start_time = time.time() while True: diff --git a/test/integration/models/lke/test_lke.py b/test/integration/models/lke/test_lke.py index bbf87bedf..a3110f0c1 100644 --- a/test/integration/models/lke/test_lke.py +++ b/test/integration/models/lke/test_lke.py @@ -1,26 +1,30 @@ import re +from test.integration.conftest import get_region from test.integration.helpers import ( get_test_label, send_request_when_resource_available, wait_for_condition, ) +from typing import Any, Dict import pytest from linode_api4 import ( + Instance, + InstanceDiskEncryptionType, LKEClusterControlPlaneACLAddressesOptions, LKEClusterControlPlaneACLOptions, LKEClusterControlPlaneOptions, ) from linode_api4.errors import ApiError -from linode_api4.objects import LKECluster, LKENodePool, LKENodePoolNode +from linode_api4.objects import LKECluster, LKENodePool @pytest.fixture(scope="session") def lke_cluster(test_linode_client): node_type = test_linode_client.linode.types()[1] # g6-standard-1 version = test_linode_client.lke.versions()[0] - region = test_linode_client.regions().first() + region = get_region(test_linode_client, {"Disk Encryption", "Kubernetes"}) node_pools = test_linode_client.lke.node_pool(node_type, 3) label = get_test_label() + "_cluster" @@ -37,7 +41,7 @@ def lke_cluster(test_linode_client): def lke_cluster_with_acl(test_linode_client): node_type = test_linode_client.linode.types()[1] # g6-standard-1 version = test_linode_client.lke.versions()[0] - region = test_linode_client.regions().first() + region = get_region(test_linode_client, {"Kubernetes"}) node_pools = test_linode_client.lke.node_pool(node_type, 1) label = get_test_label() + "_cluster" @@ -80,9 +84,21 @@ def test_get_lke_clusters(test_linode_client, lke_cluster): def test_get_lke_pool(test_linode_client, lke_cluster): cluster = lke_cluster + wait_for_condition( + 10, + 500, + get_node_status, + cluster, + "ready", + ) + pool = test_linode_client.load(LKENodePool, cluster.pools[0].id, cluster.id) - assert cluster.pools[0].id == pool.id + def _to_comparable(p: LKENodePool) -> Dict[str, Any]: + return {k: v for k, v in p._raw_json.items() if k not in {"nodes"}} + + assert _to_comparable(cluster.pools[0]) == _to_comparable(pool) + assert pool.disk_encryption == InstanceDiskEncryptionType.enabled def test_cluster_dashboard_url_view(lke_cluster): diff --git a/test/unit/objects/linode_test.py b/test/unit/objects/linode_test.py index 9759bba41..38c34e1ef 100644 --- a/test/unit/objects/linode_test.py +++ b/test/unit/objects/linode_test.py @@ -1,7 +1,7 @@ from datetime import datetime from test.unit.base import ClientBaseCase -from linode_api4 import NetworkInterface +from linode_api4 import InstanceDiskEncryptionType, NetworkInterface from linode_api4.objects import ( Config, ConfigInterface, @@ -36,6 +36,10 @@ def test_get_linode(self): linode.host_uuid, "3a3ddd59d9a78bb8de041391075df44de62bfec8" ) self.assertEqual(linode.watchdog_enabled, True) + self.assertEqual( + linode.disk_encryption, InstanceDiskEncryptionType.disabled + ) + self.assertEqual(linode.lke_cluster_id, None) json = linode._raw_json self.assertIsNotNone(json) @@ -72,7 +76,10 @@ def test_rebuild(self): linode = Instance(self.client, 123) with self.mock_post("/linode/instances/123") as m: - pw = linode.rebuild("linode/debian9") + pw = linode.rebuild( + "linode/debian9", + disk_encryption=InstanceDiskEncryptionType.enabled, + ) self.assertIsNotNone(pw) self.assertTrue(isinstance(pw, str)) @@ -84,6 +91,7 @@ def test_rebuild(self): { "image": "linode/debian9", "root_pass": pw, + "disk_encryption": "enabled", }, ) @@ -306,6 +314,15 @@ def test_transfer_year_month(self): m.call_url, "/linode/instances/123/transfer/2023/4" ) + def test_lke_cluster(self): + """ + Tests that you can grab the parent LKE cluster from an instance node + """ + linode = Instance(self.client, 456) + + assert linode.lke_cluster_id == 18881 + assert linode.lke_cluster.id == linode.lke_cluster_id + def test_duplicate(self): """ Tests that you can submit a correct disk clone api request @@ -318,6 +335,8 @@ def test_duplicate(self): m.call_url, "/linode/instances/123/disks/12345/clone" ) + assert disk.disk_encryption == InstanceDiskEncryptionType.disabled + def test_disk_password(self): """ Tests that you can submit a correct disk password reset api request @@ -393,7 +412,6 @@ def test_create_disk(self): image="linode/debian10", ) self.assertEqual(m.call_url, "/linode/instances/123/disks") - print(m.call_data) self.assertEqual( m.call_data, { @@ -407,6 +425,7 @@ def test_create_disk(self): ) assert disk.id == 12345 + assert disk.disk_encryption == InstanceDiskEncryptionType.disabled def test_instance_create_with_user_data(self): """ diff --git a/test/unit/objects/lke_test.py b/test/unit/objects/lke_test.py index a44db97ef..f39fb84ae 100644 --- a/test/unit/objects/lke_test.py +++ b/test/unit/objects/lke_test.py @@ -1,6 +1,7 @@ from datetime import datetime from test.unit.base import ClientBaseCase +from linode_api4 import InstanceDiskEncryptionType from linode_api4.objects import ( LKECluster, LKEClusterControlPlaneACLAddressesOptions, @@ -47,6 +48,9 @@ def test_get_pool(self): self.assertEqual(pool.id, 456) self.assertEqual(pool.cluster_id, 18881) self.assertEqual(pool.type.id, "g6-standard-4") + self.assertEqual( + pool.disk_encryption, InstanceDiskEncryptionType.enabled + ) self.assertIsNotNone(pool.disks) self.assertIsNotNone(pool.nodes) self.assertIsNotNone(pool.autoscaler) @@ -84,7 +88,7 @@ def test_node_view(self): self.assertEqual(m.call_url, "/lke/clusters/18881/nodes/123456") self.assertIsNotNone(node) self.assertEqual(node.id, "123456") - self.assertEqual(node.instance_id, 123458) + self.assertEqual(node.instance_id, 456) self.assertEqual(node.status, "ready") def test_node_delete(self): From 8fe7c51d022485a1eb42590429bd63d0485eff8f Mon Sep 17 00:00:00 2001 From: Erik Zilber Date: Wed, 26 Jun 2024 13:04:17 -0400 Subject: [PATCH 2/8] Added note in documentation that Parent/Child support may not yet be generally available (#427) --- linode_api4/groups/account.py | 2 ++ linode_api4/objects/account.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/linode_api4/groups/account.py b/linode_api4/groups/account.py index b45152908..21540ea7f 100644 --- a/linode_api4/groups/account.py +++ b/linode_api4/groups/account.py @@ -502,6 +502,8 @@ def child_accounts(self, *filters): """ Returns a list of all child accounts under the this parent account. + NOTE: Parent/Child related features may not be generally available. + API doc: TBD :returns: a list of all child accounts. diff --git a/linode_api4/objects/account.py b/linode_api4/objects/account.py index aa0a8f57a..8c5ad098f 100644 --- a/linode_api4/objects/account.py +++ b/linode_api4/objects/account.py @@ -60,6 +60,8 @@ class ChildAccount(Account): """ A child account under a parent account. + NOTE: Parent/Child related features may not be generally available. + API Documentation: TBD """ From 53a2c074fa9da30576d03f6055aca91b76cc4bfe Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Tue, 2 Jul 2024 14:22:07 -0700 Subject: [PATCH 3/8] test: Add error handling in e2e_cloud_firewall fixture (#431) * Add some safety to e2e_test_firewall fxiture for ConnectionError * delete comment * Update test/integration/conftest.py Co-authored-by: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> --------- Co-authored-by: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> --- test/integration/conftest.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/test/integration/conftest.py b/test/integration/conftest.py index c9eab20eb..697f33a59 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -6,6 +6,7 @@ import pytest import requests +from requests.exceptions import ConnectionError, RequestException from linode_api4 import ApiError, PlacementGroupAffinityType from linode_api4.linode_client import LinodeClient @@ -68,14 +69,22 @@ def is_valid_ipv6(address): except ipaddress.AddressValueError: return False - def get_public_ip(ip_version="ipv4"): + def get_public_ip(ip_version: str = "ipv4", retries: int = 3): url = ( f"https://api64.ipify.org?format=json" if ip_version == "ipv6" else f"https://api.ipify.org?format=json" ) - response = requests.get(url) - return str(response.json()["ip"]) + for attempt in range(retries): + try: + response = requests.get(url) + response.raise_for_status() + return str(response.json()["ip"]) + except (RequestException, ConnectionError) as e: + if attempt < retries - 1: + time.sleep(2) # Wait before retrying + else: + raise e def create_inbound_rule(ipv4_address, ipv6_address): rule = [ @@ -94,12 +103,19 @@ def create_inbound_rule(ipv4_address, ipv6_address): return rule - # Fetch the public IP addresses + try: + ipv4_address = get_public_ip("ipv4") + except (RequestException, ConnectionError, ValueError, KeyError): + ipv4_address = None - ipv4_address = get_public_ip("ipv4") - ipv6_address = get_public_ip("ipv6") + try: + ipv6_address = get_public_ip("ipv6") + except (RequestException, ConnectionError, ValueError, KeyError): + ipv6_address = None - inbound_rule = create_inbound_rule(ipv4_address, ipv6_address) + inbound_rule = [] + if ipv4_address or ipv6_address: + inbound_rule = create_inbound_rule(ipv4_address, ipv6_address) client = test_linode_client From 4bc4a21bfb3be367a08a97eb259bd1e880816d0f Mon Sep 17 00:00:00 2001 From: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> Date: Fri, 5 Jul 2024 19:47:34 -0400 Subject: [PATCH 4/8] project: MultiCluster Object Storage (#416) * Add warnings to deprecated OBJ API usage (#410) * add deprecated * add dependency * fix deprecated * Update OBJ Group and Objects for MultiCluster Object Storage API Changes (#426) --------- Co-authored-by: Ye Chen <127243817+yec-akamai@users.noreply.github.com> --- linode_api4/groups/object_storage.py | 181 ++++++++++++++---- linode_api4/objects/object_storage.py | 42 +++- pyproject.toml | 2 +- ...rage_buckets_us-east-1_example-bucket.json | 1 + test/fixtures/object-storage_keys.json | 32 +++- .../models/object_storage/test_obj.py | 92 +++++++++ test/unit/groups/__init__.py | 0 test/unit/groups/object_storage_test.py | 20 ++ test/unit/linode_client_test.py | 35 ++++ test/unit/objects/object_storage_test.py | 4 +- 10 files changed, 362 insertions(+), 47 deletions(-) create mode 100644 test/integration/models/object_storage/test_obj.py create mode 100644 test/unit/groups/__init__.py create mode 100644 test/unit/groups/object_storage_test.py diff --git a/linode_api4/groups/object_storage.py b/linode_api4/groups/object_storage.py index bbaf330d9..c42805ec1 100644 --- a/linode_api4/groups/object_storage.py +++ b/linode_api4/groups/object_storage.py @@ -1,6 +1,10 @@ +import re +import warnings from typing import List, Optional, Union from urllib import parse +from deprecated import deprecated + from linode_api4.errors import UnexpectedResponseError from linode_api4.groups import Group from linode_api4.objects import ( @@ -9,6 +13,7 @@ ObjectStorageACL, ObjectStorageBucket, ObjectStorageCluster, + ObjectStorageKeyPermission, ObjectStorageKeys, ) from linode_api4.util import drop_null_keys @@ -20,8 +25,14 @@ class ObjectStorageGroup(Group): available clusters, buckets, and managing keys and TLS/SSL certs, etc. """ + @deprecated( + reason="deprecated to use regions list API for listing available OJB clusters" + ) def clusters(self, *filters): """ + This endpoint will be deprecated to use the regions list API to list available OBJ clusters, + and a new access key API will directly expose the S3 endpoint hostname. + Returns a list of available Object Storage Clusters. You may filter this query to return only Clusters that are available in a specific region:: @@ -58,6 +69,7 @@ def keys_create( self, label: str, bucket_access: Optional[Union[dict, List[dict]]] = None, + regions: Optional[List[str]] = None, ): """ Creates a new Object Storage keypair that may be used to interact directly @@ -97,14 +109,16 @@ def keys_create( :param label: The label for this keypair, for identification only. :type label: str - :param bucket_access: One or a list of dicts with keys "cluster," - "permissions", and "bucket_name". If given, the - resulting Object Storage keys will only have the - requested level of access to the requested buckets, - if they exist and are owned by you. See the provided - :any:`bucket_access` function for a convenient way - to create these dicts. - :type bucket_access: dict or list of dict + :param bucket_access: One or a list of dicts with keys "cluster," "region", + "permissions", and "bucket_name". "cluster" key is + deprecated because multiple cluster can be placed + in the same region. Please consider switching to + regions. If given, the resulting Object Storage keys + will only have the requested level of access to the + requested buckets, if they exist and are owned by + you. See the provided :any:`bucket_access` function + for a convenient way to create these dicts. + :type bucket_access: Optional[Union[dict, List[dict]]] :returns: The new keypair, with the secret key populated. :rtype: ObjectStorageKeys @@ -115,22 +129,35 @@ def keys_create( if not isinstance(bucket_access, list): bucket_access = [bucket_access] - ba = [ - { - "permissions": c.get("permissions"), - "bucket_name": c.get("bucket_name"), - "cluster": ( - c.id - if "cluster" in c - and issubclass(type(c["cluster"]), Base) - else c.get("cluster") - ), + ba = [] + for access_rule in bucket_access: + access_rule_json = { + "permissions": access_rule.get("permissions"), + "bucket_name": access_rule.get("bucket_name"), } - for c in bucket_access - ] + + if "region" in access_rule: + access_rule_json["region"] = access_rule.get("region") + elif "cluster" in access_rule: + warnings.warn( + "'cluster' is a deprecated attribute, " + "please consider using 'region' instead.", + DeprecationWarning, + ) + access_rule_json["cluster"] = ( + access_rule.id + if "cluster" in access_rule + and issubclass(type(access_rule["cluster"]), Base) + else access_rule.get("cluster") + ) + + ba.append(access_rule_json) params["bucket_access"] = ba + if regions is not None: + params["regions"] = regions + result = self.client.post("/object-storage/keys", data=params) if not "id" in result: @@ -142,9 +169,74 @@ def keys_create( ret = ObjectStorageKeys(self.client, result["id"], result) return ret - def bucket_access(self, cluster, bucket_name, permissions): - return ObjectStorageBucket.access( - self, cluster, bucket_name, permissions + @classmethod + def bucket_access( + cls, + cluster_or_region: str, + bucket_name: str, + permissions: Union[str, ObjectStorageKeyPermission], + ): + """ + Returns a dict formatted to be included in the `bucket_access` argument + of :any:`keys_create`. See the docs for that method for an example of + usage. + + :param cluster_or_region: The region or Object Storage cluster to grant access in. + :type cluster_or_region: str + :param bucket_name: The name of the bucket to grant access to. + :type bucket_name: str + :param permissions: The permissions to grant. Should be one of "read_only" + or "read_write". + :type permissions: Union[str, ObjectStorageKeyPermission] + :param use_region: Whether to use region mode. + :type use_region: bool + + :returns: A dict formatted correctly for specifying bucket access for + new keys. + :rtype: dict + """ + + result = { + "bucket_name": bucket_name, + "permissions": permissions, + } + + if cls.is_cluster(cluster_or_region): + warnings.warn( + "Cluster ID for Object Storage APIs has been deprecated. " + "Please consider switch to a region ID (e.g., from `us-mia-1` to `us-mia`)", + DeprecationWarning, + ) + result["cluster"] = cluster_or_region + else: + result["region"] = cluster_or_region + + return result + + def buckets_in_region(self, region: str, *filters): + """ + Returns a list of Buckets in the region belonging to this Account. + + This endpoint is available for convenience. + It is recommended that instead you use the more fully-featured S3 API directly. + + API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-buckets-in-cluster-list + + :param filters: Any number of filters to apply to this query. + See :doc:`Filtering Collections` + for more details on filtering. + + :param region: The ID of an object storage region (e.g. `us-mia-1`). + :type region: str + + :returns: A list of Object Storage Buckets that in the requested cluster. + :rtype: PaginatedList of ObjectStorageBucket + """ + + return self.client._get_and_filter( + ObjectStorageBucket, + *filters, + endpoint=f"/object-storage/buckets/{region}", ) def cancel(self): @@ -197,10 +289,14 @@ def buckets(self, *filters): """ return self.client._get_and_filter(ObjectStorageBucket, *filters) + @staticmethod + def is_cluster(cluster_or_region: str): + return bool(re.match(r"^[a-z]{2}-[a-z]+-[0-9]+$", cluster_or_region)) + def bucket_create( self, - cluster, - label, + cluster_or_region: Union[str, ObjectStorageCluster], + label: str, acl: ObjectStorageACL = ObjectStorageACL.PRIVATE, cors_enabled=False, ): @@ -240,17 +336,30 @@ def bucket_create( :returns: A Object Storage Buckets that created by user. :rtype: ObjectStorageBucket """ - cluster_id = ( - cluster.id if isinstance(cluster, ObjectStorageCluster) else cluster + cluster_or_region_id = ( + cluster_or_region.id + if isinstance(cluster_or_region, ObjectStorageCluster) + else cluster_or_region ) params = { - "cluster": cluster_id, "label": label, "acl": acl, "cors_enabled": cors_enabled, } + if self.is_cluster(cluster_or_region_id): + warnings.warn( + "The cluster parameter has been deprecated for creating a object " + "storage bucket. Please consider switching to a region value. For " + "example, a cluster value of `us-mia-1` can be translated to a " + "region value of `us-mia`.", + DeprecationWarning, + ) + params["cluster"] = cluster_or_region_id + else: + params["region"] = cluster_or_region_id + result = self.client.post("/object-storage/buckets", data=params) if not "label" in result or not "cluster" in result: @@ -263,21 +372,21 @@ def bucket_create( self.client, result["label"], result["cluster"], result ) - def object_acl_config(self, cluster_id, bucket, name=None): + def object_acl_config(self, cluster_or_region_id: str, bucket, name=None): return ObjectStorageBucket( - self.client, bucket, cluster_id + self.client, bucket, cluster_or_region_id ).object_acl_config(name) def object_acl_config_update( - self, cluster_id, bucket, acl: ObjectStorageACL, name + self, cluster_or_region_id, bucket, acl: ObjectStorageACL, name ): return ObjectStorageBucket( - self.client, bucket, cluster_id + self.client, bucket, cluster_or_region_id ).object_acl_config_update(acl, name) def object_url_create( self, - cluster_id, + cluster_or_region_id, bucket, method, name, @@ -294,8 +403,8 @@ def object_url_create( API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-object-url-create - :param cluster_id: The ID of the cluster this bucket exists in. - :type cluster_id: str + :param cluster_or_region_id: The ID of the cluster or region this bucket exists in. + :type cluster_or_region_id: str :param bucket: The bucket name. :type bucket: str @@ -337,7 +446,7 @@ def object_url_create( result = self.client.post( "/object-storage/buckets/{}/{}/object-url".format( - parse.quote(str(cluster_id)), parse.quote(str(bucket)) + parse.quote(str(cluster_or_region_id)), parse.quote(str(bucket)) ), data=drop_null_keys(params), ) diff --git a/linode_api4/objects/object_storage.py b/linode_api4/objects/object_storage.py index 685925c9b..2cbcf59bd 100644 --- a/linode_api4/objects/object_storage.py +++ b/linode_api4/objects/object_storage.py @@ -1,6 +1,8 @@ from typing import Optional from urllib import parse +from deprecated import deprecated + from linode_api4.errors import UnexpectedResponseError from linode_api4.objects import ( Base, @@ -21,6 +23,11 @@ class ObjectStorageACL(StrEnum): CUSTOM = "custom" +class ObjectStorageKeyPermission(StrEnum): + READ_ONLY = "read_only" + READ_WRITE = "read_write" + + class ObjectStorageBucket(DerivedBase): """ A bucket where objects are stored in. @@ -28,12 +35,13 @@ class ObjectStorageBucket(DerivedBase): API documentation: https://www.linode.com/docs/api/object-storage/#object-storage-bucket-view """ - api_endpoint = "/object-storage/buckets/{cluster}/{label}" - parent_id_name = "cluster" + api_endpoint = "/object-storage/buckets/{region}/{label}" + parent_id_name = "region" id_attribute = "label" properties = { - "cluster": Property(identifier=True), + "region": Property(identifier=True), + "cluster": Property(), "created": Property(is_datetime=True), "hostname": Property(), "label": Property(identifier=True), @@ -57,8 +65,11 @@ def make_instance(cls, id, client, parent_id=None, json=None): """ if json is None: return None - if parent_id is None and json["cluster"]: - parent_id = json["cluster"] + + 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) @@ -386,6 +397,13 @@ def object_acl_config_update(self, acl: ObjectStorageACL, name): return MappedObject(**result) + @deprecated( + reason=( + "'access' method has been deprecated in favor of the class method " + "'bucket_access' in ObjectStorageGroup, which can be accessed by " + "'client.object_storage.access'" + ) + ) def access(self, cluster, bucket_name, permissions): """ Returns a dict formatted to be included in the `bucket_access` argument @@ -411,8 +429,14 @@ def access(self, cluster, bucket_name, permissions): } +@deprecated( + reason="deprecated to use regions list API for viewing available OJB clusters" +) class ObjectStorageCluster(Base): """ + This class will be deprecated to use the regions list to view available OBJ clusters, + and a new access key API will directly expose the S3 endpoint hostname. + A cluster where Object Storage is available. API documentation: https://www.linode.com/docs/api/object-storage/#cluster-view @@ -428,6 +452,13 @@ class ObjectStorageCluster(Base): "static_site_domain": Property(), } + @deprecated( + reason=( + "'buckets_in_cluster' method has been deprecated, please consider " + "switching to 'buckets_in_region' in the object storage group (can " + "be accessed via 'client.object_storage.buckets_in_cluster')." + ) + ) def buckets_in_cluster(self, *filters): """ Returns a list of Buckets in this cluster belonging to this Account. @@ -470,4 +501,5 @@ class ObjectStorageKeys(Base): "secret_key": Property(), "bucket_access": Property(), "limited": Property(), + "regions": Property(unordered=True), } diff --git a/pyproject.toml b/pyproject.toml index 4e2c60f00..ea96865c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] -dependencies = ["requests", "polling"] +dependencies = ["requests", "polling", "deprecated"] dynamic = ["version"] [project.optional-dependencies] diff --git a/test/fixtures/object-storage_buckets_us-east-1_example-bucket.json b/test/fixtures/object-storage_buckets_us-east-1_example-bucket.json index b8c9450b6..bb93ec99a 100644 --- a/test/fixtures/object-storage_buckets_us-east-1_example-bucket.json +++ b/test/fixtures/object-storage_buckets_us-east-1_example-bucket.json @@ -1,5 +1,6 @@ { "cluster": "us-east-1", + "region": "us-east", "created": "2019-01-01T01:23:45", "hostname": "example-bucket.us-east-1.linodeobjects.com", "label": "example-bucket", diff --git a/test/fixtures/object-storage_keys.json b/test/fixtures/object-storage_keys.json index da6c2278a..0a9181658 100644 --- a/test/fixtures/object-storage_keys.json +++ b/test/fixtures/object-storage_keys.json @@ -6,14 +6,40 @@ "id": 1, "label": "object-storage-key-1", "secret_key": "[REDACTED]", - "access_key": "testAccessKeyHere123" + "access_key": "testAccessKeyHere123", + "limited": false, + "regions": [ + { + "id": "us-east", + "s3_endpoint": "us-east-1.linodeobjects.com" + }, + { + "id": "us-west", + "s3_endpoint": "us-west-123.linodeobjects.com" + } + ] }, { "id": 2, "label": "object-storage-key-2", "secret_key": "[REDACTED]", - "access_key": "testAccessKeyHere456" + "access_key": "testAccessKeyHere456", + "limited": true, + "bucket_access": [ + { + "cluster": "us-mia-1", + "bucket_name": "example-bucket", + "permissions": "read_only", + "region": "us-mia" + } + ], + "regions": [ + { + "id": "us-mia", + "s3_endpoint": "us-mia-1.linodeobjects.com" + } + ] } ], "page": 1 -} +} \ No newline at end of file diff --git a/test/integration/models/object_storage/test_obj.py b/test/integration/models/object_storage/test_obj.py new file mode 100644 index 000000000..863eda129 --- /dev/null +++ b/test/integration/models/object_storage/test_obj.py @@ -0,0 +1,92 @@ +import time +from test.integration.conftest import get_region + +import pytest + +from linode_api4.linode_client import LinodeClient +from linode_api4.objects.object_storage import ( + ObjectStorageACL, + ObjectStorageBucket, + ObjectStorageKeyPermission, + ObjectStorageKeys, +) + + +@pytest.fixture(scope="session") +def region(test_linode_client: LinodeClient): + return get_region(test_linode_client, {"Object Storage"}).id + + +@pytest.fixture(scope="session") +def bucket(test_linode_client: LinodeClient, region: str): + bucket = test_linode_client.object_storage.bucket_create( + cluster_or_region=region, + label="bucket-" + str(time.time_ns()), + acl=ObjectStorageACL.PRIVATE, + cors_enabled=False, + ) + + yield bucket + bucket.delete() + + +@pytest.fixture(scope="session") +def obj_key(test_linode_client: LinodeClient): + key = test_linode_client.object_storage.keys_create( + label="obj-key-" + str(time.time_ns()), + ) + + yield key + key.delete() + + +@pytest.fixture(scope="session") +def obj_limited_key( + test_linode_client: LinodeClient, region: str, bucket: ObjectStorageBucket +): + key = test_linode_client.object_storage.keys_create( + label="obj-limited-key-" + str(time.time_ns()), + bucket_access=test_linode_client.object_storage.bucket_access( + cluster_or_region=region, + bucket_name=bucket.label, + permissions=ObjectStorageKeyPermission.READ_ONLY, + ), + regions=[region], + ) + + yield key + key.delete() + + +def test_keys( + test_linode_client: LinodeClient, + obj_key: ObjectStorageKeys, + obj_limited_key: ObjectStorageKeys, +): + loaded_key = test_linode_client.load(ObjectStorageKeys, obj_key.id) + loaded_limited_key = test_linode_client.load( + ObjectStorageKeys, obj_limited_key.id + ) + + assert loaded_key.label == obj_key.label + assert loaded_limited_key.label == obj_limited_key.label + + +def test_bucket( + test_linode_client: LinodeClient, + bucket: ObjectStorageBucket, +): + loaded_bucket = test_linode_client.load(ObjectStorageBucket, bucket.label) + + assert loaded_bucket.label == bucket.label + assert loaded_bucket.region == bucket.region + + +def test_bucket( + test_linode_client: LinodeClient, + bucket: ObjectStorageBucket, + region: str, +): + buckets = test_linode_client.object_storage.buckets_in_region(region=region) + assert len(buckets) >= 1 + assert any(b.label == bucket.label for b in buckets) diff --git a/test/unit/groups/__init__.py b/test/unit/groups/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/groups/object_storage_test.py b/test/unit/groups/object_storage_test.py new file mode 100644 index 000000000..31c931498 --- /dev/null +++ b/test/unit/groups/object_storage_test.py @@ -0,0 +1,20 @@ +import pytest + +from linode_api4.groups.object_storage import ObjectStorageGroup + + +@pytest.mark.parametrize( + "cluster_or_region,is_cluster", + [ + ("us-east-1", True), + ("us-central-1", True), + ("us-mia-1", True), + ("us-iad-123", True), + ("us-east", False), + ("us-central", False), + ("us-mia", False), + ("us-iad", False), + ], +) +def test_is_cluster(cluster_or_region: str, is_cluster: bool): + assert ObjectStorageGroup.is_cluster(cluster_or_region) == is_cluster diff --git a/test/unit/linode_client_test.py b/test/unit/linode_client_test.py index 3facd2e95..081b27d09 100644 --- a/test/unit/linode_client_test.py +++ b/test/unit/linode_client_test.py @@ -980,6 +980,41 @@ def test_keys_create(self): self.assertEqual(m.call_url, "/object-storage/keys") self.assertEqual(m.call_data, {"label": "object-storage-key-1"}) + def test_limited_keys_create(self): + """ + Tests that you can create Object Storage Keys + """ + with self.mock_post("object-storage/keys/2") as m: + keys = self.client.object_storage.keys_create( + "object-storage-key-1", + self.client.object_storage.bucket_access( + "us-east", + "example-bucket", + "read_only", + ), + ["us-east"], + ) + + self.assertIsNotNone(keys) + self.assertEqual(keys.id, 2) + self.assertEqual(keys.label, "object-storage-key-2") + + self.assertEqual(m.call_url, "/object-storage/keys") + self.assertEqual( + m.call_data, + { + "label": "object-storage-key-1", + "bucket_access": [ + { + "permissions": "read_only", + "bucket_name": "example-bucket", + "region": "us-east", + } + ], + "regions": ["us-east"], + }, + ) + def test_transfer(self): """ Test that you can get the amount of outbound data transfer diff --git a/test/unit/objects/object_storage_test.py b/test/unit/objects/object_storage_test.py index 59317afa1..95d781a84 100644 --- a/test/unit/objects/object_storage_test.py +++ b/test/unit/objects/object_storage_test.py @@ -53,11 +53,11 @@ def test_bucket_access_modify(self): Test that you can modify bucket access settings. """ bucket_access_modify_url = ( - "/object-storage/buckets/us-east-1/example-bucket/access" + "/object-storage/buckets/us-east/example-bucket/access" ) with self.mock_post({}) as m: object_storage_bucket = ObjectStorageBucket( - self.client, "example-bucket", "us-east-1" + self.client, "example-bucket", "us-east" ) object_storage_bucket.access_modify(ObjectStorageACL.PRIVATE, True) self.assertEqual( From 2d9a09e5e55c6ae9a4d102323d8b1551975e04ac Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Sun, 7 Jul 2024 12:59:52 -0600 Subject: [PATCH 5/8] test: Add missing integration tests to improve coverage for identified gaps (#429) * Fill in test gaps in models and improve fixtures * address pr comment and add one more test * make format --- test/integration/conftest.py | 13 ++- .../linode_client/test_linode_client.py | 28 +---- .../login_client/test_login_client.py | 2 +- .../models/account/test_account.py | 24 +++- test/integration/models/lke/test_lke.py | 11 +- .../models/longview/test_longview.py | 29 ++++- .../models/objectstorage/test_obj_storage.py | 105 ++++++++++++++++++ .../models/profile/test_profile.py | 36 ++++++ 8 files changed, 218 insertions(+), 30 deletions(-) create mode 100644 test/integration/models/objectstorage/test_obj_storage.py create mode 100644 test/integration/models/profile/test_profile.py diff --git a/test/integration/conftest.py b/test/integration/conftest.py index 697f33a59..3638bd57d 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -27,6 +27,13 @@ def get_api_url(): return os.environ.get(ENV_API_URL_NAME, "https://api.linode.com/v4beta") +def get_random_label(): + timestamp = str(time.time_ns())[:-5] + label = "label_" + timestamp + + return label + + def get_region(client: LinodeClient, capabilities: Set[str] = None): region_override = os.environ.get(ENV_REGION_OVERRIDE) @@ -329,7 +336,7 @@ def test_sshkey(test_linode_client, ssh_key_gen): @pytest.fixture -def ssh_keys_object_storage(test_linode_client): +def access_keys_object_storage(test_linode_client): client = test_linode_client label = "TestSDK-obj-storage-key" key = client.object_storage.keys_create(label) @@ -364,8 +371,10 @@ def test_firewall(test_linode_client): @pytest.fixture def test_oauth_client(test_linode_client): client = test_linode_client + label = get_random_label() + "_oauth" + oauth_client = client.account.oauth_client_create( - "test-oauth-client", "https://localhost/oauth/callback" + label, "https://localhost/oauth/callback" ) yield oauth_client diff --git a/test/integration/linode_client/test_linode_client.py b/test/integration/linode_client/test_linode_client.py index c9ce35d6e..df634cf06 100644 --- a/test/integration/linode_client/test_linode_client.py +++ b/test/integration/linode_client/test_linode_client.py @@ -353,26 +353,6 @@ def test_fails_to_create_cluster_with_invalid_version(test_linode_client): assert e.status == 400 -# ProfileGroupTest - - -def test_get_sshkeys(test_linode_client, test_sshkey): - client = test_linode_client - - ssh_keys = client.profile.ssh_keys() - - ssh_labels = [i.label for i in ssh_keys] - - assert test_sshkey.label in ssh_labels - - -def test_ssh_key_create(test_sshkey, ssh_key_gen): - pub_key = ssh_key_gen[0] - key = test_sshkey - - assert pub_key == key._raw_json["ssh_key"] - - # ObjectStorageGroupTests @@ -385,9 +365,9 @@ def test_get_object_storage_clusters(test_linode_client): assert "us-east" in clusters[0].region.id -def test_get_keys(test_linode_client, ssh_keys_object_storage): +def test_get_keys(test_linode_client, access_keys_object_storage): client = test_linode_client - key = ssh_keys_object_storage + key = access_keys_object_storage keys = client.object_storage.keys() key_labels = [i.label for i in keys] @@ -395,8 +375,8 @@ def test_get_keys(test_linode_client, ssh_keys_object_storage): assert key.label in key_labels -def test_keys_create(test_linode_client, ssh_keys_object_storage): - key = ssh_keys_object_storage +def test_keys_create(test_linode_client, access_keys_object_storage): + key = access_keys_object_storage assert type(key) == type( ObjectStorageKeys(client=test_linode_client, id="123") diff --git a/test/integration/login_client/test_login_client.py b/test/integration/login_client/test_login_client.py index 8631c2617..7cb4246ea 100644 --- a/test/integration/login_client/test_login_client.py +++ b/test/integration/login_client/test_login_client.py @@ -32,7 +32,7 @@ def test_get_oathclient(test_linode_client, test_oauth_client): oauth_client = client.load(OAuthClient, test_oauth_client.id) - assert "test-oauth-client" == oauth_client.label + assert "_oauth" in test_oauth_client.label assert "https://localhost/oauth/callback" == oauth_client.redirect_uri diff --git a/test/integration/models/account/test_account.py b/test/integration/models/account/test_account.py index 337718709..a9dce4a3a 100644 --- a/test/integration/models/account/test_account.py +++ b/test/integration/models/account/test_account.py @@ -1,4 +1,5 @@ import time +from datetime import datetime from test.integration.helpers import get_test_label import pytest @@ -91,7 +92,6 @@ def test_get_user(test_linode_client): assert username == user.username assert "email" in user._raw_json - assert "email" in user._raw_json def test_list_child_accounts(test_linode_client): @@ -102,3 +102,25 @@ def test_list_child_accounts(test_linode_client): child_account = ChildAccount(client, child_accounts[0].euuid) child_account._api_get() child_account.create_token() + + +def test_get_invoice(test_linode_client): + client = test_linode_client + + invoices = client.account.invoices() + + if len(invoices) > 0: + assert isinstance(invoices[0].subtotal, float) + assert isinstance(invoices[0].tax, float) + assert isinstance(invoices[0].total, float) + assert r"'billing_source': 'linode'" in str(invoices[0]._raw_json) + + +def test_get_payments(test_linode_client): + client = test_linode_client + + payments = client.account.payments() + + if len(payments) > 0: + assert isinstance(payments[0].date, datetime) + assert isinstance(payments[0].usd, float) diff --git a/test/integration/models/lke/test_lke.py b/test/integration/models/lke/test_lke.py index 4967c067f..2e74c8205 100644 --- a/test/integration/models/lke/test_lke.py +++ b/test/integration/models/lke/test_lke.py @@ -1,3 +1,4 @@ +import base64 import re from test.integration.helpers import ( get_test_label, @@ -95,9 +96,17 @@ def test_cluster_dashboard_url_view(lke_cluster): assert re.search("https://+", url) -def test_kubeconfig_delete(lke_cluster): +def test_get_and_delete_kubeconfig(lke_cluster): cluster = lke_cluster + kubeconfig_encoded = cluster.kubeconfig + + kubeconfig_decoded = base64.b64decode(kubeconfig_encoded).decode("utf-8") + + assert "kind: Config" in kubeconfig_decoded + + assert "apiVersion:" in kubeconfig_decoded + res = send_request_when_resource_available(300, cluster.kubeconfig_delete) assert res is None diff --git a/test/integration/models/longview/test_longview.py b/test/integration/models/longview/test_longview.py index 0fb7daf7f..f04875e63 100644 --- a/test/integration/models/longview/test_longview.py +++ b/test/integration/models/longview/test_longview.py @@ -3,7 +3,12 @@ import pytest -from linode_api4.objects import LongviewClient, LongviewSubscription +from linode_api4.objects import ( + ApiError, + LongviewClient, + LongviewPlan, + LongviewSubscription, +) @pytest.mark.smoke @@ -46,3 +51,25 @@ def test_get_longview_subscription(test_linode_client, test_longview_client): assert re.search("[0-9]+", str(sub.price.hourly)) assert re.search("[0-9]+", str(sub.price.monthly)) + + assert "longview-3" in str(subs.lists) + assert "longview-10" in str(subs.lists) + assert "longview-40" in str(subs.lists) + assert "longview-100" in str(subs.lists) + + +def test_longview_plan_update_method_not_allowed(test_linode_client): + try: + test_linode_client.longview.longview_plan_update("longview-100") + except ApiError as e: + assert e.status == 405 + assert "Method Not Allowed" in str(e) + + +def test_get_current_longview_plan(test_linode_client): + lv_plan = test_linode_client.load(LongviewPlan, "") + + if lv_plan.label is not None: + assert "Longview" in lv_plan.label + assert "hourly" in lv_plan.price.dict + assert "monthly" in lv_plan.price.dict diff --git a/test/integration/models/objectstorage/test_obj_storage.py b/test/integration/models/objectstorage/test_obj_storage.py new file mode 100644 index 000000000..e040a9d1d --- /dev/null +++ b/test/integration/models/objectstorage/test_obj_storage.py @@ -0,0 +1,105 @@ +import time +from test.integration.conftest import get_region + +import pytest + +from linode_api4.objects import ( + ObjectStorageACL, + ObjectStorageBucket, + ObjectStorageCluster, + ObjectStorageKeys, +) + + +@pytest.fixture(scope="session") +def test_object_storage_bucket(test_linode_client): + client = test_linode_client + + region = get_region(client, {"Object Storage"}) + cluster_region_name = region.id + "-1" + label = str(time.time_ns())[:-5] + "-bucket" + + bucket = client.object_storage.bucket_create( + cluster=cluster_region_name, label=label + ) + + yield bucket + + bucket.delete() + + +def test_list_obj_storage_bucket( + test_linode_client, test_object_storage_bucket +): + client = test_linode_client + + buckets = client.object_storage.buckets() + target_bucket = test_object_storage_bucket + + bucket_ids = [bucket.id for bucket in buckets] + + assert target_bucket.id in bucket_ids + assert isinstance(target_bucket, ObjectStorageBucket) + + +def test_bucket_access_modify(test_object_storage_bucket): + bucket = test_object_storage_bucket + + res = bucket.access_modify(ObjectStorageACL.PRIVATE, cors_enabled=True) + + assert res + + +def test_bucket_access_update(test_object_storage_bucket): + bucket = test_object_storage_bucket + res = bucket.access_update(ObjectStorageACL.PRIVATE, cors_enabled=True) + + assert res + + +def test_get_ssl_cert(test_object_storage_bucket): + bucket = test_object_storage_bucket + + res = bucket.ssl_cert().ssl + + assert res is False + + +def test_create_key_for_specific_bucket( + test_linode_client, test_object_storage_bucket +): + client = test_linode_client + bucket = test_object_storage_bucket + keys = client.object_storage.keys_create( + "restricted-keys", + bucket_access=client.object_storage.bucket_access( + bucket.cluster, bucket.id, "read_write" + ), + ) + + assert isinstance(keys, ObjectStorageKeys) + assert keys.bucket_access[0].bucket_name == bucket.id + assert keys.bucket_access[0].permissions == "read_write" + assert keys.bucket_access[0].cluster == bucket.cluster + + +def test_get_cluster(test_linode_client, test_object_storage_bucket): + client = test_linode_client + bucket = test_object_storage_bucket + + cluster = client.load(ObjectStorageCluster, bucket.cluster) + + assert "linodeobjects.com" in cluster.domain + assert cluster.id == bucket.cluster + assert "available" == cluster.status + + +def test_get_buckets_in_cluster(test_linode_client, test_object_storage_bucket): + client = test_linode_client + bucket = test_object_storage_bucket + + cluster = client.load(ObjectStorageCluster, bucket.cluster) + buckets = cluster.buckets_in_cluster() + bucket_ids = [bucket.id for bucket in buckets] + + assert bucket.id in bucket_ids diff --git a/test/integration/models/profile/test_profile.py b/test/integration/models/profile/test_profile.py new file mode 100644 index 000000000..cafec12ea --- /dev/null +++ b/test/integration/models/profile/test_profile.py @@ -0,0 +1,36 @@ +from linode_api4.objects import PersonalAccessToken, Profile, SSHKey + + +def test_user_profile(test_linode_client): + client = test_linode_client + + profile = client.profile() + + assert isinstance(profile, Profile) + + +def test_get_personal_access_token_objects(test_linode_client): + client = test_linode_client + + personal_access_tokens = client.profile.tokens() + + if len(personal_access_tokens) > 0: + assert isinstance(personal_access_tokens[0], PersonalAccessToken) + + +def test_get_sshkeys(test_linode_client, test_sshkey): + client = test_linode_client + + ssh_keys = client.profile.ssh_keys() + + ssh_labels = [i.label for i in ssh_keys] + + assert isinstance(test_sshkey, SSHKey) + assert test_sshkey.label in ssh_labels + + +def test_ssh_key_create(test_sshkey, ssh_key_gen): + pub_key = ssh_key_gen[0] + key = test_sshkey + + assert pub_key == key._raw_json["ssh_key"] From 10d3685d251173b1c85f997595f061c6643c822b Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Sun, 7 Jul 2024 13:04:43 -0600 Subject: [PATCH 6/8] add minimal test account option in e2e-test.yml (#430) --- .github/workflows/e2e-test.yml | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 7729b6bc2..abf5fb209 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -1,7 +1,16 @@ name: Integration Tests on: - workflow_dispatch: null + workflow_dispatch: + inputs: + use_minimal_test_account: + description: 'Use minimal test account' + required: false + default: 'false' + sha: + description: 'The hash value of the commit' + required: false + default: '' push: branches: - main @@ -13,7 +22,16 @@ jobs: env: EXIT_STATUS: 0 steps: - - name: Clone Repository + - name: Clone Repository with SHA + if: ${{ inputs.sha != '' }} + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: 'recursive' + ref: ${{ inputs.sha }} + + - name: Clone Repository without SHA + if: ${{ inputs.sha == '' }} uses: actions/checkout@v4 with: fetch-depth: 0 @@ -40,20 +58,24 @@ jobs: mv calicoctl-linux-amd64 /usr/local/bin/calicoctl mv kubectl /usr/local/bin/kubectl + - name: Set LINODE_TOKEN + run: | + echo "LINODE_TOKEN=${{ secrets[inputs.use_minimal_test_account == 'true' && 'MINIMAL_LINODE_TOKEN' || 'LINODE_TOKEN'] }}" >> $GITHUB_ENV + - name: Run Integration tests run: | timestamp=$(date +'%Y%m%d%H%M') report_filename="${timestamp}_sdk_test_report.xml" make testint TEST_ARGS="--junitxml=${report_filename}" env: - LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }} + LINODE_TOKEN: ${{ env.LINODE_TOKEN }} - name: Apply Calico Rules to LKE if: always() run: | cd scripts && ./lke_calico_rules_e2e.sh env: - LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }} + LINODE_TOKEN: ${{ env.LINODE_TOKEN }} - name: Upload test results if: always() From 639588db861966ca5cb5ec8ba80d996b4f4454e4 Mon Sep 17 00:00:00 2001 From: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> Date: Wed, 10 Jul 2024 10:26:27 -0400 Subject: [PATCH 7/8] Linodes and OBJ Integration Tests Improvements (#434) * Improve and deduplicate object storage tests * Improve linode save test * type annotations for clients --- test/integration/models/linode/test_linode.py | 6 +- .../models/object_storage/test_obj.py | 39 +++++++ .../models/objectstorage/test_obj_storage.py | 105 ------------------ 3 files changed, 42 insertions(+), 108 deletions(-) delete mode 100644 test/integration/models/objectstorage/test_obj_storage.py diff --git a/test/integration/models/linode/test_linode.py b/test/integration/models/linode/test_linode.py index a18fede11..01f3aaa16 100644 --- a/test/integration/models/linode/test_linode.py +++ b/test/integration/models/linode/test_linode.py @@ -584,7 +584,7 @@ def test_get_linode_types_overrides(test_linode_client): def test_save_linode_noforce(test_linode_client, create_linode): linode = create_linode old_label = linode.label - linode.label = "updated_no_force_label" + linode.label = old_label + "updated_no_force" linode.save(force=False) linode = test_linode_client.load(Instance, linode.id) @@ -595,8 +595,8 @@ def test_save_linode_noforce(test_linode_client, create_linode): def test_save_linode_force(test_linode_client, create_linode): linode = create_linode old_label = linode.label - linode.label = "updated_force_label" - linode.save(force=False) + linode.label = old_label + "updated_force" + linode.save(force=True) linode = test_linode_client.load(Instance, linode.id) diff --git a/test/integration/models/object_storage/test_obj.py b/test/integration/models/object_storage/test_obj.py index 863eda129..3042f326a 100644 --- a/test/integration/models/object_storage/test_obj.py +++ b/test/integration/models/object_storage/test_obj.py @@ -7,6 +7,7 @@ from linode_api4.objects.object_storage import ( ObjectStorageACL, ObjectStorageBucket, + ObjectStorageCluster, ObjectStorageKeyPermission, ObjectStorageKeys, ) @@ -90,3 +91,41 @@ def test_bucket( buckets = test_linode_client.object_storage.buckets_in_region(region=region) assert len(buckets) >= 1 assert any(b.label == bucket.label for b in buckets) + + +def test_list_obj_storage_bucket( + test_linode_client: LinodeClient, + bucket: ObjectStorageBucket, +): + buckets = test_linode_client.object_storage.buckets() + target_bucket_id = bucket.id + assert any(target_bucket_id == b.id for b in buckets) + + +def test_bucket_access_modify(bucket: ObjectStorageBucket): + bucket.access_modify(ObjectStorageACL.PRIVATE, cors_enabled=True) + + +def test_bucket_access_update(bucket: ObjectStorageBucket): + bucket.access_update(ObjectStorageACL.PRIVATE, cors_enabled=True) + + +def test_get_ssl_cert(bucket: ObjectStorageBucket): + assert not bucket.ssl_cert().ssl + + +def test_get_cluster( + test_linode_client: LinodeClient, bucket: ObjectStorageBucket +): + cluster = test_linode_client.load(ObjectStorageCluster, bucket.cluster) + + assert "linodeobjects.com" in cluster.domain + assert cluster.id == bucket.cluster + assert "available" == cluster.status + + +def test_get_buckets_in_cluster( + test_linode_client: LinodeClient, bucket: ObjectStorageBucket +): + cluster = test_linode_client.load(ObjectStorageCluster, bucket.cluster) + assert any(bucket.id == b.id for b in cluster.buckets_in_cluster()) diff --git a/test/integration/models/objectstorage/test_obj_storage.py b/test/integration/models/objectstorage/test_obj_storage.py deleted file mode 100644 index e040a9d1d..000000000 --- a/test/integration/models/objectstorage/test_obj_storage.py +++ /dev/null @@ -1,105 +0,0 @@ -import time -from test.integration.conftest import get_region - -import pytest - -from linode_api4.objects import ( - ObjectStorageACL, - ObjectStorageBucket, - ObjectStorageCluster, - ObjectStorageKeys, -) - - -@pytest.fixture(scope="session") -def test_object_storage_bucket(test_linode_client): - client = test_linode_client - - region = get_region(client, {"Object Storage"}) - cluster_region_name = region.id + "-1" - label = str(time.time_ns())[:-5] + "-bucket" - - bucket = client.object_storage.bucket_create( - cluster=cluster_region_name, label=label - ) - - yield bucket - - bucket.delete() - - -def test_list_obj_storage_bucket( - test_linode_client, test_object_storage_bucket -): - client = test_linode_client - - buckets = client.object_storage.buckets() - target_bucket = test_object_storage_bucket - - bucket_ids = [bucket.id for bucket in buckets] - - assert target_bucket.id in bucket_ids - assert isinstance(target_bucket, ObjectStorageBucket) - - -def test_bucket_access_modify(test_object_storage_bucket): - bucket = test_object_storage_bucket - - res = bucket.access_modify(ObjectStorageACL.PRIVATE, cors_enabled=True) - - assert res - - -def test_bucket_access_update(test_object_storage_bucket): - bucket = test_object_storage_bucket - res = bucket.access_update(ObjectStorageACL.PRIVATE, cors_enabled=True) - - assert res - - -def test_get_ssl_cert(test_object_storage_bucket): - bucket = test_object_storage_bucket - - res = bucket.ssl_cert().ssl - - assert res is False - - -def test_create_key_for_specific_bucket( - test_linode_client, test_object_storage_bucket -): - client = test_linode_client - bucket = test_object_storage_bucket - keys = client.object_storage.keys_create( - "restricted-keys", - bucket_access=client.object_storage.bucket_access( - bucket.cluster, bucket.id, "read_write" - ), - ) - - assert isinstance(keys, ObjectStorageKeys) - assert keys.bucket_access[0].bucket_name == bucket.id - assert keys.bucket_access[0].permissions == "read_write" - assert keys.bucket_access[0].cluster == bucket.cluster - - -def test_get_cluster(test_linode_client, test_object_storage_bucket): - client = test_linode_client - bucket = test_object_storage_bucket - - cluster = client.load(ObjectStorageCluster, bucket.cluster) - - assert "linodeobjects.com" in cluster.domain - assert cluster.id == bucket.cluster - assert "available" == cluster.status - - -def test_get_buckets_in_cluster(test_linode_client, test_object_storage_bucket): - client = test_linode_client - bucket = test_object_storage_bucket - - cluster = client.load(ObjectStorageCluster, bucket.cluster) - buckets = cluster.buckets_in_cluster() - bucket_ids = [bucket.id for bucket in buckets] - - assert bucket.id in bucket_ids From 6031fd410f4c849c537e0b9f9ddb65a0425f7931 Mon Sep 17 00:00:00 2001 From: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Date: Thu, 11 Jul 2024 10:06:18 -0400 Subject: [PATCH 8/8] Revert "new: Add support for Linode Disk Encryption (#413)" (#435) --- linode_api4/groups/linode.py | 17 +------ linode_api4/objects/linode.py | 49 +----------------- linode_api4/objects/lke.py | 1 - test/fixtures/linode_instances.json | 4 -- test/fixtures/linode_instances_123_disks.json | 6 +-- ...inode_instances_123_disks_12345_clone.json | 3 +- .../lke_clusters_18881_nodes_123456.json | 2 +- .../lke_clusters_18881_pools_456.json | 3 +- test/integration/helpers.py | 6 +-- test/integration/models/linode/test_linode.py | 50 ++----------------- test/integration/models/lke/test_lke.py | 21 ++------ test/unit/objects/linode_test.py | 29 ++--------- test/unit/objects/lke_test.py | 6 +-- 13 files changed, 20 insertions(+), 177 deletions(-) diff --git a/linode_api4/groups/linode.py b/linode_api4/groups/linode.py index c146ce46c..5f69d2b94 100644 --- a/linode_api4/groups/linode.py +++ b/linode_api4/groups/linode.py @@ -1,9 +1,7 @@ import base64 import os from collections.abc import Iterable -from typing import Optional, Union -from linode_api4 import InstanceDiskEncryptionType from linode_api4.common import load_and_validate_keys from linode_api4.errors import UnexpectedResponseError from linode_api4.groups import Group @@ -130,15 +128,7 @@ def kernels(self, *filters): # create things def instance_create( - self, - ltype, - region, - image=None, - authorized_keys=None, - disk_encryption: Optional[ - Union[InstanceDiskEncryptionType, str] - ] = None, - **kwargs, + self, ltype, region, image=None, authorized_keys=None, **kwargs ): """ Creates a new Linode Instance. This function has several modes of operation: @@ -273,8 +263,6 @@ def instance_create( :type metadata: dict :param firewall: The firewall to attach this Linode to. :type firewall: int or Firewall - :param disk_encryption: The disk encryption policy for this Linode. - :type disk_encryption: InstanceDiskEncryptionType or str :param interfaces: An array of Network Interfaces to add to this Linode’s Configuration Profile. At least one and up to three Interface objects can exist in this array. :type interfaces: list[ConfigInterface] or list[dict[str, Any]] @@ -342,9 +330,6 @@ def instance_create( "authorized_keys": authorized_keys, } - if disk_encryption is not None: - params["disk_encryption"] = str(disk_encryption) - params.update(kwargs) result = self.client.post("/linode/instances", data=params) diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index afcf6c2d5..d86ec1746 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -22,25 +22,12 @@ from linode_api4.objects.base import MappedObject from linode_api4.objects.filtering import FilterableAttribute from linode_api4.objects.networking import IPAddress, IPv6Range, VPCIPAddress -from linode_api4.objects.serializable import StrEnum from linode_api4.objects.vpc import VPC, VPCSubnet from linode_api4.paginated_list import PaginatedList PASSWORD_CHARS = string.ascii_letters + string.digits + string.punctuation -class InstanceDiskEncryptionType(StrEnum): - """ - InstanceDiskEncryptionType defines valid values for the - Instance(...).disk_encryption field. - - API Documentation: TODO - """ - - enabled = "enabled" - disabled = "disabled" - - class Backup(DerivedBase): """ A Backup of a Linode Instance. @@ -127,7 +114,6 @@ class Disk(DerivedBase): "filesystem": Property(), "updated": Property(is_datetime=True), "linode_id": Property(identifier=True), - "disk_encryption": Property(), } def duplicate(self): @@ -676,8 +662,6 @@ class Instance(Base): "host_uuid": Property(), "watchdog_enabled": Property(mutable=True), "has_user_data": Property(), - "disk_encryption": Property(), - "lke_cluster_id": Property(), } @property @@ -1407,16 +1391,7 @@ def ip_allocate(self, public=False): i = IPAddress(self._client, result["address"], result) return i - def rebuild( - self, - image, - root_pass=None, - authorized_keys=None, - disk_encryption: Optional[ - Union[InstanceDiskEncryptionType, str] - ] = None, - **kwargs, - ): + def rebuild(self, image, root_pass=None, authorized_keys=None, **kwargs): """ Rebuilding an Instance deletes all existing Disks and Configs and deploys a new :any:`Image` to it. This can be used to reset an existing @@ -1434,8 +1409,6 @@ def rebuild( be a single key, or a path to a file containing the key. :type authorized_keys: list or str - :param disk_encryption: The disk encryption policy for this Linode. - :type disk_encryption: InstanceDiskEncryptionType or str :returns: The newly generated password, if one was not provided (otherwise True) @@ -1453,10 +1426,6 @@ def rebuild( "root_pass": root_pass, "authorized_keys": authorized_keys, } - - if disk_encryption is not None: - params["disk_encryption"] = str(disk_encryption) - params.update(kwargs) result = self._client.post( @@ -1786,22 +1755,6 @@ def stats(self): "{}/stats".format(Instance.api_endpoint), model=self ) - @property - def lke_cluster(self) -> Optional["LKECluster"]: - """ - Returns the LKE Cluster this Instance is a node of. - - :returns: The LKE Cluster this Instance is a node of. - :rtype: Optional[LKECluster] - """ - - # Local import to prevent circular dependency - from linode_api4.objects.lke import ( # pylint: disable=import-outside-toplevel - LKECluster, - ) - - return LKECluster(self._client, self.lke_cluster_id) - def stats_for(self, dt): """ Returns stats for the month containing the given datetime diff --git a/linode_api4/objects/lke.py b/linode_api4/objects/lke.py index 14f7f28db..4d3ec5a16 100644 --- a/linode_api4/objects/lke.py +++ b/linode_api4/objects/lke.py @@ -132,7 +132,6 @@ class LKENodePool(DerivedBase): "cluster_id": Property(identifier=True), "type": Property(slug_relationship=Type), "disks": Property(), - "disk_encryption": Property(), "count": Property(mutable=True), "nodes": Property( volatile=True diff --git a/test/fixtures/linode_instances.json b/test/fixtures/linode_instances.json index 130d44285..651fc56c1 100644 --- a/test/fixtures/linode_instances.json +++ b/test/fixtures/linode_instances.json @@ -41,8 +41,6 @@ "tags": ["something"], "host_uuid": "3a3ddd59d9a78bb8de041391075df44de62bfec8", "watchdog_enabled": true, - "disk_encryption": "disabled", - "lke_cluster_id": null, "placement_group": { "id": 123, "label": "test", @@ -88,8 +86,6 @@ "tags": [], "host_uuid": "3a3ddd59d9a78bb8de041391075df44de62bfec8", "watchdog_enabled": false, - "disk_encryption": "enabled", - "lke_cluster_id": 18881, "placement_group": null } ] diff --git a/test/fixtures/linode_instances_123_disks.json b/test/fixtures/linode_instances_123_disks.json index ddfe7f313..eca5079e5 100644 --- a/test/fixtures/linode_instances_123_disks.json +++ b/test/fixtures/linode_instances_123_disks.json @@ -10,8 +10,7 @@ "id": 12345, "updated": "2017-01-01T00:00:00", "label": "Ubuntu 17.04 Disk", - "created": "2017-01-01T00:00:00", - "disk_encryption": "disabled" + "created": "2017-01-01T00:00:00" }, { "size": 512, @@ -20,8 +19,7 @@ "id": 12346, "updated": "2017-01-01T00:00:00", "label": "512 MB Swap Image", - "created": "2017-01-01T00:00:00", - "disk_encryption": "disabled" + "created": "2017-01-01T00:00:00" } ] } diff --git a/test/fixtures/linode_instances_123_disks_12345_clone.json b/test/fixtures/linode_instances_123_disks_12345_clone.json index 899833e56..2d378edca 100644 --- a/test/fixtures/linode_instances_123_disks_12345_clone.json +++ b/test/fixtures/linode_instances_123_disks_12345_clone.json @@ -5,7 +5,6 @@ "id": 12345, "updated": "2017-01-01T00:00:00", "label": "Ubuntu 17.04 Disk", - "created": "2017-01-01T00:00:00", - "disk_encryption": "disabled" + "created": "2017-01-01T00:00:00" } \ No newline at end of file diff --git a/test/fixtures/lke_clusters_18881_nodes_123456.json b/test/fixtures/lke_clusters_18881_nodes_123456.json index 646b62f5d..311ef3878 100644 --- a/test/fixtures/lke_clusters_18881_nodes_123456.json +++ b/test/fixtures/lke_clusters_18881_nodes_123456.json @@ -1,5 +1,5 @@ { "id": "123456", - "instance_id": 456, + "instance_id": 123458, "status": "ready" } \ No newline at end of file diff --git a/test/fixtures/lke_clusters_18881_pools_456.json b/test/fixtures/lke_clusters_18881_pools_456.json index 225023d5d..ec6b570ac 100644 --- a/test/fixtures/lke_clusters_18881_pools_456.json +++ b/test/fixtures/lke_clusters_18881_pools_456.json @@ -23,6 +23,5 @@ "example tag", "another example" ], - "type": "g6-standard-4", - "disk_encryption": "enabled" + "type": "g6-standard-4" } \ No newline at end of file diff --git a/test/integration/helpers.py b/test/integration/helpers.py index e0aab06c4..5e9d1c441 100644 --- a/test/integration/helpers.py +++ b/test/integration/helpers.py @@ -79,14 +79,12 @@ def wait_for_condition( # Retry function to help in case of requests sending too quickly before instance is ready -def retry_sending_request( - retries: int, condition: Callable, *args, **kwargs -) -> object: +def retry_sending_request(retries: int, condition: Callable, *args) -> object: curr_t = 0 while curr_t < retries: try: curr_t += 1 - res = condition(*args, **kwargs) + res = condition(*args) return res except ApiError: if curr_t >= retries: diff --git a/test/integration/models/linode/test_linode.py b/test/integration/models/linode/test_linode.py index 01f3aaa16..02b6220a3 100644 --- a/test/integration/models/linode/test_linode.py +++ b/test/integration/models/linode/test_linode.py @@ -1,5 +1,4 @@ import time -from test.integration.conftest import get_region from test.integration.helpers import ( get_test_label, retry_sending_request, @@ -19,7 +18,7 @@ Instance, Type, ) -from linode_api4.objects.linode import InstanceDiskEncryptionType, MigrationType +from linode_api4.objects.linode import MigrationType @pytest.fixture(scope="session") @@ -143,30 +142,6 @@ def create_linode_for_long_running_tests(test_linode_client, e2e_test_firewall): linode_instance.delete() -@pytest.fixture(scope="function") -def linode_with_disk_encryption(test_linode_client, request): - client = test_linode_client - - target_region = get_region(client, {"Disk Encryption"}) - timestamp = str(time.time_ns()) - label = "TestSDK-" + timestamp - - disk_encryption = request.param - - linode_instance, password = client.linode.instance_create( - "g6-nanode-1", - target_region, - image="linode/ubuntu23.04", - label=label, - booted=False, - disk_encryption=disk_encryption, - ) - - yield linode_instance - - linode_instance.delete() - - # Test helper def get_status(linode: Instance, status: str): return linode.status == status @@ -195,7 +170,8 @@ def test_linode_transfer(test_linode_client, linode_with_volume_firewall): def test_linode_rebuild(test_linode_client): client = test_linode_client - chosen_region = get_region(client, {"Disk Encryption"}) + available_regions = client.regions() + chosen_region = available_regions[4] label = get_test_label() + "_rebuild" linode, password = client.linode.instance_create( @@ -204,18 +180,12 @@ def test_linode_rebuild(test_linode_client): wait_for_condition(10, 100, get_status, linode, "running") - retry_sending_request( - 3, - linode.rebuild, - "linode/debian10", - disk_encryption=InstanceDiskEncryptionType.disabled, - ) + retry_sending_request(3, linode.rebuild, "linode/debian10") wait_for_condition(10, 100, get_status, linode, "rebuilding") assert linode.status == "rebuilding" assert linode.image.id == "linode/debian10" - assert linode.disk_encryption == InstanceDiskEncryptionType.disabled wait_for_condition(10, 300, get_status, linode, "running") @@ -418,18 +388,6 @@ def test_linode_volumes(linode_with_volume_firewall): assert "test" in volumes[0].label -@pytest.mark.parametrize( - "linode_with_disk_encryption", ["disabled"], indirect=True -) -def test_linode_with_disk_encryption_disabled(linode_with_disk_encryption): - linode = linode_with_disk_encryption - - assert linode.disk_encryption == InstanceDiskEncryptionType.disabled - assert ( - linode.disks[0].disk_encryption == InstanceDiskEncryptionType.disabled - ) - - def wait_for_disk_status(disk: Disk, timeout): start_time = time.time() while True: diff --git a/test/integration/models/lke/test_lke.py b/test/integration/models/lke/test_lke.py index ce6700b80..2e74c8205 100644 --- a/test/integration/models/lke/test_lke.py +++ b/test/integration/models/lke/test_lke.py @@ -1,17 +1,14 @@ import base64 import re -from test.integration.conftest import get_region from test.integration.helpers import ( get_test_label, send_request_when_resource_available, wait_for_condition, ) -from typing import Any, Dict import pytest from linode_api4 import ( - InstanceDiskEncryptionType, LKEClusterControlPlaneACLAddressesOptions, LKEClusterControlPlaneACLOptions, LKEClusterControlPlaneOptions, @@ -24,7 +21,7 @@ def lke_cluster(test_linode_client): node_type = test_linode_client.linode.types()[1] # g6-standard-1 version = test_linode_client.lke.versions()[0] - region = get_region(test_linode_client, {"Disk Encryption", "Kubernetes"}) + region = test_linode_client.regions().first() node_pools = test_linode_client.lke.node_pool(node_type, 3) label = get_test_label() + "_cluster" @@ -41,7 +38,7 @@ def lke_cluster(test_linode_client): def lke_cluster_with_acl(test_linode_client): node_type = test_linode_client.linode.types()[1] # g6-standard-1 version = test_linode_client.lke.versions()[0] - region = get_region(test_linode_client, {"Kubernetes"}) + region = test_linode_client.regions().first() node_pools = test_linode_client.lke.node_pool(node_type, 1) label = get_test_label() + "_cluster" @@ -84,21 +81,9 @@ def test_get_lke_clusters(test_linode_client, lke_cluster): def test_get_lke_pool(test_linode_client, lke_cluster): cluster = lke_cluster - wait_for_condition( - 10, - 500, - get_node_status, - cluster, - "ready", - ) - pool = test_linode_client.load(LKENodePool, cluster.pools[0].id, cluster.id) - def _to_comparable(p: LKENodePool) -> Dict[str, Any]: - return {k: v for k, v in p._raw_json.items() if k not in {"nodes"}} - - assert _to_comparable(cluster.pools[0]) == _to_comparable(pool) - assert pool.disk_encryption == InstanceDiskEncryptionType.enabled + assert cluster.pools[0].id == pool.id def test_cluster_dashboard_url_view(lke_cluster): diff --git a/test/unit/objects/linode_test.py b/test/unit/objects/linode_test.py index e24f1107c..8b03cbe7c 100644 --- a/test/unit/objects/linode_test.py +++ b/test/unit/objects/linode_test.py @@ -1,11 +1,7 @@ from datetime import datetime from test.unit.base import ClientBaseCase -from linode_api4 import ( - InstanceDiskEncryptionType, - InstancePlacementGroupAssignment, - NetworkInterface, -) +from linode_api4 import InstancePlacementGroupAssignment, NetworkInterface from linode_api4.objects import ( Config, ConfigInterface, @@ -40,10 +36,6 @@ def test_get_linode(self): linode.host_uuid, "3a3ddd59d9a78bb8de041391075df44de62bfec8" ) self.assertEqual(linode.watchdog_enabled, True) - self.assertEqual( - linode.disk_encryption, InstanceDiskEncryptionType.disabled - ) - self.assertEqual(linode.lke_cluster_id, None) json = linode._raw_json self.assertIsNotNone(json) @@ -80,10 +72,7 @@ def test_rebuild(self): linode = Instance(self.client, 123) with self.mock_post("/linode/instances/123") as m: - pw = linode.rebuild( - "linode/debian9", - disk_encryption=InstanceDiskEncryptionType.enabled, - ) + pw = linode.rebuild("linode/debian9") self.assertIsNotNone(pw) self.assertTrue(isinstance(pw, str)) @@ -95,7 +84,6 @@ def test_rebuild(self): { "image": "linode/debian9", "root_pass": pw, - "disk_encryption": "enabled", }, ) @@ -318,15 +306,6 @@ def test_transfer_year_month(self): m.call_url, "/linode/instances/123/transfer/2023/4" ) - def test_lke_cluster(self): - """ - Tests that you can grab the parent LKE cluster from an instance node - """ - linode = Instance(self.client, 456) - - assert linode.lke_cluster_id == 18881 - assert linode.lke_cluster.id == linode.lke_cluster_id - def test_duplicate(self): """ Tests that you can submit a correct disk clone api request @@ -339,8 +318,6 @@ def test_duplicate(self): m.call_url, "/linode/instances/123/disks/12345/clone" ) - assert disk.disk_encryption == InstanceDiskEncryptionType.disabled - def test_disk_password(self): """ Tests that you can submit a correct disk password reset api request @@ -416,6 +393,7 @@ def test_create_disk(self): image="linode/debian10", ) self.assertEqual(m.call_url, "/linode/instances/123/disks") + print(m.call_data) self.assertEqual( m.call_data, { @@ -429,7 +407,6 @@ def test_create_disk(self): ) assert disk.id == 12345 - assert disk.disk_encryption == InstanceDiskEncryptionType.disabled def test_instance_create_with_user_data(self): """ diff --git a/test/unit/objects/lke_test.py b/test/unit/objects/lke_test.py index f39fb84ae..a44db97ef 100644 --- a/test/unit/objects/lke_test.py +++ b/test/unit/objects/lke_test.py @@ -1,7 +1,6 @@ from datetime import datetime from test.unit.base import ClientBaseCase -from linode_api4 import InstanceDiskEncryptionType from linode_api4.objects import ( LKECluster, LKEClusterControlPlaneACLAddressesOptions, @@ -48,9 +47,6 @@ def test_get_pool(self): self.assertEqual(pool.id, 456) self.assertEqual(pool.cluster_id, 18881) self.assertEqual(pool.type.id, "g6-standard-4") - self.assertEqual( - pool.disk_encryption, InstanceDiskEncryptionType.enabled - ) self.assertIsNotNone(pool.disks) self.assertIsNotNone(pool.nodes) self.assertIsNotNone(pool.autoscaler) @@ -88,7 +84,7 @@ def test_node_view(self): self.assertEqual(m.call_url, "/lke/clusters/18881/nodes/123456") self.assertIsNotNone(node) self.assertEqual(node.id, "123456") - self.assertEqual(node.instance_id, 456) + self.assertEqual(node.instance_id, 123458) self.assertEqual(node.status, "ready") def test_node_delete(self):