Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions managed/devops/opscli/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
'ybops/cloud/gcp',
'ybops/cloud/onprem',
'ybops/cloud/azure',
'ybops/cloud/kubernetes',
'ybops/common',
'ybops/node_agent',
'ybops/utils'
Expand Down
8 changes: 8 additions & 0 deletions managed/devops/opscli/ybops/cloud/common/method.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,11 @@ def get_server_host_port(self, host_info, custom_ssh_port, default_port=False):
"node_agent_ip": host_info["private_ip"],
"node_agent_port": self.extra_vars["node_agent_port"]
}
if connection_type == 'kubectl':
return {
"kubectl_pod": host_info["name"],
"kubectl_namespace": host_info.get("namespace", "default")
}
raise YBOpsRuntimeError("Unknown connection type: {}".format(connection_type))

def get_server_ports_to_check(self, args):
Expand All @@ -379,6 +384,9 @@ def get_server_ports_to_check(self, args):
server_ports.append(int(args.custom_ssh_port))
elif connection_type == 'node_agent_rpc':
server_ports.append(self.extra_vars["node_agent_port"])
elif connection_type == 'kubectl':
# Kubectl doesn't use ports, return empty list
pass
return list(set(server_ports))


Expand Down
1 change: 1 addition & 0 deletions managed/devops/opscli/ybops/cloud/kubernetes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Keep
133 changes: 133 additions & 0 deletions managed/devops/opscli/ybops/cloud/kubernetes/cloud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env python

import logging
import subprocess
from ybops.cloud.common.cloud import AbstractCloud
from ybops.cloud.common.method import AbstractInstancesMethod
from ybops.cloud.kubernetes.command import KubernetesInstanceCommand, KubernetesAccessCommand
from ybops.common.exceptions import YBOpsRuntimeError


class KubernetesCloud(AbstractCloud):
"""Subclass specific to Kubernetes cloud related functionality.
Commands are executed in target pods using kubectl exec.
"""
def __init__(self):
super(KubernetesCloud, self).__init__("kubernetes")

def add_extra_args(self):
"""Override to setup cloud-specific command line flags.
"""
super(KubernetesCloud, self).add_extra_args()
self.parser.add_argument("--namespace", required=False, default="default",
help="Kubernetes namespace")
self.parser.add_argument("--kubeconfig", required=False,
help="Path to kubeconfig file")

def add_subcommands(self):
"""Override to setup the cloud-specific instances of the subcommands.
"""
self.add_subcommand(KubernetesInstanceCommand())
self.add_subcommand(KubernetesAccessCommand())

def _find_pod_by_labels(self, node_name, namespace, kubeconfig=None):
"""Find the actual pod name using Kubernetes label selectors.

Args:
node_name: Simplified node name (e.g., yb-master-0_eu-west-1a or yb-master-0)
namespace: Kubernetes namespace
kubeconfig: Path to kubeconfig file (optional)

Returns:
Tuple of (pod_name, container_name) where container_name is 'yb-master' or 'yb-tserver'
"""
# Parse the node name to extract server type, index, and zone
# Format: yb-master-0 or yb-master-0_eu-west-1a
parts = node_name.split('_')
base_name = parts[0] # yb-master-0 or yb-tserver-0
zone = parts[1] if len(parts) > 1 else None # eu-west-1a (optional)

# Determine server type and extract pod index
# The server type is also the container name
if base_name.startswith('yb-master-'):
server_type = 'yb-master'
pod_index = base_name[len('yb-master-'):]
elif base_name.startswith('yb-tserver-'):
server_type = 'yb-tserver'
pod_index = base_name[len('yb-tserver-'):]
else:
raise YBOpsRuntimeError(
f"Invalid node name format: {node_name}. Expected yb-master-N or yb-tserver-N")

# Build kubectl command with label selectors
kubectl_cmd = ["kubectl", "get", "pods"]

if kubeconfig:
kubectl_cmd.extend(["--kubeconfig", kubeconfig])

kubectl_cmd.extend(["-n", namespace])

# Build label selector (comma-separated for AND logic)
labels = f"app.kubernetes.io/name={server_type},apps.kubernetes.io/pod-index={pod_index}"
if zone:
labels += f",yugabyte.io/zone={zone}"

kubectl_cmd.extend(["-l", labels])

kubectl_cmd.extend(["-o", "jsonpath={.items[0].metadata.name}"])

logging.debug(f"Finding pod with command: {' '.join(kubectl_cmd)}")

try:
result = subprocess.run(
kubectl_cmd,
capture_output=True,
text=True,
check=True
)

if not result.stdout.strip():
zone_info = f", zone={zone}" if zone else ""
raise YBOpsRuntimeError(
f"No pod found for node '{node_name}' with labels "
f"component={server_type}, pod-index={pod_index}{zone_info}")

pod_name = result.stdout.strip()
return pod_name, server_type

except subprocess.CalledProcessError as e:
raise YBOpsRuntimeError(f"Failed to find pod for node '{node_name}': {e.stderr}")

def get_host_info(self, args, get_all=False):
"""Override to get host info for Kubernetes pods.

For kubernetes, search_pattern is the simplified node name (e.g., yb-tserver-0_eu-west-1b).
We use label selectors to find the actual pod name.
"""
node_name = args.search_pattern if hasattr(args, 'search_pattern') else None

if not node_name:
return None

namespace = args.namespace if hasattr(args, 'namespace') else "default"
kubeconfig = args.kubeconfig if hasattr(args, 'kubeconfig') else None

# Find the actual pod name and container using label selectors
actual_pod_name, container_name = self._find_pod_by_labels(node_name, namespace, kubeconfig)

result = dict(
name=actual_pod_name, # Use actual pod name for kubectl exec
# For kubernetes, we use kubectl exec, not direct IP
public_ip=actual_pod_name,
private_ip=actual_pod_name,
region=args.region if hasattr(args, 'region') else "local",
zone=args.zone if hasattr(args, 'zone') else "local",
namespace=namespace,
container=container_name, # Include container name derived from node name
instance_type="kubernetes-pod",
server_type=AbstractInstancesMethod.YB_SERVER_TYPE
)

if get_all:
return [result]
return result
48 changes: 48 additions & 0 deletions managed/devops/opscli/ybops/cloud/kubernetes/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env python

from ybops.cloud.common.command import InstanceCommand, AccessCommand
from ybops.cloud.common.method import (
AccessEditVaultMethod, AccessCreateVaultMethod, AccessDeleteKeyMethod
)
from ybops.cloud.kubernetes.method import (
KubernetesCreateInstancesMethod, KubernetesProvisionInstancesMethod,
KubernetesListInstancesMethod, KubernetesAccessAddKeyMethod,
KubernetesConfigureInstancesMethod, KubernetesInitYSQLMethod,
KubernetesCronCheckMethod, KubernetesTransferXClusterCerts,
KubernetesRunHooks, KubernetesWaitForConnection,
KubernetesManageOtelCollector
)


class KubernetesInstanceCommand(InstanceCommand):
"""Subclass for Kubernetes specific instance command.
Most methods are reused from common, only override what's specific to kubernetes.
"""
def __init__(self):
super(KubernetesInstanceCommand, self).__init__()

def add_methods(self):
# All Kubernetes methods use kubectl exec instead of SSH
self.add_method(KubernetesProvisionInstancesMethod(self))
self.add_method(KubernetesCreateInstancesMethod(self))
self.add_method(KubernetesListInstancesMethod(self))
self.add_method(KubernetesConfigureInstancesMethod(self))
self.add_method(KubernetesInitYSQLMethod(self))
self.add_method(KubernetesCronCheckMethod(self))
self.add_method(KubernetesTransferXClusterCerts(self))
self.add_method(KubernetesRunHooks(self))
self.add_method(KubernetesWaitForConnection(self))
self.add_method(KubernetesManageOtelCollector(self))


class KubernetesAccessCommand(AccessCommand):
"""Subclass for Kubernetes specific access command.
"""
def __init__(self):
super(KubernetesAccessCommand, self).__init__()

def add_methods(self):
self.add_method(KubernetesAccessAddKeyMethod(self))
self.add_method(AccessCreateVaultMethod(self))
self.add_method(AccessEditVaultMethod(self))
self.add_method(AccessDeleteKeyMethod(self))
Loading