gcpdiag.queries.gke
39class NodeConfig: 40 """Represents a GKE node pool configuration.""" 41 42 def __init__(self, resource_data): 43 self._resource_data = resource_data 44 45 def has_accelerators(self) -> bool: 46 if 'accelerators' in self._resource_data: 47 return True 48 return False 49 50 @property 51 def machine_type(self) -> str: 52 return self._resource_data['machineType'] 53 54 @property 55 def image_type(self) -> str: 56 return self._resource_data['imageType'] 57 58 @property 59 def oauth_scopes(self) -> list: 60 return self._resource_data['oauthScopes'] 61 62 @property 63 def has_serial_port_logging_enabled(self) -> bool: 64 """Check if serial port logging is enabled in the node config. 65 66 Returns: 67 bool: True if serial port logging is enabled or not explicitly disabled. 68 False if explicitly disabled. 69 """ 70 metadata = self._resource_data.get('metadata', {}) 71 return metadata.get('serial-port-logging-enable', 'true').lower() == 'true'
Represents a GKE node pool configuration.
62 @property 63 def has_serial_port_logging_enabled(self) -> bool: 64 """Check if serial port logging is enabled in the node config. 65 66 Returns: 67 bool: True if serial port logging is enabled or not explicitly disabled. 68 False if explicitly disabled. 69 """ 70 metadata = self._resource_data.get('metadata', {}) 71 return metadata.get('serial-port-logging-enable', 'true').lower() == 'true'
Check if serial port logging is enabled in the node config.
Returns:
bool: True if serial port logging is enabled or not explicitly disabled. False if explicitly disabled.
74class NodePool(models.Resource): 75 """Represents a GKE node pool.""" 76 77 version: Version 78 79 def __init__(self, cluster, resource_data): 80 super().__init__(project_id=cluster.project_id) 81 self._cluster = cluster 82 self._resource_data = resource_data 83 self.version = Version(self._resource_data['version']) 84 self._migs = None 85 86 def _get_service_account(self) -> str: 87 return self._resource_data.get('config', {}).get('serviceAccount', None) 88 89 @property 90 def full_path(self) -> str: 91 # https://container.googleapis.com/v1/projects/gcpdiag-gke1-aaaa/ 92 # locations/europe-west1/clusters/gke2/nodePools/default-pool 93 m = re.match( 94 r'https://container.googleapis.com/v1/(.*)', 95 self._resource_data.get('selfLink', ''), 96 ) 97 if not m: 98 raise RuntimeError("can't parse selfLink of nodepool resource") 99 return m.group(1) 100 101 @property 102 def short_path(self) -> str: 103 path = self.full_path 104 path = re.sub(r'^projects/', '', path) 105 path = re.sub(r'/locations/', '/', path) 106 path = re.sub(r'/zones/', '/', path) 107 path = re.sub(r'/clusters/', '/', path) 108 path = re.sub(r'/nodePools/', '/', path) 109 return path 110 111 @property 112 def name(self) -> str: 113 return self._resource_data['name'] 114 115 @property 116 def config(self) -> NodeConfig: 117 return NodeConfig(self._resource_data['config']) 118 119 @property 120 def node_count(self) -> int: 121 return self._resource_data.get('initialNodeCount', 0) 122 123 def has_default_service_account(self) -> bool: 124 sa = self._get_service_account() 125 return sa == 'default' 126 127 def has_image_streaming_enabled(self) -> bool: 128 return get_path(self._resource_data, ('config', 'gcfsConfig', 'enabled'), default=False) 129 130 def has_md_concealment_enabled(self) -> bool: 131 # Empty ({}) workloadMetadataConfig means that 'Metadata concealment' 132 # (predecessor of Workload Identity) is enabled. 133 # https://cloud.google.com/kubernetes-engine/docs/how-to/protecting-cluster-metadata#concealment 134 return ( 135 get_path( 136 self._resource_data, 137 ('config', 'workloadMetadataConfig'), 138 default=None, 139 ) 140 == {} 141 ) 142 143 def has_workload_identity_enabled(self) -> bool: 144 # 'Metadata concealment' (workloadMetadataConfig == {}) doesn't protect the 145 # default SA's token 146 return bool( 147 get_path( 148 self._resource_data, 149 ('config', 'workloadMetadataConfig'), 150 default=None, 151 ) 152 ) 153 154 @property 155 def service_account(self) -> str: 156 sa = self._get_service_account() 157 if sa == 'default': 158 project_nr = crm.get_project(self.project_id).number 159 return f'{project_nr}-compute@developer.gserviceaccount.com' 160 else: 161 return sa 162 163 @property 164 def pod_ipv4_cidr_size(self) -> int: 165 return self._resource_data['podIpv4CidrSize'] 166 167 @property 168 def pod_ipv4_cidr_block(self) -> Optional[IPv4NetOrIPv6Net]: 169 # Get the pod cidr range in use by the nodepool 170 pod_cidr = get_path(self._resource_data, ('networkConfig', 'podIpv4CidrBlock'), default=None) 171 172 if pod_cidr: 173 return ipaddress.ip_network(pod_cidr) 174 else: 175 return None 176 177 @property 178 def max_pod_per_node(self) -> int: 179 return int( 180 get_path( 181 self._resource_data, 182 ('maxPodsConstraint', 'maxPodsPerNode'), 183 default=DEFAULT_MAX_PODS_PER_NODE, 184 ) 185 ) 186 187 @property 188 def cluster(self) -> 'Cluster': 189 return self._cluster 190 191 @property 192 def instance_groups(self) -> List[gce.ManagedInstanceGroup]: 193 if self._migs is None: 194 project_migs_by_selflink = {} 195 for m in gce.get_managed_instance_groups(models.Context(project_id=self.project_id)).values(): 196 project_migs_by_selflink[m.self_link] = m 197 198 self._migs = [] 199 for url in self._resource_data.get('instanceGroupUrls', []): 200 try: 201 self._migs.append(project_migs_by_selflink[url]) 202 except KeyError: 203 continue 204 return self._migs 205 206 @property 207 def node_tags(self) -> List[str]: 208 """Returns the firewall tags used for nodes in this cluster. 209 210 If the node tags can't be determined, [] is returned. 211 """ 212 migs = self.instance_groups 213 if not migs: 214 return [] 215 return migs[0].template.tags 216 217 def get_machine_type(self) -> str: 218 """Returns the machine type of the nodepool nodes""" 219 return self.config.machine_type
Represents a GKE node pool.
89 @property 90 def full_path(self) -> str: 91 # https://container.googleapis.com/v1/projects/gcpdiag-gke1-aaaa/ 92 # locations/europe-west1/clusters/gke2/nodePools/default-pool 93 m = re.match( 94 r'https://container.googleapis.com/v1/(.*)', 95 self._resource_data.get('selfLink', ''), 96 ) 97 if not m: 98 raise RuntimeError("can't parse selfLink of nodepool resource") 99 return m.group(1)
Returns the full path of this resource.
Example: 'projects/gcpdiag-gke-1-9b90/zones/europe-west4-a/clusters/gke1'
101 @property 102 def short_path(self) -> str: 103 path = self.full_path 104 path = re.sub(r'^projects/', '', path) 105 path = re.sub(r'/locations/', '/', path) 106 path = re.sub(r'/zones/', '/', path) 107 path = re.sub(r'/clusters/', '/', path) 108 path = re.sub(r'/nodePools/', '/', path) 109 return path
Returns the short name for this resource.
Note that it isn't clear from this name what kind of resource it is.
Example: 'gke1'
130 def has_md_concealment_enabled(self) -> bool: 131 # Empty ({}) workloadMetadataConfig means that 'Metadata concealment' 132 # (predecessor of Workload Identity) is enabled. 133 # https://cloud.google.com/kubernetes-engine/docs/how-to/protecting-cluster-metadata#concealment 134 return ( 135 get_path( 136 self._resource_data, 137 ('config', 'workloadMetadataConfig'), 138 default=None, 139 ) 140 == {} 141 )
167 @property 168 def pod_ipv4_cidr_block(self) -> Optional[IPv4NetOrIPv6Net]: 169 # Get the pod cidr range in use by the nodepool 170 pod_cidr = get_path(self._resource_data, ('networkConfig', 'podIpv4CidrBlock'), default=None) 171 172 if pod_cidr: 173 return ipaddress.ip_network(pod_cidr) 174 else: 175 return None
191 @property 192 def instance_groups(self) -> List[gce.ManagedInstanceGroup]: 193 if self._migs is None: 194 project_migs_by_selflink = {} 195 for m in gce.get_managed_instance_groups(models.Context(project_id=self.project_id)).values(): 196 project_migs_by_selflink[m.self_link] = m 197 198 self._migs = [] 199 for url in self._resource_data.get('instanceGroupUrls', []): 200 try: 201 self._migs.append(project_migs_by_selflink[url]) 202 except KeyError: 203 continue 204 return self._migs
222class UndefinedClusterPropertyError(Exception): 223 """Thrown when a property of a cluster can't be determined for some reason. 224 225 For example, the cluster_hash can't be determined because there are no 226 nodepools defined. 227 """ 228 229 pass
Thrown when a property of a cluster can't be determined for some reason.
For example, the cluster_hash can't be determined because there are no nodepools defined.
232class Cluster(models.Resource): 233 """Represents a GKE cluster. 234 235 https://cloud.google.com/kubernetes-engine/docs/reference/rest/v1/projects.locations.clusters#Cluster 236 """ 237 238 _resource_data: dict 239 master_version: Version 240 _context: models.Context 241 _nodepools: Optional[List[NodePool]] 242 243 def __init__(self, project_id, resource_data, context: models.Context): 244 super().__init__(project_id=project_id) 245 self._resource_data = resource_data 246 self.master_version = Version(self._resource_data['currentMasterVersion']) 247 self._nodepools = None 248 self._context = context 249 250 @property 251 def full_path(self) -> str: 252 if utils.is_region(self._resource_data['location']): 253 return f'projects/{self.project_id}/locations/{self.location}/clusters/{self.name}' 254 else: 255 return f'projects/{self.project_id}/zones/{self.location}/clusters/{self.name}' 256 257 @property 258 def short_path(self) -> str: 259 path = self.full_path 260 path = re.sub(r'^projects/', '', path) 261 path = re.sub(r'/locations/', '/', path) 262 path = re.sub(r'/zones/', '/', path) 263 path = re.sub(r'/clusters/', '/', path) 264 return path 265 266 @property 267 def name(self) -> str: 268 return self._resource_data['name'] 269 270 @property 271 def location(self) -> str: 272 return self._resource_data['location'] 273 274 @property 275 def pod_ipv4_cidr(self) -> IPv4NetOrIPv6Net: 276 cidr = self._resource_data['clusterIpv4Cidr'] 277 return ipaddress.ip_network(cidr) 278 279 @property 280 def current_node_count(self) -> int: 281 return self._resource_data.get('currentNodeCount', 0) 282 283 @property 284 def release_channel(self) -> Optional[str]: 285 try: 286 return self._resource_data['releaseChannel']['channel'] 287 except KeyError: 288 return None 289 290 @property 291 def nap_node_image_type(self) -> Optional[str]: 292 return get_path( 293 self._resource_data, 294 ('autoscaling', 'autoprovisioningNodePoolDefaults', 'imageType'), 295 default=None, 296 ) 297 298 @property 299 def app_layer_sec_key(self) -> str: 300 return self._resource_data['databaseEncryption'].get('keyName') 301 302 @property 303 def status(self) -> str: 304 return self._resource_data['status'] 305 306 @property 307 def status_message(self) -> str: 308 return self._resource_data.get('statusMessage', None) 309 310 def has_app_layer_enc_enabled(self) -> bool: 311 # state := 'DECRYPTED' | 'ENCRYPTED', keyName := 'full_path_to_key_resouce' 312 return ( 313 get_path(self._resource_data, ('databaseEncryption', 'state'), default=None) == 'ENCRYPTED' 314 ) 315 316 def has_logging_enabled(self) -> bool: 317 return self._resource_data['loggingService'] != 'none' 318 319 def enabled_logging_components(self) -> List[str]: 320 return self._resource_data['loggingConfig']['componentConfig']['enableComponents'] 321 322 def has_monitoring_enabled(self) -> bool: 323 return self._resource_data['monitoringService'] != 'none' 324 325 def enabled_monitoring_components(self) -> List[str]: 326 return self._resource_data['monitoringConfig']['componentConfig']['enableComponents'] 327 328 def has_control_plane_logging_enabled(self) -> bool: 329 if not self.has_logging_enabled(): 330 return False 331 components = get_path( 332 self._resource_data, ('loggingConfig', 'componentConfig', 'enableComponents'), default=[] 333 ) 334 return all(c in components for c in ['API_SERVER', 'SCHEDULER', 'CONTROLLER_MANAGER']) 335 336 def has_control_plane_monitoring_enabled(self) -> bool: 337 if not self.has_monitoring_enabled(): 338 return False 339 components = get_path( 340 self._resource_data, ('monitoringConfig', 'componentConfig', 'enableComponents'), default=[] 341 ) 342 return all(c in components for c in ['API_SERVER', 'SCHEDULER', 'CONTROLLER_MANAGER']) 343 344 def has_authenticator_group_enabled(self) -> bool: 345 return len(self._resource_data.get('authenticatorGroupsConfig', {})) > 0 346 347 def has_workload_identity_enabled(self) -> bool: 348 return len(self._resource_data.get('workloadIdentityConfig', {})) > 0 349 350 def has_http_load_balancing_enabled(self) -> bool: 351 # HTTP load balancing needs to be enabled to use GKE ingress 352 return ( 353 get_path( 354 self._resource_data, 355 ('addonsConfig', 'httpLoadBalancing', 'disabled'), 356 default=None, 357 ) 358 is not True 359 ) 360 361 def has_network_policy_enabled(self) -> bool: 362 # Network policy enforcement 363 return ( 364 get_path( 365 self._resource_data, 366 ('addonsConfig', 'networkPolicyConfig', 'disabled'), 367 default=False, 368 ) 369 is not True 370 ) 371 372 def has_dpv2_enabled(self) -> bool: 373 # Checks whether dataplane V2 is enabled in clusters 374 return ( 375 get_path( 376 self._resource_data, 377 ('networkConfig', 'datapathProvider'), 378 default=None, 379 ) 380 == 'ADVANCED_DATAPATH' 381 ) 382 383 def has_intra_node_visibility_enabled(self) -> bool: 384 if ( 385 'networkConfig' in self._resource_data 386 and 'enableIntraNodeVisibility' in self._resource_data['networkConfig'] 387 ): 388 return self._resource_data['networkConfig']['enableIntraNodeVisibility'] 389 return False 390 391 def has_maintenance_window(self) -> bool: 392 # 'e3b0c442' is a hexadecimal string that represents the value of an empty 393 # string ('') in cryptography. If the maintenance windows are defined, the 394 # value of 'resourceVersion' is not empty ('e3b0c442'). 395 return self._resource_data['maintenancePolicy']['resourceVersion'] != 'e3b0c442' 396 397 @property 398 def maintenance_policy(self) -> dict: 399 """Returns the maintenance policy of the cluster.""" 400 return self._resource_data.get('maintenancePolicy', {}) 401 402 def has_image_streaming_enabled(self) -> bool: 403 """Check if cluster has Image Streaming (aka Google Container File System) 404 405 enabled 406 """ 407 global_gcsfs = get_path( 408 self._resource_data, 409 ('nodePoolDefaults', 'nodeConfigDefaults', 'gcfsConfig', 'enabled'), 410 default=False, 411 ) 412 # Check nodePoolDefaults settings 413 if global_gcsfs: 414 return True 415 for np in self.nodepools: 416 # Check if any nodepool has image streaming enabled 417 if np.has_image_streaming_enabled(): 418 return True 419 return False 420 421 @property 422 def nodepools(self) -> List[NodePool]: 423 if self._nodepools is None: 424 self._nodepools = [] 425 for n in self._resource_data.get('nodePools', []): 426 self._nodepools.append(NodePool(self, n)) 427 return self._nodepools 428 429 @property 430 def network(self) -> network.Network: 431 # projects/gcpdiag-gke1-aaaa/global/networks/default 432 network_string = self._resource_data['networkConfig']['network'] 433 m = re.match(r'projects/([^/]+)/global/networks/([^/]+)$', network_string) 434 if not m: 435 raise RuntimeError("can't parse network string: %s" % network_string) 436 return network.get_network(m.group(1), m.group(2), self._context) 437 438 @property 439 def subnetwork(self) -> Optional[models.Resource]: 440 # 'projects/gcpdiag-gke1-aaaa/regions/europe-west4/subnetworks/default' 441 if 'subnetwork' not in self._resource_data['networkConfig']: 442 return None 443 444 subnetwork_string = self._resource_data['networkConfig']['subnetwork'] 445 m = re.match( 446 r'projects/([^/]+)/regions/([^/]+)/subnetworks/([^/]+)$', 447 subnetwork_string, 448 ) 449 if not m: 450 raise RuntimeError("can't parse network string: %s" % subnetwork_string) 451 return network.get_subnetwork(m.group(1), m.group(2), m.group(3)) 452 453 @property 454 def get_subnet_name(self) -> Optional[models.Resource]: 455 if 'subnetwork' not in self._resource_data: 456 return None 457 return self._resource_data['subnetwork'] 458 459 @property 460 def get_nodepool_config(self) -> Optional[models.Resource]: 461 if 'nodePools' not in self._resource_data: 462 return None 463 return self._resource_data['nodePools'] 464 465 @property 466 def get_network_string(self) -> str: 467 if 'networkConfig' not in self._resource_data: 468 return '' 469 if 'network' not in self._resource_data['networkConfig']: 470 return '' 471 return self._resource_data['networkConfig']['network'] 472 473 @property 474 def is_private(self) -> bool: 475 if 'privateClusterConfig' not in self._resource_data: 476 return False 477 478 return self._resource_data['privateClusterConfig'].get('enablePrivateNodes', False) 479 480 @property 481 def is_vpc_native(self) -> bool: 482 return get_path( 483 self._resource_data, 484 ('ipAllocationPolicy', 'useIpAliases'), 485 default=False, 486 ) 487 488 @property 489 def is_regional(self) -> bool: 490 return len(self._resource_data['locations']) > 1 491 492 @property 493 def cluster_ca_certificate(self) -> str: 494 return self._resource_data['masterAuth']['clusterCaCertificate'] 495 496 @property 497 def endpoint(self) -> Optional[str]: 498 if 'endpoint' not in self._resource_data: 499 return None 500 return self._resource_data['endpoint'] 501 502 @property 503 def is_autopilot(self) -> bool: 504 if 'autopilot' not in self._resource_data: 505 return False 506 return self._resource_data['autopilot'].get('enabled', False) 507 508 @property 509 def masters_cidr_list(self) -> Iterable[IPv4NetOrIPv6Net]: 510 if get_path( 511 self._resource_data, 512 ('privateClusterConfig', 'masterIpv4CidrBlock'), 513 default=None, 514 ): 515 return [ 516 ipaddress.ip_network(self._resource_data['privateClusterConfig']['masterIpv4CidrBlock']) 517 ] 518 else: 519 # only older clusters still have ssh firewall rules 520 if self.current_node_count and not self.cluster_hash: 521 logging.warning("couldn't retrieve cluster hash for cluster %s.", self.name) 522 return [] 523 fw_rule_name = f'gke-{self.name}-{self.cluster_hash}-ssh' 524 rule = self.network.firewall.get_vpc_ingress_rules(name=fw_rule_name) 525 if rule and rule[0].is_enabled(): 526 return rule[0].source_ranges 527 return [] 528 529 @property 530 def cluster_hash(self) -> Optional[str]: 531 """Returns the "cluster hash" as used in automatic firewall rules for GKE clusters. 532 533 Cluster hash is the first 8 characters of cluster id. See also: 534 https://cloud.google.com/kubernetes-engine/docs/concepts/firewall-rules 535 """ 536 if 'id' in self._resource_data: 537 return self._resource_data['id'][:8] 538 raise UndefinedClusterPropertyError('no id') 539 540 @property 541 def is_nodelocal_dnscache_enabled(self) -> bool: 542 """Returns True if NodeLocal DNSCache is enabled for the cluster.""" 543 addons_config = self._resource_data.get('addonsConfig', {}) 544 dns_cache_config = addons_config.get('dnsCacheConfig', {}) 545 return dns_cache_config.get('enabled', False)
Represents a GKE cluster.
250 @property 251 def full_path(self) -> str: 252 if utils.is_region(self._resource_data['location']): 253 return f'projects/{self.project_id}/locations/{self.location}/clusters/{self.name}' 254 else: 255 return f'projects/{self.project_id}/zones/{self.location}/clusters/{self.name}'
Returns the full path of this resource.
Example: 'projects/gcpdiag-gke-1-9b90/zones/europe-west4-a/clusters/gke1'
257 @property 258 def short_path(self) -> str: 259 path = self.full_path 260 path = re.sub(r'^projects/', '', path) 261 path = re.sub(r'/locations/', '/', path) 262 path = re.sub(r'/zones/', '/', path) 263 path = re.sub(r'/clusters/', '/', path) 264 return path
Returns the short name for this resource.
Note that it isn't clear from this name what kind of resource it is.
Example: 'gke1'
328 def has_control_plane_logging_enabled(self) -> bool: 329 if not self.has_logging_enabled(): 330 return False 331 components = get_path( 332 self._resource_data, ('loggingConfig', 'componentConfig', 'enableComponents'), default=[] 333 ) 334 return all(c in components for c in ['API_SERVER', 'SCHEDULER', 'CONTROLLER_MANAGER'])
336 def has_control_plane_monitoring_enabled(self) -> bool: 337 if not self.has_monitoring_enabled(): 338 return False 339 components = get_path( 340 self._resource_data, ('monitoringConfig', 'componentConfig', 'enableComponents'), default=[] 341 ) 342 return all(c in components for c in ['API_SERVER', 'SCHEDULER', 'CONTROLLER_MANAGER'])
391 def has_maintenance_window(self) -> bool: 392 # 'e3b0c442' is a hexadecimal string that represents the value of an empty 393 # string ('') in cryptography. If the maintenance windows are defined, the 394 # value of 'resourceVersion' is not empty ('e3b0c442'). 395 return self._resource_data['maintenancePolicy']['resourceVersion'] != 'e3b0c442'
397 @property 398 def maintenance_policy(self) -> dict: 399 """Returns the maintenance policy of the cluster.""" 400 return self._resource_data.get('maintenancePolicy', {})
Returns the maintenance policy of the cluster.
402 def has_image_streaming_enabled(self) -> bool: 403 """Check if cluster has Image Streaming (aka Google Container File System) 404 405 enabled 406 """ 407 global_gcsfs = get_path( 408 self._resource_data, 409 ('nodePoolDefaults', 'nodeConfigDefaults', 'gcfsConfig', 'enabled'), 410 default=False, 411 ) 412 # Check nodePoolDefaults settings 413 if global_gcsfs: 414 return True 415 for np in self.nodepools: 416 # Check if any nodepool has image streaming enabled 417 if np.has_image_streaming_enabled(): 418 return True 419 return False
Check if cluster has Image Streaming (aka Google Container File System)
enabled
429 @property 430 def network(self) -> network.Network: 431 # projects/gcpdiag-gke1-aaaa/global/networks/default 432 network_string = self._resource_data['networkConfig']['network'] 433 m = re.match(r'projects/([^/]+)/global/networks/([^/]+)$', network_string) 434 if not m: 435 raise RuntimeError("can't parse network string: %s" % network_string) 436 return network.get_network(m.group(1), m.group(2), self._context)
438 @property 439 def subnetwork(self) -> Optional[models.Resource]: 440 # 'projects/gcpdiag-gke1-aaaa/regions/europe-west4/subnetworks/default' 441 if 'subnetwork' not in self._resource_data['networkConfig']: 442 return None 443 444 subnetwork_string = self._resource_data['networkConfig']['subnetwork'] 445 m = re.match( 446 r'projects/([^/]+)/regions/([^/]+)/subnetworks/([^/]+)$', 447 subnetwork_string, 448 ) 449 if not m: 450 raise RuntimeError("can't parse network string: %s" % subnetwork_string) 451 return network.get_subnetwork(m.group(1), m.group(2), m.group(3))
508 @property 509 def masters_cidr_list(self) -> Iterable[IPv4NetOrIPv6Net]: 510 if get_path( 511 self._resource_data, 512 ('privateClusterConfig', 'masterIpv4CidrBlock'), 513 default=None, 514 ): 515 return [ 516 ipaddress.ip_network(self._resource_data['privateClusterConfig']['masterIpv4CidrBlock']) 517 ] 518 else: 519 # only older clusters still have ssh firewall rules 520 if self.current_node_count and not self.cluster_hash: 521 logging.warning("couldn't retrieve cluster hash for cluster %s.", self.name) 522 return [] 523 fw_rule_name = f'gke-{self.name}-{self.cluster_hash}-ssh' 524 rule = self.network.firewall.get_vpc_ingress_rules(name=fw_rule_name) 525 if rule and rule[0].is_enabled(): 526 return rule[0].source_ranges 527 return []
529 @property 530 def cluster_hash(self) -> Optional[str]: 531 """Returns the "cluster hash" as used in automatic firewall rules for GKE clusters. 532 533 Cluster hash is the first 8 characters of cluster id. See also: 534 https://cloud.google.com/kubernetes-engine/docs/concepts/firewall-rules 535 """ 536 if 'id' in self._resource_data: 537 return self._resource_data['id'][:8] 538 raise UndefinedClusterPropertyError('no id')
Returns the "cluster hash" as used in automatic firewall rules for GKE clusters.
Cluster hash is the first 8 characters of cluster id. See also: https://cloud.google.com/kubernetes-engine/docs/concepts/firewall-rules
540 @property 541 def is_nodelocal_dnscache_enabled(self) -> bool: 542 """Returns True if NodeLocal DNSCache is enabled for the cluster.""" 543 addons_config = self._resource_data.get('addonsConfig', {}) 544 dns_cache_config = addons_config.get('dnsCacheConfig', {}) 545 return dns_cache_config.get('enabled', False)
Returns True if NodeLocal DNSCache is enabled for the cluster.
548@caching.cached_api_call 549def get_clusters(context: models.Context) -> Mapping[str, Cluster]: 550 """Get a list of Cluster matching the given context, indexed by cluster full path.""" 551 clusters: Dict[str, Cluster] = {} 552 if not apis.is_enabled(context.project_id, 'container'): 553 return clusters 554 container_api = apis.get_api('container', 'v1', context.project_id) 555 logging.debug('fetching list of GKE clusters in project %s', context.project_id) 556 query = ( 557 container_api.projects() 558 .locations() 559 .clusters() 560 .list(parent=f'projects/{context.project_id}/locations/-') 561 ) 562 try: 563 resp = query.execute(num_retries=config.API_RETRIES) 564 if 'clusters' not in resp: 565 return clusters 566 for resp_c in resp['clusters']: 567 # verify that we some minimal data that we expect 568 if 'name' not in resp_c or 'location' not in resp_c: 569 raise RuntimeError('missing data in projects.locations.clusters.list response') 570 if not context.match_project_resource( 571 location=resp_c.get('location', ''), 572 labels=resp_c.get('resourceLabels', {}), 573 resource=resp_c.get('name', ''), 574 ): 575 continue 576 c = Cluster(project_id=context.project_id, resource_data=resp_c, context=context) 577 clusters[c.full_path] = c 578 except googleapiclient.errors.HttpError as err: 579 raise utils.GcpApiError(err) from err 580 return clusters
Get a list of Cluster matching the given context, indexed by cluster full path.
583@caching.cached_api_call 584def get_cluster( 585 project_id, 586 cluster_id, 587 location, 588) -> Union[Cluster, None]: 589 """Get a Cluster from project_id of a context.""" 590 if not apis.is_enabled(project_id, 'container'): 591 return None 592 container_api = apis.get_api('container', 'v1', project_id) 593 logging.debug('fetching the GKE cluster %s in project %s', cluster_id, project_id) 594 query = ( 595 container_api.projects() 596 .locations() 597 .clusters() 598 .get(name=f'projects/{project_id}/locations/{location}/clusters/{cluster_id}') 599 ) 600 try: 601 resp = query.execute(num_retries=config.API_RETRIES) 602 if cluster_id not in str(resp): 603 raise RuntimeError('missing data in projects.locations.clusters.list response') 604 except googleapiclient.errors.HttpError as err: 605 raise utils.GcpApiError(err) from err 606 return Cluster( 607 project_id=project_id, 608 resource_data=resp, 609 context=models.Context(project_id=project_id), 610 )
Get a Cluster from project_id of a context.
625def get_valid_master_versions(project_id: str, location: str) -> List[str]: 626 """Get a list of valid GKE master versions.""" 627 server_config = _get_server_config(project_id, location) 628 versions: List[str] = [] 629 630 # channel versions may extend the list of all available versions.\ 631 # Especially for the Rapid channel - many new versions only available in Rapid 632 # channel and not as a static version to make sure nobody stuck on that 633 # version for an extended period of time. 634 for c in server_config['channels']: 635 versions += c['validVersions'] 636 637 versions += server_config['validMasterVersions'] 638 639 return versions
Get a list of valid GKE master versions.
642def get_valid_node_versions(project_id: str, location: str) -> List[str]: 643 """Get a list of valid GKE master versions.""" 644 server_config = _get_server_config(project_id, location) 645 versions: List[str] = [] 646 647 # See explanation in get_valid_master_versions 648 for c in server_config['channels']: 649 versions += c['validVersions'] 650 651 versions += server_config['validNodeVersions'] 652 653 return versions
Get a list of valid GKE master versions.
656class Node(models.Resource): 657 """Represents a GKE node. 658 659 This class useful for example to determine the GKE cluster when you only have 660 an GCE instance id (like from a metrics label). 661 """ 662 663 instance: gce.Instance 664 nodepool: NodePool 665 mig: gce.ManagedInstanceGroup 666 667 def __init__(self, instance, nodepool, mig): 668 super().__init__(project_id=instance.project_id) 669 self.instance = instance 670 self.nodepool = nodepool 671 self.mig = mig 672 pass 673 674 @property 675 def full_path(self) -> str: 676 return self.nodepool.cluster.full_path + '/nodes/' + self.instance.name 677 678 @property 679 def short_path(self) -> str: 680 # return self.nodepool.cluster.short_path + '/' + self.instance.name 681 return self.instance.short_path
Represents a GKE node.
This class useful for example to determine the GKE cluster when you only have an GCE instance id (like from a metrics label).
687@functools.lru_cache() 688def get_node_by_instance_id(context: models.Context, instance_id: str) -> Node: 689 """Get a gke.Node instance by instance id. 690 691 Throws a KeyError in case this instance is not found or isn't part of a GKE 692 cluster. 693 """ 694 # This will throw a KeyError if the instance is not found, which is also 695 # the behavior that we want for this function. 696 instance = gce.get_instances(context)[instance_id] 697 clusters = get_clusters(context) 698 try: 699 # instance.mig throws AttributeError if it isn't part of a mig 700 mig = instance.mig 701 702 # find a NodePool that uses this MIG 703 for c in clusters.values(): 704 for np in c.nodepools: 705 for np_mig in np.instance_groups: 706 if mig == np_mig: 707 return Node(instance=instance, nodepool=np, mig=mig) 708 709 # if we didn't find a nodepool that owns this instance, raise a KeyError 710 raise KeyError("can't determine GKE cluster for instance %s" % (instance_id)) 711 712 except AttributeError as err: 713 raise KeyError from err 714 return None
Get a gke.Node instance by instance id.
Throws a KeyError in case this instance is not found or isn't part of a GKE cluster.
717@caching.cached_api_call 718def get_release_schedule() -> Dict: 719 """Extract the release schedule for gke clusters 720 721 Returns: 722 A dictionary of release schedule. 723 """ 724 page_url = 'https://cloud.google.com/kubernetes-engine/docs/release-schedule' 725 release_data = {} 726 # estimate first month of the quarter 727 quarter_dates = {'Q1': '1', 'Q2': '4', 'Q3': '7', 'Q4': '10'} 728 try: 729 rows = web.fetch_and_extract_table_data( 730 page_url, tag='table', class_name='gke-release-schedule' 731 ) 732 733 # Function to parse a date string or return None for 'N/A' 734 def parse_date(date_str) -> Optional[Union[datetime.date, str]]: 735 if not date_str: 736 return None 737 if 'already reached EOL' in date_str: 738 return 'already reached EOL' 739 p = r'(?P<year>\d{4})-(?:(?P<quarter>Q[1-4])|(?P<month>[0-9]{1,2}))(?:-(?P<day>[0-9]{1,2}))?' 740 match = re.search(p, date_str) 741 # Handle incomplete dates in 'YYYY-MM' form 742 if match and match.group('month') and not match.group('day'): 743 return datetime.date.fromisoformat(f'{date_str}-15') 744 # Handle quarter year (for example, 2025-Q3) approximations that are updated when known. 745 # https://cloud.google.com/kubernetes-engine/docs/release-schedule.md#fn6 746 if match and match.group('quarter') and not match.group('day'): 747 date_str = f'{match.group("year")}-{quarter_dates[match.group("quarter")]}-01' 748 return datetime.date.fromisoformat(date_str) 749 if match and match.group('year') and match.group('month') and match.group('day'): 750 return datetime.date.fromisoformat(date_str) 751 # anything less like N/A return None 752 return None 753 754 # Iterate over each row and extract the data 755 for cols in rows: 756 if not cols: 757 continue 758 if len(cols) < 10: 759 continue 760 minor_version = cols[0] 761 rapid_avail = parse_date(cols[1]) 762 regular_avail = parse_date(cols[3]) 763 stable_avail = parse_date(cols[5]) 764 extended_avail = parse_date(cols[7]) 765 end_of_standard_support = parse_date(cols[9]) 766 end_of_extended_support = parse_date(cols[10]) if len(cols) > 10 else None 767 768 # Add the extracted data into the dictionary in the desired format 769 release_data[minor_version] = { 770 'rapid_avail': rapid_avail, 771 'regular_avail': regular_avail, 772 'stable_avail': stable_avail, 773 'extended_avail': extended_avail, 774 'eol': end_of_standard_support, 775 'extended_eol': end_of_extended_support, 776 } 777 return release_data 778 except ( 779 requests.exceptions.RequestException, 780 AttributeError, 781 TypeError, 782 ValueError, 783 IndexError, 784 ) as e: 785 logging.error('Error in extracting gke release schedule: %s', e) 786 return release_data
Extract the release schedule for gke clusters
Returns:
A dictionary of release schedule.