Skip to content

Commit eb3de3d

Browse files
k8s: patch get_kubeconfig to work around deserialization issue
1 parent 715b0c9 commit eb3de3d

4 files changed

Lines changed: 295 additions & 47 deletions

File tree

README.md

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -216,31 +216,6 @@ docker run -it --rm --name pydo -v $PWD/tests:/tests pydo:dev pytest tests/mocke
216216

217217
> This selection lists the known issues of the client generator.
218218
219-
#### `kubernetes.get_kubeconfig` Does not serialize response content
220-
221-
In the generated Python client, calling client.kubernetes.get_kubeconfig(cluster_id) raises a deserialization error when the response content-type is application/yaml. This occurs because the generator does not correctly handle YAML responses. We should investigate whether the OpenAPI spec or generator configuration can be adjusted to support this content-type. If not, the issue should be reported upstream to improve YAML support in client generation.
222-
223-
Workaround (with std lib httplib):
224-
225-
```python
226-
from http.client import HTTPSConnection
227-
228-
conn = HTTPSConnection('api.digitalocean.com')
229-
conn.request(
230-
'GET',
231-
f'/v2/kubernetes/clusters/{cluster_id}/kubeconfig',
232-
headers={'Authorization': f'Bearer {os.environ["DIGITALOCEAN_TOKEN"]}'}
233-
)
234-
response = conn.getresponse()
235-
236-
if response.getcode() > 400:
237-
msg = 'Unable to get kubeconfig'
238-
raise RuntimeError(msg)
239-
240-
kube_config = response.read().decode('utf-8')
241-
conn.close()
242-
```
243-
244219
#### `invoices.get_pdf_by_uuid(invoice_uuid=invoice_uuid_param)` Does not return PDF
245220

246221
In the generated python client, when calling `invoices.get_pdf_by_uuid`, the response returns a Iterator[bytes] that does not format correctly into a PDF.

src/pydo/aio/operations/_patch.py

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,116 @@
66
77
Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize
88
"""
9-
from typing import TYPE_CHECKING
9+
from typing import TYPE_CHECKING, Any, cast
10+
11+
from azure.core.exceptions import (
12+
ClientAuthenticationError,
13+
HttpResponseError,
14+
ResourceExistsError,
15+
ResourceNotFoundError,
16+
ResourceNotModifiedError,
17+
map_error,
18+
)
19+
from azure.core.pipeline import PipelineResponse
20+
from azure.core.tracing.decorator_async import distributed_trace_async
21+
22+
from ._operations import (
23+
KubernetesOperations as _KubernetesOperations,
24+
build_kubernetes_get_kubeconfig_request,
25+
)
1026

1127
if TYPE_CHECKING:
1228
# pylint: disable=unused-import,ungrouped-imports
13-
from typing import List
29+
from typing import MutableMapping, Type
30+
31+
__all__ = ("KubernetesOperations",)
32+
33+
34+
# Override: generated client expects JSON but this endpoint returns application/yaml;
35+
# we return the response body as str instead of deserializing.
36+
class KubernetesOperations(_KubernetesOperations):
37+
"""Kubernetes operations."""
38+
39+
@distributed_trace_async
40+
async def get_kubeconfig(
41+
self, cluster_id: str, *, expiry_seconds: int = 0, **kwargs: Any
42+
) -> str:
43+
"""Retrieve the kubeconfig for a Kubernetes Cluster.
44+
45+
This endpoint returns a kubeconfig file in YAML format. It can be used to
46+
connect to and administer the cluster using the Kubernetes command line tool,
47+
``kubectl``, or other programs supporting kubeconfig files (e.g., client libraries).
48+
49+
The resulting kubeconfig file uses token-based authentication for clusters
50+
supporting it, and certificate-based authentication otherwise. For a list of
51+
supported versions and more information, see "How to Connect to a DigitalOcean
52+
Kubernetes Cluster"
53+
https://docs.digitalocean.com/products/kubernetes/how-to/connect-to-cluster/
54+
55+
Clusters supporting token-based authentication may define an expiration by
56+
passing a duration in seconds as a query parameter (expiry_seconds).
57+
If not set or 0, then the token will have a 7 day expiry. The query parameter
58+
has no impact in certificate-based authentication.
59+
60+
Kubernetes Roles granted to a user with a token-based kubeconfig are derived from that user's
61+
DigitalOcean role. Custom roles require additional configuration by a cluster administrator.
62+
63+
:param cluster_id: A unique ID that can be used to reference a Kubernetes cluster. Required.
64+
:type cluster_id: str
65+
:keyword expiry_seconds: The duration in seconds that the returned Kubernetes credentials will
66+
be valid. If not set or 0, the credentials will have a 7 day expiry. Default value is 0.
67+
:paramtype expiry_seconds: int
68+
:return: The kubeconfig file contents as a string (YAML).
69+
:rtype: str
70+
:raises ~azure.core.exceptions.HttpResponseError:
71+
"""
72+
error_map: "MutableMapping[int, Type[HttpResponseError]]" = {
73+
404: ResourceNotFoundError,
74+
409: ResourceExistsError,
75+
304: ResourceNotModifiedError,
76+
401: cast(
77+
"Type[HttpResponseError]",
78+
lambda response: ClientAuthenticationError(response=response),
79+
),
80+
429: HttpResponseError,
81+
500: HttpResponseError,
82+
}
83+
error_map.update(kwargs.pop("error_map", {}) or {})
84+
85+
_headers = kwargs.pop("headers", {}) or {}
86+
_params = kwargs.pop("params", {}) or {}
87+
88+
_request = build_kubernetes_get_kubeconfig_request(
89+
cluster_id=cluster_id,
90+
expiry_seconds=expiry_seconds,
91+
headers=_headers,
92+
params=_params,
93+
)
94+
_request.url = self._client.format_url(_request.url)
95+
96+
# stream=True so the pipeline's content policy skips deserialization (API returns YAML, not JSON)
97+
pipeline_response: PipelineResponse = (
98+
await self._client._pipeline.run( # pylint: disable=protected-access
99+
_request, stream=True, **kwargs
100+
)
101+
)
102+
103+
response = pipeline_response.http_response
104+
105+
if response.status_code not in [200]:
106+
await response.read()
107+
map_error(
108+
status_code=response.status_code,
109+
response=response,
110+
error_map=error_map,
111+
)
112+
raise HttpResponseError(response=response)
14113

15-
__all__ = (
16-
[]
17-
) # type: List[str] # Add all objects you want publicly available to users at this package level
114+
if hasattr(response, "read"):
115+
body = await response.read()
116+
else:
117+
body = response.content
118+
return body.decode("utf-8") if body else ""
18119

19120

20121
def patch_sdk():

src/pydo/operations/_patch.py

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,115 @@
66
77
Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize
88
"""
9-
from typing import TYPE_CHECKING
9+
from typing import TYPE_CHECKING, Any, cast
1010

11-
from ._operations import DropletsOperations as Droplets
11+
from azure.core.exceptions import (
12+
ClientAuthenticationError,
13+
HttpResponseError,
14+
ResourceExistsError,
15+
ResourceNotFoundError,
16+
ResourceNotModifiedError,
17+
map_error,
18+
)
19+
from azure.core.pipeline import PipelineResponse
20+
from azure.core.tracing.decorator import distributed_trace
21+
22+
from ._operations import (
23+
KubernetesOperations as _KubernetesOperations,
24+
build_kubernetes_get_kubeconfig_request,
25+
)
1226

1327
if TYPE_CHECKING:
1428
# pylint: disable=unused-import,ungrouped-imports
15-
pass
29+
from typing import MutableMapping, Type
30+
31+
32+
__all__ = ["KubernetesOperations"]
33+
34+
35+
# Override: generated client expects JSON but this endpoint returns application/yaml;
36+
# we return the response body as str instead of deserializing.
37+
class KubernetesOperations(_KubernetesOperations):
38+
"""Kubernetes operations."""
39+
40+
@distributed_trace
41+
def get_kubeconfig(
42+
self, cluster_id: str, *, expiry_seconds: int = 0, **kwargs: Any
43+
) -> str:
44+
"""Retrieve the kubeconfig for a Kubernetes Cluster.
45+
46+
This endpoint returns a kubeconfig file in YAML format. It can be used to
47+
connect to and administer the cluster using the Kubernetes command line tool,
48+
``kubectl``, or other programs supporting kubeconfig files (e.g., client libraries).
49+
50+
The resulting kubeconfig file uses token-based authentication for clusters
51+
supporting it, and certificate-based authentication otherwise. For a list of
52+
supported versions and more information, see "How to Connect to a DigitalOcean
53+
Kubernetes Cluster"
54+
https://docs.digitalocean.com/products/kubernetes/how-to/connect-to-cluster/
55+
56+
Clusters supporting token-based authentication may define an expiration by
57+
passing a duration in seconds as a query parameter (expiry_seconds).
58+
If not set or 0, then the token will have a 7 day expiry. The query parameter
59+
has no impact in certificate-based authentication.
60+
61+
Kubernetes Roles granted to a user with a token-based kubeconfig are derived from that user's
62+
DigitalOcean role. Custom roles require additional configuration by a cluster administrator.
63+
64+
:param cluster_id: A unique ID that can be used to reference a Kubernetes cluster. Required.
65+
:type cluster_id: str
66+
:keyword expiry_seconds: The duration in seconds that the returned Kubernetes credentials will
67+
be valid. If not set or 0, the credentials will have a 7 day expiry. Default value is 0.
68+
:paramtype expiry_seconds: int
69+
:return: The kubeconfig file contents as a string (YAML).
70+
:rtype: str
71+
:raises ~azure.core.exceptions.HttpResponseError:
72+
"""
73+
error_map: "MutableMapping[int, Type[HttpResponseError]]" = {
74+
404: ResourceNotFoundError,
75+
409: ResourceExistsError,
76+
304: ResourceNotModifiedError,
77+
401: cast(
78+
"Type[HttpResponseError]",
79+
lambda response: ClientAuthenticationError(response=response),
80+
),
81+
429: HttpResponseError,
82+
500: HttpResponseError,
83+
}
84+
error_map.update(kwargs.pop("error_map", {}) or {})
85+
86+
_headers = kwargs.pop("headers", {}) or {}
87+
_params = kwargs.pop("params", {}) or {}
88+
89+
_request = build_kubernetes_get_kubeconfig_request(
90+
cluster_id=cluster_id,
91+
expiry_seconds=expiry_seconds,
92+
headers=_headers,
93+
params=_params,
94+
)
95+
_request.url = self._client.format_url(_request.url)
96+
97+
# stream=True so the pipeline's content policy skips deserialization (API returns YAML, not JSON).
98+
# Without this, the policy raises DecodeError for application/yaml. See test_kubernetes_get_kubeconfig.
99+
pipeline_response: PipelineResponse = (
100+
self._client._pipeline.run( # pylint: disable=protected-access
101+
_request, stream=True, **kwargs
102+
)
103+
)
104+
105+
response = pipeline_response.http_response
16106

107+
if response.status_code not in [200]:
108+
response.read()
109+
map_error(
110+
status_code=response.status_code,
111+
response=response,
112+
error_map=error_map,
113+
)
114+
raise HttpResponseError(response=response)
17115

18-
__all__ = []
116+
body = response.read() if hasattr(response, "read") else response.content
117+
return body.decode("utf-8") if body else ""
19118

20119

21120
def patch_sdk():

0 commit comments

Comments
 (0)