{"id":42570647,"url":"https://github.com/puzl-cloud/kubesdk","last_synced_at":"2026-01-28T21:07:41.294Z","repository":{"id":327495146,"uuid":"1076163061","full_name":"puzl-cloud/kubesdk","owner":"puzl-cloud","description":"Kubernetes client for Python + CRD\u003c\u003eAPI models (both ways) generator. Fast, fully typed, async.","archived":false,"fork":false,"pushed_at":"2026-01-09T23:22:20.000Z","size":952,"stargazers_count":73,"open_issues_count":9,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-10T20:08:37.092Z","etag":null,"topics":["asyncio","client","crd","k8s","kubernetes","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/puzl-cloud.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-14T13:41:08.000Z","updated_at":"2026-01-09T23:13:16.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/puzl-cloud/kubesdk","commit_stats":null,"previous_names":["puzl-cloud/kubesdk"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/puzl-cloud/kubesdk","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/puzl-cloud%2Fkubesdk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/puzl-cloud%2Fkubesdk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/puzl-cloud%2Fkubesdk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/puzl-cloud%2Fkubesdk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/puzl-cloud","download_url":"https://codeload.github.com/puzl-cloud/kubesdk/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/puzl-cloud%2Fkubesdk/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28851838,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-28T15:15:36.453Z","status":"ssl_error","status_checked_at":"2026-01-28T15:15:13.020Z","response_time":57,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asyncio","client","crd","k8s","kubernetes","python"],"created_at":"2026-01-28T21:07:40.703Z","updated_at":"2026-01-28T21:07:41.286Z","avatar_url":"https://github.com/puzl-cloud.png","language":"Python","funding_links":[],"categories":["Projects"],"sub_categories":["DevOps Tools"],"readme":"\n[![kubesdk](https://img.shields.io/pypi/v/kubesdk.svg?label=kubesdk)](https://pypi.org/project/kubesdk)\n[![kube-models](https://img.shields.io/pypi/v/kube-models.svg?label=kube-models)](https://pypi.org/project/kube-models)\n[![kubesdk-cli](https://img.shields.io/pypi/v/kubesdk-cli.svg?label=kubesdk-cli)](https://pypi.org/project/kubesdk-cli)\n[![python versions](https://img.shields.io/pypi/pyversions/kubesdk.svg)](https://pypi.org/project/kubesdk)\n[![coverage](https://img.shields.io/coverallsCoverage/github/puzl-cloud/kubesdk?label=coverage)](https://coveralls.io/github/puzl-cloud/kubesdk)\n[![actions status](https://github.com/puzl-cloud/kubesdk/actions/workflows/publish.yml/badge.svg)](https://github.com/puzl-cloud/kubesdk/actions/workflows/publish.yml)\n\n# kubesdk\n\n`kubesdk` is a modern, async-first Kubernetes client and API model generator for Python.\n- Developer-friendly, with fully typed APIs so IDE auto-complete works reliably across built-in resources and your custom resources. \n- Made for large multi-cluster workloads.  \n- Minimal external dependencies (client itself depends on `aiohttp` and `PyYAML` only).\n\nThe project is split into three packages:\n\n## `kubesdk`\n\nThe core client library, which you install and use in your project.\n\n## `kube-models`\n\nPre-generated Python models for all upstream Kubernetes APIs, for every Kubernetes version **1.23+**. All Kubernetes APIs are bundled under a single `kube-models` package version, so you don’t end up in model-versioning hell. \n\nSeparate models package gives you ability to use latest client version with legacy Kubernetes APIs and vice versa.\n\nYou can find the latest generated models [here](https://github.com/puzl-cloud/kube-models). They are automatically uploaded to an external repository to avoid increasing the size of the main `kubesdk` repo.\n\n## `kubesdk-cli`\n\nCLI that generates models from a live cluster or OpenAPI spec, including your own CRDs.\n\n## Comparison with other Python clients\n\n| Feature / Library                  | **kubesdk** | kubernetes-asyncio | Official client (`kubernetes`) | kr8s     | lightkube |\n|------------------------------------|-------------|-------------------|------------------------------|----------|----------|\n| Async client                       | ✅           | ✅                 | ✗                            | ✅        | ✅        |\n| IDE-friendly client methods typing | ✅ Full      | ◑ Partial         | ◑ Partial                    | ◑ Partial | ✅ Good   |\n| Typed models for all built-in APIs | ✅           | ✅                 | ✅                            | ◑ Partial | ✅        |\n| Built-in multi-cluster ergonomics  | ✅           | ◑ Manual          | ◑ Manual                     | ◑ Manual | ◑ Manual |\n| Easy API model generation (CLI)    | ✅           | ✗                 | ✗                            | ◑        | ◑        |\n| High-level JSON Patch helpers (typed)      | ✅           | ✗                 | ✗                            | ✗        | ✗        |\n| One API surface for core + CRDs    | ✅           | ✗                 | ✗                            | ◑        | ✅        |\n| Separated API models package       | ✅           | ✗                 | ✗                            | ✗        | ✅        |\n| Performance on large-scale workloads | ✅ \u003e1000 RPS | ✅ \u003e1000 RPS       | \u003c100 RPS                     | \u003c100 RPS | \u003c100 RPS |\n\n### Benchmark\n\n[Benchmark](https://github.com/puzl-cloud/k8s-clients-bench) results were collected against **[kind](https://github.com/kubernetes-sigs/kind) (Kubernetes in Docker)**, which provides a fast, consistent local environment for comparing client overhead under the same cluster conditions.\n\n![Benchmark results](https://raw.githubusercontent.com/puzl-cloud/k8s-clients-bench/refs/heads/main/python_kubernetes_clients_benchmark.png)\n\n## Installation\n\n```bash\npip install kubesdk[cli]\n```\n\n## Quick examples\n\n### Create and read resource\n\n```python\nimport asyncio\n\nfrom kube_models.apis_apps_v1.io.k8s.api.apps.v1 import (\n    Deployment,\n    DeploymentSpec,\n    LabelSelector,\n)\nfrom kube_models.api_v1.io.k8s.api.core.v1 import (\n    PodTemplateSpec,\n    PodSpec,\n    Container,\n)\nfrom kube_models.api_v1.io.k8s.apimachinery.pkg.apis.meta.v1 import ObjectMeta\n\nfrom kubesdk import login, create_k8s_resource, get_k8s_resource\n\n\nasync def main() -\u003e None:\n    # Load available cluster config and establish cluster connection process-wide\n    await login()\n\n    deployment = Deployment(\n        metadata=ObjectMeta(name=\"example-nginx\", namespace=\"default\"),\n        spec=DeploymentSpec(\n            replicas=2,\n            selector=LabelSelector(matchLabels={\"app\": \"example-nginx\"}),\n            template=PodTemplateSpec(\n                metadata=ObjectMeta(labels={\"app\": \"example-nginx\"}),\n                spec=PodSpec(\n                    containers=[\n                        Container(\n                            name=\"nginx\",\n                            image=\"nginx:stable\",\n                        )\n                    ]\n                ),\n            ),\n        ),\n    )\n\n    # Create the Deployment\n    await create_k8s_resource(deployment)\n\n    # Read it back\n    created = await get_k8s_resource(Deployment, \"example-nginx\", \"default\")\n    \n    # IDE autocomplete works here\n    print(\"Container name:\", created.spec.template.spec.containers[0].name)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n### Watch resources\n\n```python\nimport asyncio\n\nfrom kube_models.apis_apps_v1.io.k8s.api.apps.v1 import Deployment\nfrom kubesdk import login, watch_k8s_resources\n\n\nasync def main() -\u003e None:\n    await login()\n\n    async for event in watch_k8s_resources(Deployment, namespace=\"default\"):\n        deploy = event.object\n        print(event.type, deploy.metadata.name)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n### Delete resources\n\n```python\nimport asyncio\n\nfrom kube_models.apis_apps_v1.io.k8s.api.apps.v1 import Deployment\nfrom kubesdk import login, delete_k8s_resource\n\n\nasync def main() -\u003e None:\n    await login()\n    await delete_k8s_resource(Deployment, \"example-nginx\", \"default\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n### Patch resource\n\n```python\nfrom dataclasses import replace\n\nfrom kube_models.api_v1.io.k8s.api.core.v1 import LimitRange, LimitRangeSpec, LimitRangeItem\nfrom kube_models.api_v1.io.k8s.apimachinery.pkg.apis.meta.v1 import OwnerReference, ObjectMeta\n\nfrom kubesdk import create_k8s_resource, update_k8s_resource, from_root_, path_, replace_\n\n\nasync def patch_limit_range() -\u003e None:\n    \"\"\"\n    Example: bump PVC min storage and add an OwnerReference in a single,\n    server-side patch. kubesdk will compute the diff between `latest` and\n    `updated` and pick the best patch type (strategic/merge) automatically.\n    \"\"\"\n    # Create the initial LimitRange object.\n    namespace = \"default\"\n    initial_range = LimitRange(\n        metadata=ObjectMeta(\n            name=\"example-limit-range\",\n            namespace=namespace,\n        ),\n        spec=LimitRangeSpec(\n            limits=[\n                LimitRangeItem(\n                    type=\"PersistentVolumeClaim\",\n                    min={\"storage\": \"1Gi\"},\n                )\n            ]\n        ),\n    )\n\n    # The client returns the latest version from the API server.\n    latest: LimitRange = await create_k8s_resource(initial_range)\n\n    #\n    # We want to make a few modifications, will do them one by one. \n    # First, append a new OwnerReference.\n    #\n    # IDE autocomplete works here\n    owner_ref_path = path_(from_root_(LimitRange).metadata.ownerReferences)\n    updated_range = replace_(\n        latest,\n        \n        # IDE autocomplete works here\n        path=owner_ref_path,\n        \n        # Typecheck works here\n        new_value=latest.metadata.ownerReferences + [\n            OwnerReference(\n                uid=\"9153e39d-87d1-46b2-b251-5f6636c30610\",\n                apiVersion=\"v1\",\n                kind=\"Secret\",\n                name=\"test-secret-1\",\n            ),\n        ]\n    )\n    \n    #\n    # Then, set a new list of limits with updated PVC min storage.\n    #\n    # IDE autocomplete works here\n    limits_path = path_(from_root_(LimitRange).spec.limits)\n    updated_range = replace_(\n        updated_range,\n        \n        # IDE autocomplete works here\n        path=limits_path,\n        \n        # Typecheck works here\n        new_value=[\n            replace(lim, min={\"storage\": \"3Gi\"})\n            if lim.type == \"PersistentVolumeClaim\" else lim\n            for lim in latest.spec.limits\n        ]\n    )\n\n    update_all_changed_fields = True\n    # Let kubesdk compute the diff and patch everything that changed\n    if update_all_changed_fields:\n        await update_k8s_resource(updated_range, built_from_latest=latest)\n\n    # Or, restrict the patch to specific paths only (optional)\n    else:\n        await update_k8s_resource(\n            updated_range,\n            built_from_latest=latest,\n            paths=[owner_ref_path, limits_path],\n        )\n```\n\n### Working with multiple clusters\n\n```python\nimport asyncio\nfrom dataclasses import replace\n\nfrom kubesdk import login, KubeConfig, ServerInfo, watch_k8s_resources, create_or_update_k8s_resource, \\\n    delete_k8s_resource, WatchEventType\nfrom kube_models.api_v1.io.k8s.api.core.v1 import Secret\n\n\nasync def sync_secrets_between_clusters(src_cluster: ServerInfo, dst_cluster: ServerInfo):\n    src_ns, dst_ns = \"default\", \"test-kubesdk\"\n    async for event in watch_k8s_resources(Secret, namespace=src_ns, server=src_cluster.server):\n        if event.type == WatchEventType.ERROR:\n            status = event.object\n            raise Exception(f\"Failed to watch Secrets: {status.data}\")\n\n        # Optional\n        if event.type == WatchEventType.BOOKMARK:\n            continue\n\n        # Sync Secret on any other event\n        src_secret = event.object\n        if event.type == WatchEventType.DELETED:\n            # Try to delete, skip if not found\n            await delete_k8s_resource(\n                Secret, src_secret.metadata.name, dst_ns, server=dst_cluster.server, return_api_exceptions=[404])\n            continue\n\n        dst_secret = replace(\n            src_secret,\n            metadata=replace(src_secret.metadata, namespace=dst_ns,\n                # Drop all k8s runtime fields\n                uid=None,\n                resourceVersion=None,\n                managedFields=None))\n\n        # If the Secret exists, a patch is applied; if it doesn't, it will be created.\n        await create_or_update_k8s_resource(dst_secret, server=dst_cluster.server)\n        print(f\"Secret {dst_secret.metadata.name} has been synced \"\n              f\"from `{src_ns}` ns in {src_cluster.server} to `{dst_ns}` ns in {dst_cluster.server}\")\n\n\nasync def main():\n    default = await login()\n    eu_finland_1 = await login(kubeconfig=KubeConfig(context_name=\"eu-finland-1.clusters.puzl.cloud\"))\n\n    # Endless syncing loop\n    while True:\n        try:\n            await sync_secrets_between_clusters(default, eu_finland_1)\n        except Exception as e:\n            print(e)\n            await asyncio.sleep(5)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n### Custom Resource Definitions\n\nYou can generate your custom resource models from your Kubernetes cluster API directly using CLI. Another option is to define them manually. Below is the example of a `FeatureFlag` CR.\n\n#### Operator\n\nA `FeatureFlag` CR is a simple k8s resource that drives a progressive rollout by updating Nginx `Ingress` canary annotations (assumed you are using Nginx). \n- Operator watches `FeatureFlag` objects and sets `nginx.ingress.kubernetes.io/canary=true` and `nginx.ingress.kubernetes.io/canary-weight=\u003c0..100\u003e` on the referenced `spec.canary_ingress`. \n- When the flag is disabled or resource is deleted, the operator forces the canary weight to `0` (no canary traffic).\n\n```python\n# operator.py\nfrom __future__ import annotations\n\nimport asyncio\nfrom dataclasses import dataclass\n\nfrom kubesdk import login, watch_k8s_resources, update_k8s_resource, WatchEventType, path_, from_root_, replace_, \\\n    K8sAPIRequestLoggingConfig\nfrom kubesdk.crd import CustomK8sResourceDefinition, CustomK8sResource, crd_field, PrinterColumn, CRDFieldSpec\nfrom kube_models import Loadable\nfrom kube_models.api_v1.io.k8s.apimachinery.pkg.apis.meta import ObjectMeta\nfrom kube_models.apis_networking_k8s_io_v1.io.k8s.api.networking.v1 import Ingress\n\n# Log each API request\nfrom kubesdk.client import DEFAULT_LOGGING\nDEFAULT_LOGGING.on_success = True\n\n\n@dataclass(kw_only=True, frozen=True, slots=True)\nclass FeatureFlagSpec(Loadable):\n    # You can use standard k8s property settings in CRDFieldSpec\n    enabled: bool = crd_field(spec=CRDFieldSpec(default=False))\n    rollout_percent: int = crd_field(spec=CRDFieldSpec(minimum=0, maximum=100, default=0))\n\n    # Name of the canary Ingress (points to canary Service)\n    # PrinterColumn will show this field's value in `Ingress` column in kubectl output\n    canary_ingress: str = crd_field(spec=CRDFieldSpec(printer_column=PrinterColumn(name=\"Ingress\")))\n\n\n@dataclass(kw_only=True, frozen=True, slots=True)\nclass FeatureFlagV1Alpha1(CustomK8sResource):\n    is_namespaced_ = True\n    group_ = \"my-beautiful-saas.com\"\n    plural_ = \"featureflags\"\n\n    apiVersion = f\"{group_}/v1alpha1\"\n    kind = \"FeatureFlag\"\n\n    spec: FeatureFlagSpec\n\n\n@dataclass\nclass FeatureFlagCRD(CustomK8sResourceDefinition):\n    versions = [FeatureFlagV1Alpha1]\n    crd_short_names_ = [\"ff\"]\n\n\nasync def operator():\n    finalizer_name = FeatureFlagV1Alpha1.group_\n    await login()\n\n    async for event in watch_k8s_resources(FeatureFlagV1Alpha1):\n        if event.type == WatchEventType.BOOKMARK:\n            continue\n\n        flag, meta = event.object, event.object.metadata\n        deleting = meta.deletionTimestamp is not None\n        actually_enabled = False if deleting or event.type == WatchEventType.DELETED else flag.spec.enabled\n        weight = int(flag.spec.rollout_percent or 0) if actually_enabled else 0\n\n        # Add finalizer on create/normal updates (so we clean up on delete safely)\n        fin_path = path_(from_root_(FeatureFlagV1Alpha1).metadata.finalizers)\n        if not deleting and event.type != WatchEventType.DELETED and finalizer_name not in meta.finalizers:\n            new_finalizers = meta.finalizers + [finalizer_name]\n            updated_flag = replace_(flag, fin_path, new_finalizers)\n            await update_k8s_resource(updated_flag, paths=[fin_path])  # patch finalizers only\n\n        new_annotations = {\n            \"nginx.ingress.kubernetes.io/canary\": \"true\",\n            \"nginx.ingress.kubernetes.io/canary-weight\": str(weight)\n        }\n        desired_ingress = Ingress(metadata=ObjectMeta(\n            name=flag.spec.canary_ingress,\n            namespace=meta.namespace,\n            annotations=new_annotations\n        ))\n\n        annotations_path = path_(from_root_(Ingress).metadata.annotations)  # patch annotations only\n        await update_k8s_resource(desired_ingress, paths=[annotations_path])\n\n        # On delete: remove finalizer so the CR can be deleted\n        if deleting and finalizer_name in meta.finalizers:\n            new_finalizers = [f for f in meta.finalizers if f != finalizer_name]\n            updated_flag = replace_(flag, fin_path, new_finalizers)\n            do_not_log_404 = K8sAPIRequestLoggingConfig(not_error_statuses=[404])\n            await update_k8s_resource(updated_flag, paths=[fin_path], return_api_exceptions=[404], log=do_not_log_404)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(operator())\n```\n\n#### CRD\n\nBefore running the operator, you need to generate and apply your CRD in the Kubernetes cluster. Call generator in the dir with your `operator.py` from above:\n\n```shell\nkubesdk generate crd --from-dir . --output ./my-crd\nkubectl apply -f ./my-crd/featureflags.my-beautiful-saas.yaml\n```\n\n#### Run and test the operator\n\n1. Create demo `Ingress` resource\n\n```yaml\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: checkout-canary\n  namespace: default\nspec:\n  ingressClassName: nginx\n  rules:\n    - host: checkout-canary.local\n      http:\n        paths:\n          - path: /\n            pathType: Prefix\n            backend:\n              service:\n                name: dummy-service\n                port:\n                  number: 80\n```\n\n```shell\nkubectl apply -f checkout-canary-ingress.yaml\n```\n\n2. Apply your `FeatureFlag` custom resource spec into cluster\n\n```yaml\napiVersion: my-beautiful-saas.com/v1alpha1\nkind: FeatureFlag\nmetadata:\n  name: checkout-canary  # the same as Ingress metadata.name\n  namespace: default  # in the same namespace \nspec:\n  enabled: true\n  rollout_percent: 20\n  canary_ingress: checkout-canary\n```\n\n```shell\nkubectl apply -f checkout-canary-feature-flag.yaml\n```\n\n3. Check both annotations' values\n\n```shell\nkubectl get ingress checkout-canary -n default -o jsonpath=\"{.metadata.annotations.nginx\\.ingress\\.kubernetes\\.io/canary}{'\\n'}{.metadata.annotations.nginx\\.ingress\\.kubernetes\\.io/canary-weight}{'\\n'}\"\n```\n\nThe command must return\n\n```\ntrue\n20\n```\n\n### CLI\n\nGenerate models directly from a live cluster OpenAPI:\n\n```shell\nkubesdk generate models \\\n  --url https://my-cluster.example.com:6443 \\\n  --output ./kube_models \\\n  --module-name kube_models \\\n  --http-headers \"Authorization: Bearer $(cat /path/to/token)\" \\\n  --skip-tls\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpuzl-cloud%2Fkubesdk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpuzl-cloud%2Fkubesdk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpuzl-cloud%2Fkubesdk/lists"}