diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5bea77b2c..d97f93452 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,7 +7,3 @@ **What are the steps to reproduce the issue or verify the changes?** **How do I run the relevant unit/integration tests?** - -## 📷 Preview - -**If applicable, include a screenshot or code snippet of this change. Otherwise, please remove this section.** \ No newline at end of file diff --git a/.github/workflows/e2e-test-pr.yml b/.github/workflows/e2e-test-pr.yml index 86809d177..f765b0a0d 100644 --- a/.github/workflows/e2e-test-pr.yml +++ b/.github/workflows/e2e-test-pr.yml @@ -27,6 +27,14 @@ on: pull_request_number: description: 'The number of the PR.' required: false + test_report_upload: + description: 'Indicates whether to upload the test report to object storage. Defaults to "false"' + required: false + default: 'false' + type: choice + options: + - 'true' + - 'false' name: PR E2E Tests @@ -101,7 +109,7 @@ jobs: LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }} - name: Upload test results - if: always() + if: always() && github.repository == 'linode/linode_api4-python' && (github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.test_report_upload == 'true')) run: | filename=$(ls | grep -E '^[0-9]{12}_sdk_test_report\.xml$') python3 e2e_scripts/tod_scripts/xml_to_obj_storage/scripts/add_gha_info_to_xml.py \ diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index e2762ff95..5c24361d0 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -41,6 +41,14 @@ on: options: - 'true' - 'false' + test_report_upload: + description: 'Indicates whether to upload the test report to object storage. Defaults to "false"' + type: choice + required: false + default: 'false' + options: + - 'true' + - 'false' push: branches: - main @@ -172,7 +180,8 @@ jobs: process-upload-report: runs-on: ubuntu-latest needs: [integration-tests] - if: always() && github.repository == 'linode/linode_api4-python' # Run even if integration tests fail and only on main repository + # Run even if integration tests fail on main repository AND push event OR test_report_upload is true in case of manual run + if: always() && github.repository == 'linode/linode_api4-python' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.test_report_upload == 'true')) outputs: summary: ${{ steps.set-test-summary.outputs.summary }} @@ -271,4 +280,4 @@ jobs: payload: | channel: ${{ secrets.SLACK_CHANNEL_ID }} thread_ts: "${{ steps.main_message.outputs.ts }}" - text: "${{ needs.process-upload-report.outputs.summary }}" + text: "${{ needs.process-upload-report.outputs.summary }}" \ No newline at end of file diff --git a/linode_api4/groups/linode.py b/linode_api4/groups/linode.py index f88808e64..e32a284f1 100644 --- a/linode_api4/groups/linode.py +++ b/linode_api4/groups/linode.py @@ -335,8 +335,6 @@ def instance_create( :type network_helper: bool :param maintenance_policy: The slug of the maintenance policy to apply during maintenance. If not provided, the default policy (linode/migrate) will be applied. - NOTE: This field is in beta and may only - function if base_url is set to `https://api.linode.com/v4beta`. :type maintenance_policy: str :returns: A new Instance object, or a tuple containing the new Instance and diff --git a/linode_api4/groups/lke.py b/linode_api4/groups/lke.py index c3d6fdc5d..330c1d378 100644 --- a/linode_api4/groups/lke.py +++ b/linode_api4/groups/lke.py @@ -62,8 +62,8 @@ def cluster_create( self, region, label, - node_pools, kube_version, + node_pools: Optional[list] = None, control_plane: Union[ LKEClusterControlPlaneOptions, Dict[str, Any] ] = None, @@ -119,6 +119,15 @@ def cluster_create( :returns: The new LKE Cluster :rtype: LKECluster """ + if node_pools is None: + node_pools = [] + + if len(node_pools) == 0 and ( + tier is None or tier.lower() != "enterprise" + ): + raise ValueError( + "LKE standard clusters must have at least one node pool." + ) params = { "label": label, diff --git a/linode_api4/groups/maintenance.py b/linode_api4/groups/maintenance.py index 7d56cec6e..63cb424df 100644 --- a/linode_api4/groups/maintenance.py +++ b/linode_api4/groups/maintenance.py @@ -9,8 +9,6 @@ class MaintenanceGroup(Group): def maintenance_policies(self): """ - .. note:: This endpoint is in beta. This will only function if base_url is set to `https://api.linode.com/v4beta`. - Returns a collection of MaintenancePolicy objects representing available maintenance policies that can be applied to Linodes diff --git a/linode_api4/objects/__init__.py b/linode_api4/objects/__init__.py index 009e9436e..89a681635 100644 --- a/linode_api4/objects/__init__.py +++ b/linode_api4/objects/__init__.py @@ -3,7 +3,7 @@ from .dbase import DerivedBase from .serializable import JSONObject from .filtering import and_, or_ -from .region import Region +from .region import Region, Capability from .image import Image from .linode import * from .linode_interfaces import * diff --git a/linode_api4/objects/account.py b/linode_api4/objects/account.py index 54298ed11..a4aca1848 100644 --- a/linode_api4/objects/account.py +++ b/linode_api4/objects/account.py @@ -218,9 +218,7 @@ class AccountSettings(Base): "object_storage": Property(), "backups_enabled": Property(mutable=True), "interfaces_for_new_linodes": Property(mutable=True), - "maintenance_policy": Property( - mutable=True - ), # Note: This field is only available when using v4beta. + "maintenance_policy": Property(mutable=True), } @@ -249,7 +247,7 @@ class Event(Base): "duration": Property(), "secondary_entity": Property(), "message": Property(), - "maintenance_policy_set": Property(), # Note: This field is only available when using v4beta. + "maintenance_policy_set": Property(), "description": Property(), "source": Property(), "not_before": Property(is_datetime=True), diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index fae0926d5..ccddd7e40 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -1,6 +1,7 @@ import copy import string import sys +import warnings from dataclasses import dataclass, field from datetime import datetime from enum import Enum @@ -217,7 +218,7 @@ def resize(self, new_size): class Kernel(Base): """ The primary component of every Linux system. The kernel interfaces - with the system’s hardware and it controls the operating system’s core functionality. + with the system’s hardware, and it controls the operating system’s core functionality. Your Compute Instance is capable of running one of three kinds of kernels: @@ -237,6 +238,10 @@ class Kernel(Base): to compile the kernel from source than to download it from your package manager. For more information on custom compiled kernels, review our guides for Debian, Ubuntu, and CentOS. + .. note:: + The ``xen`` property is deprecated and is no longer returned by the API. + It is maintained for backward compatibility only. + API Documentation: https://techdocs.akamai.com/linode-api/reference/get-kernel """ @@ -256,6 +261,16 @@ class Kernel(Base): "pvops": Property(), } + def __getattribute__(self, name: str) -> object: + if name == "xen": + warnings.warn( + "The 'xen' property of Kernel is deprecated and is no longer " + "returned by the API. It is maintained for backward compatibility only.", + DeprecationWarning, + stacklevel=2, + ) + return super().__getattribute__(name) + class Type(Base): """ @@ -800,9 +815,7 @@ class Instance(Base): "lke_cluster_id": Property(), "capabilities": Property(unordered=True), "interface_generation": Property(), - "maintenance_policy": Property( - mutable=True - ), # Note: This field is only available when using v4beta. + "maintenance_policy": Property(mutable=True), "locks": Property(unordered=True), } diff --git a/linode_api4/objects/lock.py b/linode_api4/objects/lock.py index b6552da7b..9cee64517 100644 --- a/linode_api4/objects/lock.py +++ b/linode_api4/objects/lock.py @@ -10,7 +10,7 @@ class LockType(StrEnum): """ LockType defines valid values for resource lock types. - API Documentation: https://techdocs.akamai.com/linode-api/reference/post-lock + API Documentation: TBD """ cannot_delete = "cannot_delete" @@ -22,7 +22,7 @@ class LockEntity(JSONObject): """ Represents the entity that is locked. - API Documentation: https://techdocs.akamai.com/linode-api/reference/get-lock + API Documentation: TBD """ id: int = 0 @@ -35,7 +35,7 @@ class Lock(Base): """ A resource lock that prevents deletion or modification of a resource. - API Documentation: https://techdocs.akamai.com/linode-api/reference/get-lock + API Documentation: TBD """ api_endpoint = "/locks/{id}" diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py index 4315e4c2e..ca8f83921 100644 --- a/linode_api4/objects/monitor.py +++ b/linode_api4/objects/monitor.py @@ -116,6 +116,19 @@ class DashboardType(StrEnum): custom = "custom" +class AlertStatus(StrEnum): + """ + Enum for supported alert status values. + """ + + AlertDefinitionStatusProvisioning = "provisioning" + AlertDefinitionStatusEnabling = "enabling" + AlertDefinitionStatusDisabling = "disabling" + AlertDefinitionStatusEnabled = "enabled" + AlertDefinitionStatusDisabled = "disabled" + AlertDefinitionStatusFailed = "failed" + + @dataclass class Filter(JSONObject): """ @@ -428,6 +441,40 @@ class ChannelContent(JSONObject): # Other channel types like 'webhook', 'slack' could be added here as Optional fields. +@dataclass +class EmailDetails(JSONObject): + """ + Represents email-specific details for an alert channel. + """ + + usernames: Optional[List[str]] = None + recipient_type: Optional[str] = None + + +@dataclass +class ChannelDetails(JSONObject): + """ + Represents the details block for an AlertChannel, which varies by channel type. + """ + + email: Optional[EmailDetails] = None + + +@dataclass +class AlertInfo(JSONObject): + """ + Represents a reference to alerts associated with an alert channel. + Fields: + - url: str - API URL to fetch the alerts for this channel + - type: str - Type identifier (e.g., 'alerts-definitions') + - alert_count: int - Number of alerts associated with this channel + """ + + url: str = "" + _type: str = field(default="", metadata={"json_key": "type"}) + alert_count: int = 0 + + class AlertChannel(Base): """ Represents an alert channel used to deliver notifications when alerts @@ -450,7 +497,8 @@ class AlertChannel(Base): "label": Property(), "type": Property(), "channel_type": Property(), - "alerts": Property(mutable=False, json_object=Alerts), + "details": Property(mutable=False, json_object=ChannelDetails), + "alerts": Property(mutable=False, json_object=AlertInfo), "content": Property(mutable=False, json_object=ChannelContent), "created": Property(is_datetime=True), "updated": Property(is_datetime=True), diff --git a/linode_api4/objects/nodebalancer.py b/linode_api4/objects/nodebalancer.py index f02dda269..cb6e566f7 100644 --- a/linode_api4/objects/nodebalancer.py +++ b/linode_api4/objects/nodebalancer.py @@ -252,6 +252,7 @@ class NodeBalancer(Base): "transfer": Property(), "tags": Property(mutable=True, unordered=True), "client_udp_sess_throttle": Property(mutable=True), + "locks": Property(unordered=True), } # create derived objects diff --git a/linode_api4/objects/region.py b/linode_api4/objects/region.py index c9dc05099..3c8986259 100644 --- a/linode_api4/objects/region.py +++ b/linode_api4/objects/region.py @@ -3,6 +3,68 @@ from linode_api4.errors import UnexpectedResponseError from linode_api4.objects.base import Base, JSONObject, Property +from linode_api4.objects.serializable import StrEnum + + +class Capability(StrEnum): + """ + An enum class that represents the capabilities that Linode offers + across different regions and services. + + These capabilities indicate what services are available in each data center. + """ + + linodes = "Linodes" + nodebalancers = "NodeBalancers" + block_storage = "Block Storage" + object_storage = "Object Storage" + object_storage_regions = "Object Storage Access Key Regions" + object_storage_endpoint_types = "Object Storage Endpoint Types" + lke = "Kubernetes" + lke_ha_controlplanes = "LKE HA Control Planes" + lke_e = "Kubernetes Enterprise" + firewall = "Cloud Firewall" + gpu = "GPU Linodes" + vlans = "Vlans" + vpcs = "VPCs" + vpcs_extra = "VPCs Extra" + machine_images = "Machine Images" + dbaas = "Managed Databases" + dbaas_beta = "Managed Databases Beta" + bs_migrations = "Block Storage Migrations" + metadata = "Metadata" + premium_plans = "Premium Plans" + edge_plans = "Edge Plans" + distributed_plans = "Distributed Plans" + lke_control_plane_acl = "LKE Network Access Control List (IP ACL)" + aclb = "Akamai Cloud Load Balancer" + support_ticket_severity = "Support Ticket Severity" + backups = "Backups" + placement_group = "Placement Group" + disk_encryption = "Disk Encryption" + la_disk_encryption = "LA Disk Encryption" + akamai_ram_protection = "Akamai RAM Protection" + blockstorage_encryption = "Block Storage Encryption" + blockstorage_perf_b1 = "Block Storage Performance B1" + blockstorage_perf_b1_default = "Block Storage Performance B1 Default" + aclp = "Akamai Cloud Pulse" + aclp_logs = "Akamai Cloud Pulse Logs" + aclp_logs_lkee = "Akamai Cloud Pulse Logs LKE-E Audit" + aclp_logs_dc_lkee = "ACLP Logs Datacenter LKE-E" + smtp_enabled = "SMTP Enabled" + stackscripts = "StackScripts" + vpu = "NETINT Quadra T1U" + linode_interfaces = "Linode Interfaces" + maintenance_policy = "Maintenance Policy" + vpc_dual_stack = "VPC Dual Stack" + vpc_ipv6_stack = "VPC IPv6 Stack" + nlb = "Network LoadBalancer" + natgateway = "NAT Gateway" + lke_e_byovpc = "Kubernetes Enterprise BYO VPC" + lke_e_stacktype = "Kubernetes Enterprise Dual Stack" + ruleset = "Cloud Firewall Rule Set" + prefixlists = "Cloud Firewall Prefix Lists" + current_prefixlists = "Cloud Firewall Prefix List Current References" @dataclass diff --git a/test/fixtures/nodebalancers.json b/test/fixtures/nodebalancers.json index 85eec186b..9b4dc8dae 100644 --- a/test/fixtures/nodebalancers.json +++ b/test/fixtures/nodebalancers.json @@ -10,7 +10,8 @@ "updated": "2018-01-01T00:01:01", "label": "balancer123456", "client_conn_throttle": 0, - "tags": ["something"] + "tags": ["something"], + "locks": ["cannot_delete_with_subresources"] }, { "created": "2018-01-01T00:01:01", @@ -22,7 +23,8 @@ "updated": "2018-01-01T00:01:01", "label": "balancer123457", "client_conn_throttle": 0, - "tags": [] + "tags": [], + "locks": [] } ], "results": 2, diff --git a/test/fixtures/nodebalancers_123456.json b/test/fixtures/nodebalancers_123456.json index e965d4379..a78c8d3e3 100644 --- a/test/fixtures/nodebalancers_123456.json +++ b/test/fixtures/nodebalancers_123456.json @@ -10,5 +10,8 @@ "client_conn_throttle": 0, "tags": [ "something" + ], + "locks": [ + "cannot_delete_with_subresources" ] } \ No newline at end of file diff --git a/test/integration/filters/fixtures.py b/test/integration/filters/fixtures.py index 31b7edcbf..e753236dd 100644 --- a/test/integration/filters/fixtures.py +++ b/test/integration/filters/fixtures.py @@ -25,11 +25,11 @@ def lke_cluster_instance(test_linode_client): region = get_region(test_linode_client, {"Kubernetes", "Disk Encryption"}) - node_pools = test_linode_client.lke.node_pool(node_type, 3) + node_pool = test_linode_client.lke.node_pool(node_type, 3) label = get_test_label() + "_cluster" cluster = test_linode_client.lke.cluster_create( - region, label, node_pools, version + region, label, version, [node_pool] ) yield cluster diff --git a/test/integration/filters/model_filters_test.py b/test/integration/filters/model_filters_test.py index 22bb8299e..55bed6ac3 100644 --- a/test/integration/filters/model_filters_test.py +++ b/test/integration/filters/model_filters_test.py @@ -63,12 +63,13 @@ def test_linode_type_model_filter(test_linode_client): def test_lke_cluster_model_filter(test_linode_client, lke_cluster_instance): client = test_linode_client + lke_cluster = lke_cluster_instance filtered_cluster = client.lke.clusters( - LKECluster.label.contains(lke_cluster_instance.label) + LKECluster.label.contains(lke_cluster.label) ) - assert filtered_cluster[0].id == lke_cluster_instance.id + assert filtered_cluster[0].id == lke_cluster.id def test_networking_firewall_model_filter( diff --git a/test/integration/linode_client/test_linode_client.py b/test/integration/linode_client/test_linode_client.py index eb1b06369..4060064d3 100644 --- a/test/integration/linode_client/test_linode_client.py +++ b/test/integration/linode_client/test_linode_client.py @@ -328,10 +328,10 @@ def test_cluster_create_with_api_objects(test_linode_client): node_type = client.linode.types()[1] # g6-standard-1 version = client.lke.versions()[0] region = get_region(client, {"Kubernetes"}) - node_pools = client.lke.node_pool(node_type, 3) + node_pool = client.lke.node_pool(node_type, 3) label = get_test_label() - cluster = client.lke.cluster_create(region, label, node_pools, version) + cluster = client.lke.cluster_create(region, label, version, [node_pool]) assert cluster.region.id == region.id assert cluster.k8s_version.id == version.id @@ -350,8 +350,8 @@ def test_fails_to_create_cluster_with_invalid_version(test_linode_client): cluster = client.lke.cluster_create( region, "example-cluster", - {"type": "g6-standard-1", "count": 3}, invalid_version, + {"type": "g6-standard-1", "count": 3}, ) except ApiError as e: assert "not valid" in str(e.json) diff --git a/test/integration/models/account/test_account.py b/test/integration/models/account/test_account.py index 5833a9344..4c4dcc134 100644 --- a/test/integration/models/account/test_account.py +++ b/test/integration/models/account/test_account.py @@ -1,7 +1,11 @@ import time from datetime import datetime from test.integration.conftest import get_region -from test.integration.helpers import get_test_label, retry_sending_request +from test.integration.helpers import ( + get_test_label, + retry_sending_request, + wait_for_condition, +) import pytest @@ -102,13 +106,18 @@ def test_latest_get_event(test_linode_client, e2e_test_firewall): firewall=e2e_test_firewall, ) - events = client.load(Event, "") + def get_linode_status(): + return linode.status == "running" - latest_events = events._raw_json.get("data") + # To ensure the Linode is running and the 'event' key has been populated + wait_for_condition(3, 100, get_linode_status) + + events = client.load(Event, "") + latest_events = events._raw_json.get("data")[:15] linode.delete() - for event in latest_events[:15]: + for event in latest_events: if label == event["entity"]["label"]: break else: diff --git a/test/integration/models/domain/test_domain.py b/test/integration/models/domain/test_domain.py index 9dc180a6e..d7956d421 100644 --- a/test/integration/models/domain/test_domain.py +++ b/test/integration/models/domain/test_domain.py @@ -43,10 +43,10 @@ def test_clone(test_linode_client, test_domain): dom = "example.clone-" + timestamp + "-inttestsdk.org" domain.clone(dom) - ds = test_linode_client.domains() - time.sleep(1) + ds = test_linode_client.domains() + domains = [i.domain for i in ds] assert dom in domains diff --git a/test/integration/models/linode/test_linode.py b/test/integration/models/linode/test_linode.py index c485dd19c..9f6194fa9 100644 --- a/test/integration/models/linode/test_linode.py +++ b/test/integration/models/linode/test_linode.py @@ -611,6 +611,12 @@ def test_linode_initate_migration(test_linode_client, e2e_test_firewall): migration_type=MigrationType.COLD, ) + def get_linode_status(): + return linode.status == "offline" + + # To verify that Linode's status changed before deletion (during migration status is set to 'offline') + wait_for_condition(5, 120, get_linode_status) + res = linode.delete() assert res @@ -1101,8 +1107,7 @@ def test_delete_interface_containing_vpc( def test_create_linode_with_maintenance_policy(test_linode_client): client = test_linode_client - # TODO: Replace with random region after GA - region = "ap-south" + region = get_region(client, {"Linodes", "Cloud Firewall"}, site_type="core") label = get_test_label() policies = client.maintenance.maintenance_policies() diff --git a/test/integration/models/lke/test_lke.py b/test/integration/models/lke/test_lke.py index 71ebc1ff2..116665df6 100644 --- a/test/integration/models/lke/test_lke.py +++ b/test/integration/models/lke/test_lke.py @@ -38,7 +38,7 @@ def lke_cluster(test_linode_client): label = get_test_label() + "_cluster" cluster = test_linode_client.lke.cluster_create( - region, label, node_pools, version + region, label, version, node_pools ) yield cluster @@ -57,8 +57,8 @@ def lke_cluster_with_acl(test_linode_client): cluster = test_linode_client.lke.cluster_create( region, label, - node_pools, version, + node_pools, control_plane=LKEClusterControlPlaneOptions( acl=LKEClusterControlPlaneACLOptions( enabled=True, @@ -103,7 +103,7 @@ def lke_cluster_with_labels_and_taints(test_linode_client): label = get_test_label() + "_cluster" cluster = test_linode_client.lke.cluster_create( - region, label, node_pools, version + region, label, version, node_pools ) yield cluster @@ -124,8 +124,8 @@ def lke_cluster_with_apl(test_linode_client): cluster = test_linode_client.lke.cluster_create( region, label, - node_pools, version, + node_pools, control_plane=LKEClusterControlPlaneOptions( high_availability=True, ), @@ -160,8 +160,8 @@ def lke_cluster_enterprise(e2e_test_firewall, test_linode_client): cluster = test_linode_client.lke.cluster_create( region, label, - node_pools, version, + node_pools, tier="enterprise", ) diff --git a/test/integration/models/monitor/test_monitor.py b/test/integration/models/monitor/test_monitor.py index b6cf40b54..908ac1a44 100644 --- a/test/integration/models/monitor/test_monitor.py +++ b/test/integration/models/monitor/test_monitor.py @@ -16,6 +16,7 @@ MonitorService, MonitorServiceToken, ) +from linode_api4.objects.monitor import AlertStatus # List all dashboards @@ -227,7 +228,8 @@ def wait_for_alert_ready(alert_id, service_type: str): interval = initial_timeout alert = client.load(AlertDefinition, alert_id, service_type) while ( - getattr(alert, "status", None) == "in progress" + getattr(alert, "status", None) + != AlertStatus.AlertDefinitionStatusEnabled and (time.time() - start) < timeout ): time.sleep(interval) diff --git a/test/integration/models/networking/test_networking.py b/test/integration/models/networking/test_networking.py index 0edd5bd0a..27ffbb444 100644 --- a/test/integration/models/networking/test_networking.py +++ b/test/integration/models/networking/test_networking.py @@ -32,7 +32,7 @@ ) -def create_linode(test_linode_client): +def create_linode_func(test_linode_client): client = test_linode_client label = get_test_label() @@ -49,7 +49,7 @@ def create_linode(test_linode_client): @pytest.fixture def create_linode_for_ip_share(test_linode_client): - linode = create_linode(test_linode_client) + linode = create_linode_func(test_linode_client) yield linode @@ -58,7 +58,7 @@ def create_linode_for_ip_share(test_linode_client): @pytest.fixture def create_linode_to_be_shared_with_ips(test_linode_client): - linode = create_linode(test_linode_client) + linode = create_linode_func(test_linode_client) yield linode @@ -302,6 +302,8 @@ def test_create_and_delete_vlan(test_linode_client, linode_for_vlan_tests): wait_for_condition(3, 100, get_status, linode, "rebooting") assert linode.status == "rebooting" + wait_for_condition(3, 100, get_status, linode, "running") + # Delete the VLAN is_deleted = test_linode_client.networking.delete_vlan( vlan_label, linode.region @@ -334,6 +336,7 @@ def test_get_global_firewall_settings(test_linode_client): def test_ip_info(test_linode_client, create_linode): linode = create_linode + wait_for_condition(3, 100, get_status, linode, "running") ip_info = test_linode_client.load(IPAddress, linode.ipv4[0]) diff --git a/test/integration/models/nodebalancer/test_nodebalancer.py b/test/integration/models/nodebalancer/test_nodebalancer.py index 9e7537897..692efb027 100644 --- a/test/integration/models/nodebalancer/test_nodebalancer.py +++ b/test/integration/models/nodebalancer/test_nodebalancer.py @@ -130,7 +130,7 @@ def test_get_nb_config_with_udp(test_linode_client, create_nb_config_with_udp): assert "udp" == config.protocol assert 1234 == config.udp_check_port - assert 16 == config.udp_session_timeout + assert 2 == config.udp_session_timeout def test_update_nb_config(test_linode_client, create_nb_config_with_udp): diff --git a/test/unit/groups/lke_test.py b/test/unit/groups/lke_test.py index a39db81a6..802960192 100644 --- a/test/unit/groups/lke_test.py +++ b/test/unit/groups/lke_test.py @@ -21,8 +21,8 @@ def test_cluster_create_with_acl(self): self.client.lke.cluster_create( "us-mia", "test-acl-cluster", - [self.client.lke.node_pool("g6-nanode-1", 3)], "1.29", + [self.client.lke.node_pool("g6-nanode-1", 3)], control_plane=LKEClusterControlPlaneOptions( acl=LKEClusterControlPlaneACLOptions( enabled=True, @@ -41,3 +41,53 @@ def test_cluster_create_with_acl(self): assert m.call_data["control_plane"]["acl"]["addresses"]["ipv6"] == [ "1234::5678" ] + + def test_cluster_create_enterprise_without_node_pools(self): + """ + Tests that an enterprise LKE cluster can be created without node pools. + """ + with self.mock_post("lke/clusters") as m: + self.client.lke.cluster_create( + "us-west", + "test-enterprise-cluster", + "1.29", + tier="enterprise", + ) + + assert m.call_data["region"] == "us-west" + assert m.call_data["label"] == "test-enterprise-cluster" + assert m.call_data["k8s_version"] == "1.29" + assert m.call_data["tier"] == "enterprise" + assert m.call_data["node_pools"] == [] + + def test_cluster_create_enterprise_case_insensitive(self): + """ + Tests that tier comparison is case-insensitive for enterprise tier. + """ + with self.mock_post("lke/clusters") as m: + self.client.lke.cluster_create( + "us-west", + "test-enterprise-cluster", + "1.29", + tier="ENTERPRISE", + ) + + assert m.call_data["tier"] == "ENTERPRISE" + assert m.call_data["node_pools"] == [] + + def test_cluster_create_standard_without_node_pools_raises_error(self): + """ + Tests that creating a standard LKE cluster without node pools raises ValueError. + """ + with self.assertRaises(ValueError) as context: + self.client.lke.cluster_create( + "us-east", + "test-standard-cluster", + "1.29", + tier="standard", + ) + + self.assertIn( + "LKE standard clusters must have at least one node pool", + str(context.exception), + ) diff --git a/test/unit/linode_client_test.py b/test/unit/linode_client_test.py index d87e08894..e82f3562d 100644 --- a/test/unit/linode_client_test.py +++ b/test/unit/linode_client_test.py @@ -817,7 +817,7 @@ def test_cluster_create_with_api_objects(self): node_pools = self.client.lke.node_pool(node_type, 3) with self.mock_post("lke/clusters") as m: cluster = self.client.lke.cluster_create( - region, "example-cluster", node_pools, version + region, "example-cluster", version, node_pools ) self.assertEqual(m.call_data["region"], "ap-west") self.assertEqual( @@ -850,8 +850,8 @@ def test_cluster_create_with_string_repr(self): cluster = self.client.lke.cluster_create( "ap-west", "example-cluster", - {"type": "g6-standard-1", "count": 3}, "1.19", + {"type": "g6-standard-1", "count": 3}, ) self.assertEqual(m.call_data["region"], "ap-west") self.assertEqual( diff --git a/test/unit/objects/lke_test.py b/test/unit/objects/lke_test.py index 10284a0c9..91f9ed3fe 100644 --- a/test/unit/objects/lke_test.py +++ b/test/unit/objects/lke_test.py @@ -302,6 +302,7 @@ def test_cluster_create_with_labels_and_taints(self): self.client.lke.cluster_create( "us-mia", "test-acl-cluster", + "1.29", [ self.client.lke.node_pool( "g6-nanode-1", @@ -317,7 +318,6 @@ def test_cluster_create_with_labels_and_taints(self): ], ) ], - "1.29", ) assert m.call_data["node_pools"][0] == { @@ -339,13 +339,13 @@ def test_cluster_create_with_apl(self): cluster = self.client.lke.cluster_create( "us-mia", "test-aapl-cluster", + "1.29", [ self.client.lke.node_pool( "g6-dedicated-4", 3, ) ], - "1.29", apl_enabled=True, control_plane=LKEClusterControlPlaneOptions( high_availability=True, diff --git a/test/unit/objects/nodebalancers_test.py b/test/unit/objects/nodebalancers_test.py index ed0f0c320..c02b40ea3 100644 --- a/test/unit/objects/nodebalancers_test.py +++ b/test/unit/objects/nodebalancers_test.py @@ -175,6 +175,24 @@ def test_update(self): }, ) + def test_locks_not_in_put(self): + """ + Test that locks are not included in PUT request when updating a NodeBalancer. + Locks are managed through the separate /v4/locks endpoint. + """ + nb = NodeBalancer(self.client, 123456) + # Access locks to ensure it's loaded + self.assertEqual(nb.locks, ["cannot_delete_with_subresources"]) + + nb.label = "new-label" + + with self.mock_put("nodebalancers/123456") as m: + nb.save() + self.assertEqual(m.call_url, "/nodebalancers/123456") + # Verify locks is NOT in the PUT data + self.assertNotIn("locks", m.call_data) + self.assertEqual(m.call_data["label"], "new-label") + def test_firewalls(self): """ Test that you can get the firewalls for the requested NodeBalancer.