|
| 1 | +import os |
| 2 | +import time |
| 3 | +from typing import List |
| 4 | + |
| 5 | +from gcore import Gcore |
| 6 | +from gcore.types.cloud.baremetal_flavor import BaremetalFlavor |
| 7 | +from gcore.types.cloud.k8s_cluster_version import K8SClusterVersion |
| 8 | +from gcore.types.cloud.k8s.cluster_create_params import Pool |
| 9 | + |
| 10 | + |
| 11 | +def main() -> None: |
| 12 | + # TODO set API key before running |
| 13 | + # api_key = os.environ["GCORE_API_KEY"] |
| 14 | + # TODO set cloud project ID before running |
| 15 | + # cloud_project_id = os.environ["GCORE_CLOUD_PROJECT_ID"] |
| 16 | + # TODO set cloud region ID before running |
| 17 | + # cloud_region_id = os.environ["GCORE_CLOUD_REGION_ID"] |
| 18 | + # TODO set a pre-existing SSH keypair name before running. The K8s cluster |
| 19 | + # create endpoint requires a keypair to bootstrap nodes. |
| 20 | + keypair_name = os.environ["GCORE_CLOUD_K8S_KEYPAIR_NAME"] |
| 21 | + |
| 22 | + gcore = Gcore( |
| 23 | + # No need to explicitly pass to Gcore constructor if using environment variables |
| 24 | + # api_key=api_key, |
| 25 | + # cloud_project_id=cloud_project_id, |
| 26 | + # cloud_region_id=cloud_region_id, |
| 27 | + ) |
| 28 | + |
| 29 | + # Look up a small non-GPU flavor and the latest version to use for cluster creation. |
| 30 | + flavors = list_flavors(client=gcore) |
| 31 | + pool_flavor_id = _pick_pool_flavor(flavors) |
| 32 | + |
| 33 | + versions = list_versions(client=gcore) |
| 34 | + cluster_version = _pick_create_version(versions) |
| 35 | + |
| 36 | + # Cluster lifecycle. |
| 37 | + cluster_name = create_cluster( |
| 38 | + client=gcore, |
| 39 | + flavor_id=pool_flavor_id, |
| 40 | + version=cluster_version, |
| 41 | + keypair_name=keypair_name, |
| 42 | + ) |
| 43 | + |
| 44 | + get_cluster(client=gcore, cluster_name=cluster_name) |
| 45 | + list_clusters(client=gcore) |
| 46 | + update_cluster(client=gcore, cluster_name=cluster_name) |
| 47 | + |
| 48 | + # Upgrade the cluster to the newest available version, if one exists. |
| 49 | + upgrade_versions = list_upgrade_versions(client=gcore, cluster_name=cluster_name) |
| 50 | + if upgrade_versions: |
| 51 | + upgrade_cluster(client=gcore, cluster_name=cluster_name, target_version=upgrade_versions[-1].version) |
| 52 | + # The upgrade task finishes before the cluster itself settles back to |
| 53 | + # Provisioned, so subsequent mutations (e.g. creating a new pool) can |
| 54 | + # be rejected with "Cluster or pool is not ready". Wait it out. |
| 55 | + wait_for_cluster_ready(client=gcore, cluster_name=cluster_name) |
| 56 | + else: |
| 57 | + print(f"No upgrade versions available for cluster {cluster_name}; skipping upgrade") |
| 58 | + |
| 59 | + get_certificate(client=gcore, cluster_name=cluster_name) |
| 60 | + get_kubeconfig(client=gcore, cluster_name=cluster_name) |
| 61 | + |
| 62 | + # Pool operations. |
| 63 | + list_pools(client=gcore, cluster_name=cluster_name) |
| 64 | + first_pool = _get_first_pool(client=gcore, cluster_name=cluster_name) |
| 65 | + |
| 66 | + check_pool_quota(client=gcore, flavor_id=pool_flavor_id) |
| 67 | + second_pool = create_pool(client=gcore, cluster_name=cluster_name, flavor_id=pool_flavor_id) |
| 68 | + update_pool(client=gcore, cluster_name=cluster_name, pool_name=second_pool) |
| 69 | + resize_pool(client=gcore, cluster_name=cluster_name, pool_name=second_pool) |
| 70 | + list_pool_nodes(client=gcore, cluster_name=cluster_name, pool_name=second_pool) |
| 71 | + delete_pool(client=gcore, cluster_name=cluster_name, pool_name=second_pool) |
| 72 | + |
| 73 | + # Ensure the initial pool is still reachable after the second pool is gone. |
| 74 | + get_pool(client=gcore, cluster_name=cluster_name, pool_name=first_pool) |
| 75 | + |
| 76 | + list_cluster_nodes(client=gcore, cluster_name=cluster_name) |
| 77 | + |
| 78 | + # Final cleanup. |
| 79 | + delete_cluster(client=gcore, cluster_name=cluster_name) |
| 80 | + |
| 81 | + |
| 82 | +def create_cluster(*, client: Gcore, flavor_id: str, version: str, keypair_name: str) -> str: |
| 83 | + print("\n=== CREATE K8S CLUSTER ===") |
| 84 | + cluster = client.cloud.k8s.clusters.create_and_poll( |
| 85 | + name="gcore-py-example-k8s", |
| 86 | + keypair=keypair_name, |
| 87 | + version=version, |
| 88 | + pools=[ |
| 89 | + Pool( |
| 90 | + name="gcore-python-pool", |
| 91 | + flavor_id=flavor_id, |
| 92 | + min_node_count=1, |
| 93 | + max_node_count=2, |
| 94 | + boot_volume_size=50, |
| 95 | + boot_volume_type="standard", |
| 96 | + auto_healing_enabled=True, |
| 97 | + is_public_ipv4=True, |
| 98 | + servergroup_policy="soft-anti-affinity", |
| 99 | + labels={"pool": "gcore-python-example"}, |
| 100 | + ), |
| 101 | + ], |
| 102 | + ) |
| 103 | + print(f"Created K8s cluster: name={cluster.name}, version={cluster.version}, status={cluster.status}") |
| 104 | + print("==========================") |
| 105 | + return cluster.name |
| 106 | + |
| 107 | + |
| 108 | +def get_cluster(*, client: Gcore, cluster_name: str) -> None: |
| 109 | + print("\n=== GET K8S CLUSTER ===") |
| 110 | + cluster = client.cloud.k8s.clusters.get(cluster_name=cluster_name) |
| 111 | + print( |
| 112 | + f"K8s cluster: name={cluster.name}, version={cluster.version}, " |
| 113 | + f"status={cluster.status}, pools={len(cluster.pools)}" |
| 114 | + ) |
| 115 | + print("=======================") |
| 116 | + |
| 117 | + |
| 118 | +def list_clusters(*, client: Gcore) -> None: |
| 119 | + print("\n=== LIST K8S CLUSTERS ===") |
| 120 | + clusters = client.cloud.k8s.clusters.list() |
| 121 | + if not clusters.results: |
| 122 | + print(" No k8s clusters found.") |
| 123 | + for count, cluster in enumerate(clusters.results, 1): |
| 124 | + print(f" {count}. K8s cluster: name={cluster.name}, version={cluster.version}, status={cluster.status}") |
| 125 | + print("=========================") |
| 126 | + |
| 127 | + |
| 128 | +def update_cluster(*, client: Gcore, cluster_name: str) -> None: |
| 129 | + print("\n=== UPDATE K8S CLUSTER ===") |
| 130 | + # Override a cluster-autoscaler parameter — a safe, idempotent change that |
| 131 | + # exercises update_and_poll. |
| 132 | + cluster = client.cloud.k8s.clusters.update_and_poll( |
| 133 | + cluster_name=cluster_name, |
| 134 | + autoscaler_config={"scale-down-unneeded-time": "5m"}, |
| 135 | + ) |
| 136 | + print(f"Updated K8s cluster: name={cluster.name}, autoscaler_config={cluster.autoscaler_config}") |
| 137 | + print("==========================") |
| 138 | + |
| 139 | + |
| 140 | +def list_upgrade_versions(*, client: Gcore, cluster_name: str) -> List[K8SClusterVersion]: |
| 141 | + print("\n=== LIST K8S CLUSTER UPGRADE VERSIONS ===") |
| 142 | + versions = client.cloud.k8s.clusters.list_versions_for_upgrade(cluster_name=cluster_name) |
| 143 | + for count, version in enumerate(versions.results, 1): |
| 144 | + print(f" {count}. Upgrade version: {version.version}") |
| 145 | + if not versions.results: |
| 146 | + print(" Cluster is already on the latest available version.") |
| 147 | + print("==========================================") |
| 148 | + return versions.results |
| 149 | + |
| 150 | + |
| 151 | +def upgrade_cluster(*, client: Gcore, cluster_name: str, target_version: str) -> None: |
| 152 | + print("\n=== UPGRADE K8S CLUSTER ===") |
| 153 | + cluster = client.cloud.k8s.clusters.upgrade_and_poll(cluster_name=cluster_name, version=target_version) |
| 154 | + print(f"Upgraded K8s cluster: name={cluster.name}, version={cluster.version}") |
| 155 | + print("===========================") |
| 156 | + |
| 157 | + |
| 158 | +def get_certificate(*, client: Gcore, cluster_name: str) -> None: |
| 159 | + print("\n=== GET K8S CLUSTER CERTIFICATE ===") |
| 160 | + cert = client.cloud.k8s.clusters.get_certificate(cluster_name=cluster_name) |
| 161 | + print(f"Certificate bytes={len(cert.certificate)}, Key bytes={len(cert.key)}") |
| 162 | + print("===================================") |
| 163 | + |
| 164 | + |
| 165 | +def wait_for_cluster_ready(*, client: Gcore, cluster_name: str) -> None: |
| 166 | + print("\n=== WAIT FOR K8S CLUSTER READY ===") |
| 167 | + poll_interval = 10 |
| 168 | + max_attempts = 60 |
| 169 | + for attempt in range(1, max_attempts + 1): |
| 170 | + cluster = client.cloud.k8s.clusters.get(cluster_name=cluster_name) |
| 171 | + pending: List[str] = [] |
| 172 | + if cluster.status != "Provisioned": |
| 173 | + pending.append(f"cluster={cluster.status}") |
| 174 | + for pool in cluster.pools: |
| 175 | + if pool.status != "Running": |
| 176 | + pending.append(f"pool[{pool.name}]={pool.status}") |
| 177 | + if not pending: |
| 178 | + print(f"Cluster {cluster_name} is ready (all pools Running)") |
| 179 | + print("==================================") |
| 180 | + return |
| 181 | + print(f" attempt {attempt}: waiting on {' '.join(pending)}") |
| 182 | + time.sleep(poll_interval) |
| 183 | + raise TimeoutError(f"Cluster {cluster_name} did not become ready after {max_attempts} attempts") |
| 184 | + |
| 185 | + |
| 186 | +def get_kubeconfig(*, client: Gcore, cluster_name: str) -> None: |
| 187 | + print("\n=== GET K8S KUBECONFIG ===") |
| 188 | + kubeconfig = client.cloud.k8s.clusters.kubeconfig.get(cluster_name=cluster_name) |
| 189 | + print( |
| 190 | + f"Kubeconfig: host={kubeconfig.host}, created_at={kubeconfig.created_at}, " |
| 191 | + f"expires_at={kubeconfig.expires_at}, bytes={len(kubeconfig.config)}" |
| 192 | + ) |
| 193 | + print("==========================") |
| 194 | + |
| 195 | + |
| 196 | +def list_pools(*, client: Gcore, cluster_name: str) -> None: |
| 197 | + print("\n=== LIST K8S CLUSTER POOLS ===") |
| 198 | + pools = client.cloud.k8s.clusters.pools.list(cluster_name=cluster_name) |
| 199 | + if not pools.results: |
| 200 | + print(" No pools found.") |
| 201 | + for count, pool in enumerate(pools.results, 1): |
| 202 | + print( |
| 203 | + f" {count}. Pool: name={pool.name}, flavor={pool.flavor_id}, " |
| 204 | + f"node_count={pool.node_count}, status={pool.status}" |
| 205 | + ) |
| 206 | + print("==============================") |
| 207 | + |
| 208 | + |
| 209 | +def get_pool(*, client: Gcore, cluster_name: str, pool_name: str) -> None: |
| 210 | + print("\n=== GET K8S CLUSTER POOL ===") |
| 211 | + pool = client.cloud.k8s.clusters.pools.get(pool_name=pool_name, cluster_name=cluster_name) |
| 212 | + print( |
| 213 | + f"Pool: name={pool.name}, flavor={pool.flavor_id}, node_count={pool.node_count}, " |
| 214 | + f"min={pool.min_node_count}, max={pool.max_node_count}, status={pool.status}" |
| 215 | + ) |
| 216 | + print("============================") |
| 217 | + |
| 218 | + |
| 219 | +def check_pool_quota(*, client: Gcore, flavor_id: str) -> None: |
| 220 | + print("\n=== CHECK K8S POOL QUOTA ===") |
| 221 | + quota = client.cloud.k8s.clusters.pools.check_quota( |
| 222 | + flavor_id=flavor_id, |
| 223 | + name="gcore-python-pool-2", |
| 224 | + min_node_count=1, |
| 225 | + max_node_count=2, |
| 226 | + boot_volume_size=50, |
| 227 | + servergroup_policy="soft-anti-affinity", |
| 228 | + ) |
| 229 | + print(f"CPU: limit={quota.cpu_count_limit}, requested={quota.cpu_count_requested}, usage={quota.cpu_count_usage}") |
| 230 | + print(f"RAM (MiB): limit={quota.ram_limit}, requested={quota.ram_requested}, usage={quota.ram_usage}") |
| 231 | + print( |
| 232 | + f"Volumes: count_limit={quota.volume_count_limit}, count_usage={quota.volume_count_usage}, " |
| 233 | + f"size_limit={quota.volume_size_limit} GiB" |
| 234 | + ) |
| 235 | + print("============================") |
| 236 | + |
| 237 | + |
| 238 | +def create_pool(*, client: Gcore, cluster_name: str, flavor_id: str) -> str: |
| 239 | + print("\n=== CREATE K8S CLUSTER POOL ===") |
| 240 | + pool = client.cloud.k8s.clusters.pools.create_and_poll( |
| 241 | + cluster_name=cluster_name, |
| 242 | + name="gcore-python-pool-2", |
| 243 | + flavor_id=flavor_id, |
| 244 | + min_node_count=1, |
| 245 | + max_node_count=2, |
| 246 | + boot_volume_size=50, |
| 247 | + boot_volume_type="standard", |
| 248 | + auto_healing_enabled=True, |
| 249 | + is_public_ipv4=True, |
| 250 | + servergroup_policy="soft-anti-affinity", |
| 251 | + labels={"pool": "gcore-python-example-2"}, |
| 252 | + ) |
| 253 | + print(f"Created pool: name={pool.name}, flavor={pool.flavor_id}, node_count={pool.node_count}") |
| 254 | + print("===============================") |
| 255 | + return pool.name |
| 256 | + |
| 257 | + |
| 258 | +def update_pool(*, client: Gcore, cluster_name: str, pool_name: str) -> None: |
| 259 | + print("\n=== UPDATE K8S CLUSTER POOL ===") |
| 260 | + pool = client.cloud.k8s.clusters.pools.update( |
| 261 | + pool_name=pool_name, |
| 262 | + cluster_name=cluster_name, |
| 263 | + labels={"pool": "gcore-python-example-2", "stage": "updated"}, |
| 264 | + ) |
| 265 | + print(f"Updated pool: name={pool.name}, labels={pool.labels}") |
| 266 | + print("===============================") |
| 267 | + |
| 268 | + |
| 269 | +def resize_pool(*, client: Gcore, cluster_name: str, pool_name: str) -> None: |
| 270 | + print("\n=== RESIZE K8S CLUSTER POOL ===") |
| 271 | + pool = client.cloud.k8s.clusters.pools.resize_and_poll( |
| 272 | + pool_name=pool_name, |
| 273 | + cluster_name=cluster_name, |
| 274 | + node_count=2, |
| 275 | + ) |
| 276 | + print(f"Resized pool: name={pool.name}, node_count={pool.node_count}") |
| 277 | + print("===============================") |
| 278 | + |
| 279 | + |
| 280 | +def delete_pool(*, client: Gcore, cluster_name: str, pool_name: str) -> None: |
| 281 | + print("\n=== DELETE K8S CLUSTER POOL ===") |
| 282 | + client.cloud.k8s.clusters.pools.delete_and_poll(pool_name=pool_name, cluster_name=cluster_name) |
| 283 | + print(f"Deleted pool: name={pool_name}") |
| 284 | + print("===============================") |
| 285 | + |
| 286 | + |
| 287 | +def list_cluster_nodes(*, client: Gcore, cluster_name: str) -> None: |
| 288 | + print("\n=== LIST K8S CLUSTER NODES ===") |
| 289 | + nodes = client.cloud.k8s.clusters.nodes.list(cluster_name=cluster_name) |
| 290 | + if not nodes.results: |
| 291 | + print(" No nodes found.") |
| 292 | + for count, node in enumerate(nodes.results, 1): |
| 293 | + print(f" {count}. Node: ID={node.id}, name={node.name}, status={node.status}") |
| 294 | + print("==============================") |
| 295 | + |
| 296 | + |
| 297 | +def list_pool_nodes(*, client: Gcore, cluster_name: str, pool_name: str) -> None: |
| 298 | + print("\n=== LIST K8S POOL NODES ===") |
| 299 | + nodes = client.cloud.k8s.clusters.pools.nodes.list(pool_name=pool_name, cluster_name=cluster_name) |
| 300 | + if not nodes.results: |
| 301 | + print(" No pool nodes found.") |
| 302 | + for count, node in enumerate(nodes.results, 1): |
| 303 | + print(f" {count}. Node: ID={node.id}, name={node.name}, status={node.status}") |
| 304 | + print("===========================") |
| 305 | + |
| 306 | + |
| 307 | +def delete_cluster(*, client: Gcore, cluster_name: str) -> None: |
| 308 | + print("\n=== DELETE K8S CLUSTER ===") |
| 309 | + client.cloud.k8s.clusters.delete_and_poll(cluster_name=cluster_name) |
| 310 | + print(f"Deleted K8s cluster: name={cluster_name}") |
| 311 | + print("==========================") |
| 312 | + |
| 313 | + |
| 314 | +def list_flavors(*, client: Gcore) -> List[BaremetalFlavor]: |
| 315 | + print("\n=== LIST K8S FLAVORS ===") |
| 316 | + flavors = client.cloud.k8s.flavors.list(exclude_gpu=True, include_capacity=True) |
| 317 | + |
| 318 | + display_count = min(3, len(flavors.results)) |
| 319 | + for i in range(display_count): |
| 320 | + flavor = flavors.results[i] |
| 321 | + print( |
| 322 | + f" {i + 1}. Flavor: ID={flavor.flavor_id}, name={flavor.flavor_name}, " |
| 323 | + f"RAM={flavor.ram} MB, VCPUs={flavor.vcpus}" |
| 324 | + ) |
| 325 | + |
| 326 | + if len(flavors.results) > display_count: |
| 327 | + print(f" ... and {len(flavors.results) - display_count} more flavors") |
| 328 | + |
| 329 | + print(f"Total k8s flavors: {len(flavors.results)}") |
| 330 | + print("========================") |
| 331 | + return flavors.results |
| 332 | + |
| 333 | + |
| 334 | +def list_versions(*, client: Gcore) -> List[K8SClusterVersion]: |
| 335 | + print("\n=== LIST K8S VERSIONS ===") |
| 336 | + versions = client.cloud.k8s.list_versions() |
| 337 | + for count, version in enumerate(versions.results, 1): |
| 338 | + print(f" {count}. Version: {version.version}") |
| 339 | + print(f"Total k8s versions: {len(versions.results)}") |
| 340 | + print("=========================") |
| 341 | + return versions.results |
| 342 | + |
| 343 | + |
| 344 | +def _pick_pool_flavor(flavors: List[BaremetalFlavor]) -> str: |
| 345 | + # Prefer the smallest g1-standard VM flavor with capacity; skip disabled, |
| 346 | + # zero-capacity, and test/fake flavors. |
| 347 | + candidates = [ |
| 348 | + f |
| 349 | + for f in flavors |
| 350 | + if not f.disabled |
| 351 | + and (f.capacity or 0) > 0 |
| 352 | + and "test" not in f.flavor_name |
| 353 | + and "fake" not in f.flavor_name |
| 354 | + and f.flavor_name.startswith("g1-standard-") |
| 355 | + ] |
| 356 | + if not candidates: |
| 357 | + raise ValueError("No suitable g1-standard k8s flavor with capacity found") |
| 358 | + |
| 359 | + pick = min(candidates, key=lambda f: f.ram) |
| 360 | + print(f"Selected k8s pool flavor: {pick.flavor_name} (RAM: {pick.ram} MB, VCPUs: {pick.vcpus})") |
| 361 | + return pick.flavor_id |
| 362 | + |
| 363 | + |
| 364 | +def _pick_create_version(versions: List[K8SClusterVersion]) -> str: |
| 365 | + if not versions: |
| 366 | + raise ValueError("No k8s versions available for cluster creation") |
| 367 | + # The API returns versions in ascending order. Pick the penultimate |
| 368 | + # version when possible so the example always has a newer version to |
| 369 | + # upgrade to later. Fall back to the only available version otherwise. |
| 370 | + idx = max(len(versions) - 2, 0) |
| 371 | + picked = versions[idx].version |
| 372 | + print(f"Selected k8s version for creation: {picked}") |
| 373 | + return picked |
| 374 | + |
| 375 | + |
| 376 | +def _get_first_pool(*, client: Gcore, cluster_name: str) -> str: |
| 377 | + pools = client.cloud.k8s.clusters.pools.list(cluster_name=cluster_name) |
| 378 | + if not pools.results: |
| 379 | + raise ValueError(f"Cluster {cluster_name} has no pools") |
| 380 | + return pools.results[0].name |
| 381 | + |
| 382 | + |
| 383 | +if __name__ == "__main__": |
| 384 | + main() |
0 commit comments