Skip to content

new: Add support for LKE Control Plane ACLs - #406

Merged
lgarber-akamai merged 16 commits into
linode:devfrom
lgarber-akamai:new/lke-acl
May 23, 2024
Merged

new: Add support for LKE Control Plane ACLs#406
lgarber-akamai merged 16 commits into
linode:devfrom
lgarber-akamai:new/lke-acl

Conversation

@lgarber-akamai

@lgarber-akamai lgarber-akamai commented May 14, 2024

Copy link
Copy Markdown
Contributor

📝 Description

This pull request adds support for configuring and viewing the ACL configuration for an LKE cluster's control plane.

NOTE: This PR does NOT include the PUT LKE cluster changes because the acl configuration is not returned in the GET LKE cluster response.

New Endpoint Methods

  • control_plane_acl - GET /lke/clusters/{cluster_id}/control_plane_acl
  • control_plane_acl_update(...) - PUT /lke/clusters/{cluster_id}/control_plane_acl
  • control_plane_acl_delete() - POST /lke/clusters/{cluster_id}/control_plane_acl

Updated Endpoint Methods

  • LKEGroup.cluster_create(...) - Add control_plane field to method arguments

Misc Changes

  • Added data classes for LKE cluster control plane and all substructures
  • Added logic to JSONObject to remove Optional values from the generated dict if None
    • Added a new always_include class var used to designate optional values that should always be included in the generated Dict
  • Updated test fixture framework to support underscores in path

✔️ How to Test

The following test steps assume you have pulled down this PR locally and run make install.

Unit Testing

make testunit

Integration Testing

make TEST_COMMAND=models/lke/test_lke.py testint

Manual Testing

In a Python SDK sandbox environment (e.g. dx-devenv), run the following:

import os

from linode_api4 import (
    LinodeClient,
    LKEClusterControlPlaneOptions,
    LKEClusterControlPlaneACLOptions,
    LKEClusterControlPlaneACLAddressesOptions,
)

client = LinodeClient(token=os.getenv("LINODE_TOKEN"))

cluster = client.lke.cluster_create(
    "us-mia",
    "test-cluster",
    client.lke.node_pool("g6-standard-1", 1),
    "1.29",
    control_plane=LKEClusterControlPlaneOptions(
        acl=LKEClusterControlPlaneACLOptions(
            enabled=True,
            addresses=LKEClusterControlPlaneACLAddressesOptions(
                ipv4=["10.0.0.1/32"], ipv6=["1234::5678"]
            ),
        )
    ),
)

print("Original ACL:", cluster.control_plane_acl)

cluster.control_plane_acl_update(
    LKEClusterControlPlaneACLOptions(
        addresses=LKEClusterControlPlaneACLAddressesOptions(ipv4=["10.0.0.2/32"])
    )
)

print("Updated ACL:", cluster.control_plane_acl)

cluster.control_plane_acl_delete()

print("Deleted ACL:", cluster.control_plane_acl)
  1. Ensure the script runs successfully and the output matches the following:
Original ACL: LKEClusterControlPlaneACL(enabled=True, addresses=LKEClusterControlPlaneACLAddresses(ipv4=['10.0.0.1/32'], ipv6=['1234::5678/128']))
Updated ACL: LKEClusterControlPlaneACL(enabled=True, addresses=LKEClusterControlPlaneACLAddresses(ipv4=['10.0.0.2/32'], ipv6=None))
Deleted ACL: LKEClusterControlPlaneACL(enabled=False, addresses=None)

Comment thread test/unit/fixtures.py

# This regex is useful for finding individual underscore characters,
# which is necessary to allow us to use underscores in URL paths.
PATH_REPLACEMENT_REGEX = re.compile(r"(?<!_)_(?!_)")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is necessary for lke/clusters/18881/control_plane_acl fixture since the _s were being replaced with /s.

"{}/servicetoken".format(LKECluster.api_endpoint), model=self
)

def control_plane_acl_update(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This pattern is not consistent with the rest of the codebase, but unfortunately we cannot implement it using the traditional properties because ACL is not returned from the LKE cluster GET endpoint.


@property
def ips(self, *filters) -> PaginatedList:
def ips(self) -> PaginatedList:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This isn't directly related to this change but it was causing the linter to fail. This shouldn't be a breaking change since arguments could already not be passed into ips 👍

Comment on lines +60 to +65
always_include: ClassVar[Set[str]] = {}
"""
A set of keys corresponding to fields that should always be
included in the generated output regardless of whether their values
are None.
"""

@lgarber-akamai lgarber-akamai May 15, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was considering implementing something more similar to omitempty in Go, but in this case I think there are far fewer instances where we would want to include null values rather than excluding them. I'd definitely appreciate some feedback on this!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, I agree that in the most of the case we just want to omit empty values.

Comment on lines +154 to +202
cls = type(self)
type_hints = get_type_hints(cls)

def attempt_serialize(value: Any) -> Any:
"""
Attempts to serialize the given value, else returns the value unchanged.
"""
if issubclass(type(value), JSONObject):
return value._serialize()

return value

def should_include(key: str, value: Any) -> bool:
"""
Returns whether the given key/value pair should be included in the resulting dict.
"""

if key in cls.always_include:
return True

hint = type_hints.get(key)

# We want to exclude any Optional values that are None
# NOTE: We need to check for Union here because Optional is an alias of Union.
if (
hint is None
or get_origin(hint) is not Union
or type(None) not in get_args(hint)
):
return True

return value is not None

result = {}

for k, v in vars(self).items():
if not should_include(k, v):
continue

if isinstance(v, List):
v = [attempt_serialize(j) for j in v]
elif isinstance(v, Dict):
v = {k: attempt_serialize(j) for k, j in v.items()}
else:
v = attempt_serialize(v)

result[k] = v

return result

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately we have to reinvent the wheel here because we don't have access to type hints or always_include for nested JSONObjects in the asdict dict_factory method.

Ideally I'd want to break this out into a separate PR but excluding optional fields from generated request bodies is a requirement for LKEClusterControlPlaneACL.

Comment on lines +32 to +86
@dataclass
class LKEClusterControlPlaneACLAddressesOptions(JSONObject):
"""
LKEClusterControlPlaneACLAddressesOptions are options used to configure
IP ranges that are explicitly allowed to access an LKE cluster's control plane.
"""

ipv4: Optional[List[str]] = None
ipv6: Optional[List[str]] = None


@dataclass
class LKEClusterControlPlaneACLOptions(JSONObject):
"""
LKEClusterControlPlaneACLOptions is used to set
the ACL configuration of an LKE cluster's control plane.
"""

enabled: Optional[bool] = None
addresses: Optional[LKEClusterControlPlaneACLAddressesOptions] = None


@dataclass
class LKEClusterControlPlaneOptions(JSONObject):
"""
LKEClusterControlPlaneOptions is used to configure
the control plane of an LKE cluster during its creation.
"""

high_availability: Optional[bool] = None
acl: Optional[LKEClusterControlPlaneACLOptions] = None


@dataclass
class LKEClusterControlPlaneACLAddresses(JSONObject):
"""
LKEClusterControlPlaneACLAddresses describes IP ranges that are explicitly allowed
to access an LKE cluster's control plane.
"""

ipv4: List[str] = None
ipv6: List[str] = None


@dataclass
class LKEClusterControlPlaneACL(JSONObject):
"""
LKEClusterControlPlaneACL describes the ACL configuration of an LKE cluster's
control plane.
"""

enabled: bool = False
addresses: LKEClusterControlPlaneACLAddressesOptions = None


Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is more of a linodego pattern but I think it's necessary given we're starting to see more diverging API request & response structures

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm wondering if we should separate them in ansible and tf as well. One of the use case that I can think of is in terraform framework, nested block can't be both optional and computed. It's a bit annoying when doing the migration. We did the customized object type, but Hashicorp once suggested that we could have two separated block attributes, one is optional and one is computed to implement that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

One of the use case that I can think of is in terraform framework, nested block can't be both optional and computed. It's a bit annoying when doing the migration. We did the customized object type, but Hashicorp once suggested that we could have two separated block attributes, one is optional and one is computed to implement that.

That's a good call out! I think in the long term we'll need to split apart user-defined and computed structures anyway because of the limitations in TF framework you mentioned

@lgarber-akamai lgarber-akamai added the new-feature for new features in the changelog. label May 16, 2024
@lgarber-akamai
lgarber-akamai marked this pull request as ready for review May 16, 2024 15:32
@lgarber-akamai
lgarber-akamai requested a review from a team as a code owner May 16, 2024 15:32
@lgarber-akamai
lgarber-akamai requested review from yec-akamai and ykim-akamai and removed request for a team May 16, 2024 15:32
Comment thread linode_api4/objects/lke.py

@yec-akamai yec-akamai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tested and everything passed locally, nice work!

Comment on lines +60 to +65
always_include: ClassVar[Set[str]] = {}
"""
A set of keys corresponding to fields that should always be
included in the generated output regardless of whether their values
are None.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, I agree that in the most of the case we just want to omit empty values.

@ykim-akamai ykim-akamai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, tests pass locally

@ykim-akamai

Copy link
Copy Markdown
Contributor

Also, thanks for fixing the lint error 👍

@lgarber-akamai
lgarber-akamai merged commit 5f1c1df into linode:dev May 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new-feature for new features in the changelog.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants