Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retrieve application and pod statuses #1332

Merged
merged 15 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
68 changes: 10 additions & 58 deletions plugins/appresources.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,8 @@
import os
from crmetrics import CRBase

class AppResourcesFinder(CRBase):

def _run_command(self, cmd):
cmdOut = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate()
out = cmdOut[0].decode('utf-8')
err = cmdOut[1].decode('utf-8')
return out, err

def _get_resources(self, kind, plural, targetNS, kubeconfig):
cmd = "kubectl get " + plural + " -n " + targetNS + " " + kubeconfig
out, err = self._run_command(cmd)
resources = []
for line in out.split("\n"):
res_details = {}
line = line.strip()
if 'NAME' not in line and line != '' and line != '\n':
line1 = ' '.join(line.split())
parts = line1.split(" ")
res_name = parts[0].strip()
res_details['name'] = res_name
res_details['namespace'] = targetNS
res_details['kind'] = kind
resources.append(res_details)
return resources
class AppResourcesFinder(CRBase):

def get_kubeplus_ns(self, kubeconfig):
cmd = 'kubectl get deployments -A ' + kubeconfig
Expand All @@ -43,7 +21,7 @@ def get_kubeplus_ns(self, kubeconfig):
return kubeplus_ns

def get_target_ns(self, kubeplus_ns, kind, instance, kubeconfig):
cmd = 'kubectl get ' + kind + ' ' + instance + " -n " + kubeplus_ns + ' -o json ' + kubeconfig
cmd = 'kubectl get ' + kind + ' ' + instance + " -n " + kubeplus_ns + ' -o json ' + kubeconfig
out, err = self._run_command(cmd)
targetNS = ''
releaseName = ''
Expand All @@ -56,10 +34,10 @@ def get_target_ns(self, kubeplus_ns, kind, instance, kubeconfig):
targetNS = parts[0].strip()
releaseName = parts[1].strip().split("\n")[0]
return targetNS, releaseName
return targetNS, releaseName
return targetNS, releaseName

def get_helm_resources(self, targetNS, helmrelease, kubeconfig):
#print("Inside helm_resources")
# print("Inside helm_resources")
cmd = "helm get all " + helmrelease + " -n " + targetNS + ' ' + kubeconfig
out, err = self._run_command(cmd)

Expand All @@ -74,7 +52,7 @@ def get_helm_resources(self, targetNS, helmrelease, kubeconfig):
res_details = {}
res_details['name'] = res_name
res_details['namespace'] = targetNS
res_details['kind'] = kind
res_details['kind'] = kind
resources.append(res_details)

new_resource = False
Expand All @@ -96,40 +74,14 @@ def get_pods(self, targetNS, kind, instance, kubeconfig):
resources = self._get_resources('Pod', 'pods', targetNS, kubeconfig)
return resources

def check_res_exists(self, kind, instance, kubeconfig):
cmd = 'kubectl get ' + kind + ' -A ' + kubeconfig
out, err = self._run_command(cmd)
for line in out.split("\n"):
if instance in line:
parts = line.split(" ")
ns = parts[0].strip()
return True, ns, ''
return False, '', kind + ' ' + instance + ' not found.'

def verify_kind_is_consumerapi(self, kind, kubeconfig):

if kind.lower() in 'resourcecompositions':
return False

cmd = 'kubectl get crds ' + kubeconfig
out, err = self._run_command(cmd)
for line in out.split("\n"):
parts = line.split(" ")
fqn = parts[0].strip()
parts1 = fqn.split(".")
plural = parts1[0]
singular = plural[0:len(plural)-1]
if kind.lower() == singular:
return True
return False

if __name__ == '__main__':
appResourcesFinder = AppResourcesFinder()
kind = sys.argv[1]
instance = sys.argv[2]
kubeconfig = sys.argv[3]

#print("kind:" + kind + " instance:" + instance + " kubeconfig:" + kubeconfig)
# print("kind:" + kind + " instance:" + instance + " kubeconfig:" + kubeconfig)

valid_consumer_api = appResourcesFinder.verify_kind_is_consumerapi(kind, kubeconfig)
if not valid_consumer_api:
Expand All @@ -148,7 +100,7 @@ def verify_kind_is_consumerapi(self, kind, kubeconfig):
targetNS, helmrelease = appResourcesFinder.get_target_ns(res_ns, kind, instance, kubeconfig)
if targetNS == '' and helmrelease == '':
print("No Helm release found for {} resource {}".format(kind, instance))
#print(targetNS + " " + helmrelease)
# print(targetNS + " " + helmrelease)
pods = appResourcesFinder.get_pods(targetNS, kind, instance, kubeconfig)
networkpolicies = appResourcesFinder.get_networkpolicies(targetNS, kind, instance, kubeconfig)
resourcequotas = appResourcesFinder.get_resourcequotas(targetNS, kind, instance, kubeconfig)
Expand All @@ -162,10 +114,10 @@ def verify_kind_is_consumerapi(self, kind, kubeconfig):

# Ref: https://www.educba.com/python-print-table/
# https://stackoverflow.com/questions/20309255/how-to-pad-a-string-to-a-fixed-length-with-spaces-in-python
print ("{:<25} {:<25} {:<25} ".format("NAMESPACE", "KIND", "NAME"))
print ("{:<25} {:<25} {:<25} ".format(kubeplus_ns, kind, instance))
print("{:<25} {:<25} {:<25} ".format("NAMESPACE", "KIND", "NAME"))
print("{:<25} {:<25} {:<25} ".format(kubeplus_ns, kind, instance))
for res in allresources:
ns = res['namespace']
kind = res['kind']
name = res['name']
print ("{:<25} {:<25} {:<25} ".format(ns, kind, name))
print("{:<25} {:<25} {:<25} ".format(ns, kind, name))
107 changes: 107 additions & 0 deletions plugins/appstatus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import subprocess
import sys
import json
from crmetrics import CRBase

'''
new plugin app-status -- takes in kind and instance and displays
kind, namespace, name and status and lists pods in that application
with their status

application name is the namespace and pods will be under this namespace
for application status itself, name would just be the helm release

Input: name of kind and name of application instance

TODO: reexamine all error checks for plugins
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get rid of TODO

'''


class AppStatusFinder(CRBase):

def get_app_instance_status(self, kind, instance, kubeconfig):
cmd = 'kubectl get %s %s -o json %s' % (kind, instance, kubeconfig)
out, err = self._run_command(cmd)
if err != "":
print("Something went wrong while getting app instance status.")
print(err)
exit(1)
'''
with response, check if status exists
check if helmrelease exists and extract name of instance
or check if status contains an error
otherwise (i.e. status missing), display "App not deployed properly"
'''
# response = json.dumps(json.loads(out), indent=4)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete.

response = json.loads(out)
if 'status' in response:
if 'helmrelease' in response['status']:
helm_release = response['status']['helmrelease'].strip('\n')
ns, name = helm_release.split(':')
return name, ns, True, None
else:
# an error has occurred
status = response['status']
return status, None, False, None
Copy link
Contributor

@devdattakulkarni devdattakulkarni Aug 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest defining variables for the second and third return parameters and setting their values after lines 21 and 25. That way, by looking at the return statement it will be clear what parameters are being returned.


else:
return '', 'Application not deployed properly', False, None




def get_app_pods(self, namespace, kubeconfig):
cmd = 'kubectl get pods -n %s %s -o json' % (namespace, kubeconfig)
# pods = self._get_resources(None, 'pods', namespace, kubeconfig)
out, err = self._run_command(cmd)
# format?
response = json.loads(out)
pods = []
for pod in response['items']:
name = pod['metadata']['name']
typ = pod['kind']
ns = pod['metadata']['namespace']
phase = pod['status']['phase']
pods.append((name, typ, ns, phase))
return pods


if __name__ == '__main__':
appStatusFinder = AppStatusFinder()
kind = sys.argv[1]
instance = sys.argv[2]
kubeconfig = sys.argv[3]

valid_consumer_api = appStatusFinder.verify_kind_is_consumerapi(kind, kubeconfig)
if not valid_consumer_api:
print(("{} is not a valid Consumer API.").format(kind))
exit(0)

res_exists, ns, err = appStatusFinder.check_res_exists(kind, instance, kubeconfig)
if not res_exists:
print(err)
exit(0)

working, error = appStatusFinder.validate_kind_and_instance(kind, instance, ns)
if working == False:
print(err)
exit(1)

release_name_or_status, release_ns, deployed, err = appStatusFinder.get_app_instance_status(kind, instance, kubeconfig)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need the last parameter since it is always returned as None.

if err is not None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need this check. The error check is happening in lines 12-15.

print(err)
exit(1)

if deployed:
deploy_str = 'Deployed'
else:
print(release_name_or_status)
exit(1)

# if not deployed, there's probably an error -- maybe display a different response in this case
pods = appStatusFinder.get_app_pods(instance, kubeconfig)

print("{:<35} {:<35} {:<35} {:<35}".format("NAME", "TYPE", "NAMESPACE", "STATUS"))
print("{:<35} {:<35} {:<35} {:<35}".format(release_name_or_status, 'helmrelease', release_ns, deploy_str))
for pod_name, typ, pod_ns, phase in pods:
print("{:<35} {:<35} {:<35} {:<35}".format(pod_name, typ, pod_ns, phase))
65 changes: 64 additions & 1 deletion plugins/crmetrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
import platform
import pprint
import time

import yaml
import utils

class CRBase(object):
def _run_command(self, cmd):
cmdOut = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate()
out = cmdOut[0].decode('utf-8')
err = cmdOut[1].decode('utf-8')
return out, err

def parse_pod_details(self, out, instance):
pod_list = []
Expand Down Expand Up @@ -114,6 +119,64 @@ def get_resources_connections(self, kind, instance, namespace, kubeconfig):
print(e)
return json_output

def _get_resources(self, kind, plural, targetNS, kubeconfig):
cmd = "kubectl get " + plural + " -n " + targetNS + " " + kubeconfig
out, err = self._run_command(cmd)
resources = []
for line in out.split("\n"):
res_details = {}
line = line.strip()
if 'NAME' not in line and line != '' and line != '\n':
line1 = ' '.join(line.split())
parts = line1.split(" ")
res_name = parts[0].strip()
res_details['name'] = res_name
res_details['namespace'] = targetNS
res_details['kind'] = kind
resources.append(res_details)
return resources

def check_res_exists(self, kind, instance, kubeconfig):
cmd = 'kubectl get ' + kind + ' -A ' + kubeconfig
out, err = self._run_command(cmd)
for line in out.split("\n"):
if instance in line:
parts = line.split(" ")
ns = parts[0].strip()
return True, ns, ''
return False, '', kind + ' ' + instance + ' not found.'

def verify_kind_is_consumerapi(self, kind, kubeconfig):

if kind.lower() in 'resourcecompositions':
return False

cmd = 'kubectl get crds ' + kubeconfig
out, err = self._run_command(cmd)
for line in out.split("\n"):
parts = line.split(" ")
fqn = parts[0].strip()
parts1 = fqn.split(".")
plural = parts1[0]
singular = plural[0:len(plural) - 1]
if kind.lower() == singular:
return True
return False

'''
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this TODO?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I'm removing it now -- the method that that TODO describes was written right below it.

TODO: add method that accepts kind name (HelloWorldService), instance name (hs1), and namespace
(KUBEPLUS_NS)
it will run `kubectl get kind_name instance_name -n namespace` and if this errors out, no further check
is needed and the user must be informed
otherwise, proceed as normal
'''
def validate_kind_and_instance(self, kind, instance, namespace):
cmd = 'kubectl get %s %s -n %s' % (kind, instance, namespace)
_, err = self._run_command(cmd)
if err == '': # or None?
return True, None
return False, err


class CRMetrics(CRBase):

Expand Down
53 changes: 53 additions & 0 deletions plugins/kubectl-appstatus
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/bin/bash
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have made this file executable, right? (chmod +x kubectl-appstatus)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.


source utils.sh

print_help () {
echo "NAME"
echo " kubectl appstatus"
echo ""
echo "SYNOPSIS"
echo " kubectl appstatus <Kind> <Instance> -k <Absolute path to kubeconfig>"
echo ""
echo "DESCRIPTION"
echo " kubectl appstatus shows the status of the application instance and its pods"
exit 0
}

if (( $# < 2 )); then
print_help
fi

kind=$1
instance=$2
kubeconfig=""

shift;
shift;

while getopts ":k:" opt; do
case ${opt} in
k )
kubeconfig1=$OPTARG
if [ ! -f $kubeconfig1 ]; then
echo "Kubeconfig $kubeconfig1 does not exist."
exit 0
fi;;
? )
echo "Invalid option: ${1} " 1>&2
print_help
exit 0
;;
esac
done

kubeconfig="--kubeconfig="$kubeconfig1

canonicalKind=$(get_canonical_kind $kind)

if [[ $canonicalKind == *"Unknown"* ]]; then
echo "$canonicalKind"
exit 0
fi

python3 /$KUBEPLUS_HOME/plugins/appstatus.py $canonicalKind $instance $kubeconfig
Loading
Loading