Skip to content

Commit

Permalink
Add Dynamic Client support (#260)
Browse files Browse the repository at this point in the history
* Add dynamic client support

* Add dynamic client support

* refactor(dynamic): use await magic method

* Add docs readme info for DynamicClient

Signed-off-by: Bob Haddleton <bob.haddleton@nokia.com>

* Fix example

Signed-off-by: Bob Haddleton <bob.haddleton@nokia.com>

* Fix examples

Signed-off-by: Bob Haddleton <bob.haddleton@nokia.com>

* Remove requirements entry and fix example

Signed-off-by: Bob Haddleton <bob.haddleton@nokia.com>

* [docs] add readme info for DynamicClient

Signed-off-by: Bob Haddleton <bob.haddleton@nokia.com>

* [fix] cleanup examples

Signed-off-by: Bob Haddleton <bob.haddleton@nokia.com>

* [refactor] review feedback changes

Signed-off-by: Bob Haddleton <bob.haddleton@nokia.com>

* [refactor] remove changes from client/rest.py

Signed-off-by: Bob Haddleton <bob.haddleton@nokia.com>

* [test] Fix e2e test case

Signed-off-by: Bob Haddleton <bob.haddleton@nokia.com>

* [lint] Fix flake8 errors

Signed-off-by: Bob Haddleton <bob.haddleton@nokia.com>

* [lint] Fix isort errors

Signed-off-by: Bob Haddleton <bob.haddleton@nokia.com>

---------

Signed-off-by: Bob Haddleton <bob.haddleton@nokia.com>
  • Loading branch information
bobh66 authored Jul 2, 2023
1 parent b291a3a commit 4695715
Show file tree
Hide file tree
Showing 19 changed files with 3,148 additions and 1 deletion.
34 changes: 34 additions & 0 deletions doc/source/readme.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,40 @@ To list all pods:
More complicated examples, like asynchronous multiple watch or tail logs from pods,
you can find in `examples/` folder.

There is also support for DynamicClient which will query the cluster for all supported
resources. This allows for dynamic resource selection at runtime.
The above example using DynamicClient would look like this:
::

import asyncio
from kubernetes_asyncio import config
from kubernetes_asyncio.client.api_client import ApiClient
from kubernetes_asyncio.dynamic import DynamicClient


async def main():
# Configs can be set in Configuration class directly or using helper
# utility. If no argument provided, the config will be loaded from
# default location.
await config.load_kube_config()

# use the context manager to close http sessions automatically
async with ApiClient() as api:
client = await DynamicClient(api)
v1 = await client.resources.get(api_version="v1", kind="Pod")
print("Listing pods with their IPs:")
ret = await v1.get()

for i in ret.items:
print(i.status.pod_ip, i.metadata.namespace, i.metadata.name)


if __name__ == '__main__':
loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close()

Additional examples are in the `examples/dynamic-client` folder.

Versions
--------
Expand Down
50 changes: 50 additions & 0 deletions examples/dynamic-client/accept_header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright 2021 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
This example demonstrates how to pass the custom header in the cluster.
"""

import asyncio

from kubernetes_asyncio.client import api_client
from kubernetes_asyncio.client.configuration import Configuration
from kubernetes_asyncio.config import kube_config
from kubernetes_asyncio.dynamic import DynamicClient


async def main():
# Creating a dynamic client
config = Configuration()
await kube_config.load_kube_config(client_configuration=config)
async with api_client.ApiClient(configuration=config) as apic:
async with DynamicClient(apic) as client:
# fetching the node api
api = await client.resources.get(api_version="v1", kind="Node")

# Creating a custom header
params = {'header_params': {'Accept': 'application/json;as=PartialObjectMetadataList;v=v1;g=meta.k8s.io'}}

resp = await api.get(**params)

# Printing the kind and apiVersion after passing new header params.
print("VERSION\t\t\t\tKIND")
print(f"{resp.apiVersion}\t\t{resp.kind}")


if __name__ == "__main__":
loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close()
198 changes: 198 additions & 0 deletions examples/dynamic-client/cluster_scoped_custom_resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# Copyright 2021 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
This example demonstrates the following:
- Creation of a custom resource definition (CRD) using dynamic-client
- Creation of cluster scoped custom resources (CR) using the above created CRD
- List, patch (update), delete the custom resources
- Delete the custom resource definition (CRD)
"""

import asyncio

from kubernetes_asyncio.client import api_client
from kubernetes_asyncio.client.configuration import Configuration
from kubernetes_asyncio.config import kube_config
from kubernetes_asyncio.dynamic import DynamicClient
from kubernetes_asyncio.dynamic.exceptions import ResourceNotFoundError


async def main():
# Creating a dynamic client
config = Configuration()
await kube_config.load_kube_config(client_configuration=config)
async with api_client.ApiClient(configuration=config) as apic:
client = await DynamicClient(apic)

# fetching the custom resource definition (CRD) api
crd_api = await client.resources.get(
api_version="apiextensions.k8s.io/v1", kind="CustomResourceDefinition"
)

# Creating a Namespaced CRD named "ingressroutes.apps.example.com"
name = "ingressroutes.apps.example.com"

crd_manifest = {
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": {
"name": name,
},
"spec": {
"group": "apps.example.com",
"versions": [
{
"name": "v1",
"schema": {
"openAPIV3Schema": {
"properties": {
"spec": {
"properties": {
"strategy": {"type": "string"},
"virtualhost": {
"properties": {
"fqdn": {"type": "string"},
"tls": {
"properties": {
"secretName": {"type": "string"}
},
"type": "object",
},
},
"type": "object",
},
},
"type": "object",
}
},
"type": "object",
}
},
"served": True,
"storage": True,
}
],
"scope": "Cluster",
"names": {
"plural": "ingressroutes",
"listKind": "IngressRouteList",
"singular": "ingressroute",
"kind": "IngressRoute",
"shortNames": ["ir"],
},
},
}

crd_creation_response = await crd_api.create(crd_manifest)
print(
"\n[INFO] custom resource definition `ingressroutes.apps.example.com` created\n"
)
print("SCOPE\t\tNAME")
print(f"{crd_creation_response.spec.scope}\t\t{crd_creation_response.metadata.name}")

# Fetching the "ingressroutes" CRD api

try:
await client.resources.get(
api_version="apps.example.com/v1", kind="IngressRoute"
)
except ResourceNotFoundError:
# Need to wait a sec for the discovery layer to get updated
await asyncio.sleep(2)

ingressroute_api = await client.resources.get(
api_version="apps.example.com/v1", kind="IngressRoute"
)

# Creating a custom resource (CR) `ingress-route-*`, using the above CRD `ingressroutes.apps.example.com`

ingressroute_manifest_first = {
"apiVersion": "apps.example.com/v1",
"kind": "IngressRoute",
"metadata": {
"name": "ingress-route-first",
},
"spec": {
"virtualhost": {
"fqdn": "www.google.com",
"tls": {"secretName": "google-tls"},
},
"strategy": "RoundRobin",
},
}

ingressroute_manifest_second = {
"apiVersion": "apps.example.com/v1",
"kind": "IngressRoute",
"metadata": {
"name": "ingress-route-second",
},
"spec": {
"virtualhost": {
"fqdn": "www.yahoo.com",
"tls": {"secretName": "yahoo-tls"},
},
"strategy": "RoundRobin",
},
}

await ingressroute_api.create(body=ingressroute_manifest_first)
await ingressroute_api.create(body=ingressroute_manifest_second)
print("\n[INFO] custom resources `ingress-route-*` created\n")

# Listing the `ingress-route-*` custom resources

ingress_routes_list = await ingressroute_api.get()
print("NAME\t\t\t\tFQDN\t\tTLS\t\t\t\tSTRATEGY")
for item in ingress_routes_list.items:
print(f"{item.metadata.name}\t{item.spec.virtualhost.fqdn}\t{item.spec.virtualhost.tls}\t"
f"{item.spec.strategy}")

# Patching the ingressroutes custom resources

ingressroute_manifest_first["spec"]["strategy"] = "Random"
ingressroute_manifest_second["spec"]["strategy"] = "WeightedLeastRequest"

await ingressroute_api.patch(body=ingressroute_manifest_first, content_type="application/merge-patch+json")
await ingressroute_api.patch(body=ingressroute_manifest_second, content_type="application/merge-patch+json")

print(
"\n[INFO] custom resources `ingress-route-*` patched to update the strategy\n"
)
patched_ingress_routes_list = await ingressroute_api.get()
print("NAME\t\t\t\tFQDN\t\t\tTLS\t\t\tSTRATEGY")
for item in patched_ingress_routes_list.items:
print(f"{item.metadata.name}\t{item.spec.virtualhost.fqdn}\t{item.spec.virtualhost.tls}\t"
f"{item.spec.strategy}")

# Deleting the ingressroutes custom resources

await ingressroute_api.delete(name="ingress-route-first")
await ingressroute_api.delete(name="ingress-route-second")

print("\n[INFO] custom resources `ingress-route-*` deleted")

# Deleting the ingressroutes.apps.example.com custom resource definition

await crd_api.delete(name=name)
print(
"\n[INFO] custom resource definition `ingressroutes.apps.example.com` deleted"
)


if __name__ == "__main__":
loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close()
92 changes: 92 additions & 0 deletions examples/dynamic-client/configmap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Copyright 2021 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
This example demonstrates the following:
- Creation of a k8s configmap using dynamic-client
- List, patch(update), delete the configmap
"""

import asyncio

from kubernetes_asyncio.client import api_client
from kubernetes_asyncio.client.configuration import Configuration
from kubernetes_asyncio.config import kube_config
from kubernetes_asyncio.dynamic import DynamicClient


async def main():
# Creating a dynamic client
config = Configuration()
await kube_config.load_kube_config(client_configuration=config)
async with api_client.ApiClient(configuration=config) as apic:
client = await DynamicClient(apic)

# fetching the configmap api
api = await client.resources.get(api_version="v1", kind="ConfigMap")

configmap_name = "test-configmap"

configmap_manifest = {
"kind": "ConfigMap",
"apiVersion": "v1",
"metadata": {
"name": configmap_name,
"labels": {
"foo": "bar",
},
},
"data": {
"config.json": '{"command":"/usr/bin/mysqld_safe"}',
"frontend.cnf": "[mysqld]\nbind-address = 10.0.0.3\n",
},
}

# Creating configmap `test-configmap` in the `default` namespace

await api.create(body=configmap_manifest, namespace="default")

print("\n[INFO] configmap `test-configmap` created\n")

# Listing the configmaps in the `default` namespace

configmap_list = await api.get(
name=configmap_name, namespace="default", label_selector="foo=bar"
)

print(f"NAME:\n{configmap_list.metadata.name}\n")
print(f"DATA:\n{configmap_list.data}\n")

# Updating the configmap's data, `config.json`

configmap_manifest["data"]["config.json"] = "{}"

configmap_patched = await api.patch(
name=configmap_name, namespace="default", body=configmap_manifest
)

print("\n[INFO] configmap `test-configmap` patched\n")
print(f"NAME:\n{configmap_patched.metadata.name}\n")
print(f"DATA:\n{configmap_patched.data}\n")

# Deleting configmap `test-configmap` from the `default` namespace

await api.delete(name=configmap_name, body={}, namespace="default")
print("\n[INFO] configmap `test-configmap` deleted\n")


if __name__ == "__main__":
loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close()
Loading

0 comments on commit 4695715

Please sign in to comment.