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

feat: odigos describe sub commands for auto completion #1407

Merged
merged 8 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
211 changes: 123 additions & 88 deletions cli/cmd/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,24 @@ import (
"github.com/odigos-io/odigos/k8sutils/pkg/envoverwrite"
"github.com/odigos-io/odigos/k8sutils/pkg/workload"
"github.com/spf13/cobra"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)

var (
describeNamespaceFlag string
)

type K8sSourceObject struct {
metav1.ObjectMeta `json:"metadata,omitempty"`
Kind string
PodTemplateSpec *v1.PodTemplateSpec
LabelSelector *metav1.LabelSelector
}

func wrapTextInRed(text string) string {
return "\033[31m" + text + "\033[0m"
}
Expand All @@ -43,55 +46,7 @@ func wrapTextSuccessOfFailure(text string, success bool) string {
}
}

func cmdKindToK8sGVR(kind string) (schema.GroupVersionResource, error) {
kind = strings.ToLower(kind)
if kind == "deployment" || kind == "deployments" || kind == "dep" {
return schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}, nil
}
if kind == "statefulset" || kind == "statefulsets" || kind == "sts" {
return schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "statefulsets"}, nil
}
if kind == "daemonset" || kind == "daemonsets" || kind == "ds" {
return schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "daemonsets"}, nil
}

return schema.GroupVersionResource{}, fmt.Errorf("unsupported kind: %s", kind)
}

func extractPodInfo(obj *unstructured.Unstructured) (*v1.PodTemplateSpec, string, error) {
gvk := obj.GroupVersionKind()

switch gvk.Kind {
case "Deployment":
var deployment appsv1.Deployment
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &deployment)
if err != nil {
return nil, "", fmt.Errorf("failed to cast to Deployment: %v", err)
}
return &deployment.Spec.Template, metav1.FormatLabelSelector(deployment.Spec.Selector), nil

case "StatefulSet":
var statefulSet appsv1.StatefulSet
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &statefulSet)
if err != nil {
return nil, "", fmt.Errorf("failed to cast to StatefulSet: %v", err)
}
return &statefulSet.Spec.Template, metav1.FormatLabelSelector(statefulSet.Spec.Selector), nil

case "DaemonSet":
var daemonSet appsv1.DaemonSet
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &daemonSet)
if err != nil {
return nil, "", fmt.Errorf("failed to cast to DaemonSet: %v", err)
}
return &daemonSet.Spec.Template, metav1.FormatLabelSelector(daemonSet.Spec.Selector), nil

default:
return nil, "", fmt.Errorf("unsupported kind: %s", gvk.Kind)
}
}

func getInstrumentationLabelTexts(workload *unstructured.Unstructured, ns *v1.Namespace) (workloadText, nsText, decisionText string, instrumented bool) {
func getInstrumentationLabelTexts(workload *K8sSourceObject, ns *v1.Namespace) (workloadText, nsText, decisionText string, instrumented bool) {
blumamir marked this conversation as resolved.
Show resolved Hide resolved
workloadLabel, workloadFound := workload.GetLabels()[consts.OdigosInstrumentationLabel]
nsLabel, nsFound := ns.GetLabels()[consts.OdigosInstrumentationLabel]

Expand All @@ -110,17 +65,17 @@ func getInstrumentationLabelTexts(workload *unstructured.Unstructured, ns *v1.Na
if workloadFound {
instrumented = workloadLabel == consts.InstrumentationEnabled
if instrumented {
decisionText = "Workload is instrumented because the " + workload.GetKind() + " contains the label '" + consts.OdigosInstrumentationLabel + "=" + workloadLabel + "'"
decisionText = "Workload is instrumented because the " + workload.Kind + " contains the label '" + consts.OdigosInstrumentationLabel + "=" + workloadLabel + "'"
blumamir marked this conversation as resolved.
Show resolved Hide resolved
} else {
decisionText = "Workload is NOT instrumented because the " + workload.GetKind() + " contains the label '" + consts.OdigosInstrumentationLabel + "=" + workloadLabel + "'"
decisionText = "Workload is NOT instrumented because the " + workload.Kind + " contains the label '" + consts.OdigosInstrumentationLabel + "=" + workloadLabel + "'"
}
} else {
instrumented = nsLabel == consts.InstrumentationEnabled
if instrumented {
decisionText = "Workload is instrumented because the " + workload.GetKind() + " is not labeled, and the namespace is labeled with '" + consts.OdigosInstrumentationLabel + "=" + nsLabel + "'"
decisionText = "Workload is instrumented because the " + workload.Kind + " is not labeled, and the namespace is labeled with '" + consts.OdigosInstrumentationLabel + "=" + nsLabel + "'"
} else {
if nsFound {
decisionText = "Workload is NOT instrumented because the " + workload.GetKind() + " is not labeled, and the namespace is labeled with '" + consts.OdigosInstrumentationLabel + "=" + nsLabel + "'"
decisionText = "Workload is NOT instrumented because the " + workload.Kind + " is not labeled, and the namespace is labeled with '" + consts.OdigosInstrumentationLabel + "=" + nsLabel + "'"
} else {
decisionText = "Workload is NOT instrumented because neither the workload nor the namespace has the '" + consts.OdigosInstrumentationLabel + "' label set"
}
Expand All @@ -130,23 +85,15 @@ func getInstrumentationLabelTexts(workload *unstructured.Unstructured, ns *v1.Na
return
}

func getRelevantResources(ctx context.Context, client *kube.Client, ns string, kind string, name string) (workloadObj *unstructured.Unstructured, namespace *corev1.Namespace, instrumentationConfig *odigosv1.InstrumentationConfig, instrumentedApplication *odigosv1.InstrumentedApplication, podTemplate *corev1.PodTemplateSpec, instrumentationInstances *odigosv1.InstrumentationInstanceList, pods *corev1.PodList, err error) {
gvr, err := cmdKindToK8sGVR(kind)
if err != nil {
return
}

workloadObj, err = client.Dynamic.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return
}
func getRelevantResources(ctx context.Context, client *kube.Client, workloadObj *K8sSourceObject) (namespace *corev1.Namespace, instrumentationConfig *odigosv1.InstrumentationConfig, instrumentedApplication *odigosv1.InstrumentedApplication, instrumentationInstances *odigosv1.InstrumentationInstanceList, pods *corev1.PodList, err error) {

ns := workloadObj.GetNamespace()
namespace, err = client.CoreV1().Namespaces().Get(ctx, ns, metav1.GetOptions{})
if err != nil {
return
}

runtimeObjectName := workload.GetRuntimeObjectName(workloadObj.GetName(), workloadObj.GetKind())
runtimeObjectName := workload.GetRuntimeObjectName(workloadObj.GetName(), workloadObj.Kind)
instrumentationConfig, err = client.OdigosClient.InstrumentationConfigs(ns).Get(ctx, runtimeObjectName, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
Expand Down Expand Up @@ -178,8 +125,7 @@ func getRelevantResources(ctx context.Context, client *kube.Client, ns string, k
return
}

var podLabelSelector string
podTemplate, podLabelSelector, err = extractPodInfo(workloadObj)
podLabelSelector := metav1.FormatLabelSelector(workloadObj.LabelSelector)
if err != nil {
// if pod info cannot be extracted, it is an unrecoverable error
return
Expand All @@ -193,10 +139,9 @@ func getRelevantResources(ctx context.Context, client *kube.Client, ns string, k
return
}

func printWorkloadManifestInfo(workloadObj *unstructured.Unstructured, namespace *corev1.Namespace) bool {
fmt.Println("Showing details for Workload")
func printWorkloadManifestInfo(workloadObj *K8sSourceObject, namespace *corev1.Namespace) bool {
fmt.Println("Name: ", workloadObj.GetName())
fmt.Println("Kind: ", workloadObj.GetKind())
fmt.Println("Kind: ", workloadObj.Kind)
fmt.Println("Namespace: ", workloadObj.GetNamespace())

fmt.Println("")
Expand Down Expand Up @@ -279,7 +224,7 @@ func printInstrumentedApplicationInfo(instrumentedApplication *odigosv1.Instrume
}
}

func printAppliedInstrumentationDeviceInfo(workloadObj *unstructured.Unstructured, instrumentedApplication *odigosv1.InstrumentedApplication, podTemplate *corev1.PodTemplateSpec, instrumented bool) map[string][]string {
func printAppliedInstrumentationDeviceInfo(workloadObj *K8sSourceObject, instrumentedApplication *odigosv1.InstrumentedApplication, instrumented bool) map[string][]string {
appliedInstrumentationDeviceStatusMessage := "Unknown"
if !instrumented {
// if the workload is not instrumented, the instrumentation device expected
Expand All @@ -303,7 +248,7 @@ func printAppliedInstrumentationDeviceInfo(workloadObj *unstructured.Unstructure
fmt.Println("Instrumentation Device:")
fmt.Println(" Status:", appliedInstrumentationDeviceStatusMessage)
containerNameToExpectedDevices := make(map[string][]string)
for _, container := range podTemplate.Spec.Containers {
for _, container := range workloadObj.PodTemplateSpec.Spec.Containers {
fmt.Println(" - Container Name:", container.Name)
odigosDevices := make([]string, 0)
for resourceName := range container.Resources.Limits {
Expand Down Expand Up @@ -448,44 +393,134 @@ func printPodsInfo(pods *corev1.PodList, instrumentationInstances *odigosv1.Inst
}
}

// installCmd represents the install command
var describeCmd = &cobra.Command{
Use: "describe",
Short: "Show details of a specific odigos entity",
Long: `Print detailed description of a specific odigos entity, which can be used to troubleshoot issues`,
}

var describeSourceCmd = &cobra.Command{
Use: "source",
Short: "Show details of a specific odigos source",
Long: `Print detailed description of a specific odigos source, which can be used to troubleshoot issues`,
}

func printDescribeSource(ctx context.Context, client *kube.Client, workloadObj *K8sSourceObject) {
namespace, instrumentationConfig, instrumentedApplication, instrumentationInstances, pods, err := getRelevantResources(ctx, client, workloadObj)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}

instrumented := printWorkloadManifestInfo(workloadObj, namespace)
printInstrumentationConfigInfo(instrumentationConfig, instrumented)
printInstrumentedApplicationInfo(instrumentedApplication, instrumented)
containerNameToExpectedDevices := printAppliedInstrumentationDeviceInfo(workloadObj, instrumentedApplication, instrumented)
printPodsInfo(pods, instrumentationInstances, containerNameToExpectedDevices)
}

var describeSourceDeploymentCmd = &cobra.Command{
Use: "deployment <name>",
Short: "Show details of a specific odigos source of type deployment",
Long: `Print detailed description of a specific odigos source of type deployment, which can be used to troubleshoot issues`,
Aliases: []string{"deploy", "deployments", "deploy.apps", "deployment.apps", "deployments.apps"},
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {

client, err := kube.CreateClient(cmd)
if err != nil {
kube.PrintClientErrorAndExit(err)
}

ctx := cmd.Context()
name := args[0]
ns := cmd.Flag("namespace").Value.String()

if len(args) != 2 {
fmt.Println("Usage: odigos describe <kind> <name>")
deployment, err := client.AppsV1().Deployments(ns).Get(ctx, name, metav1.GetOptions{})
Copy link
Contributor

Choose a reason for hiding this comment

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

I see this function happens also at sources.go, login.go, uninstall.go.
Maybe we can create a util file and put the function over there.
Your call..

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Since it's just one line, I prefer to use the k8s client directly and not introduce more complexity to the code

if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
workloadObj := &K8sSourceObject{
Kind: "deployment",
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure but I think deployment.Kind returns this string.
Also other places

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Unfortunately, The Kind property is not populated by the client when instantiating a Deployment object. This is why I needed to add it explicitly

ObjectMeta: deployment.ObjectMeta,
PodTemplateSpec: &deployment.Spec.Template,
LabelSelector: deployment.Spec.Selector,
}
printDescribeSource(cmd.Context(), client, workloadObj)
blumamir marked this conversation as resolved.
Show resolved Hide resolved
},
}

kind := args[0]
name := args[1]
var describeSourceDaemonSetCmd = &cobra.Command{
Use: "daemonset <name>",
Short: "Show details of a specific odigos source of type daemonset",
Long: `Print detailed description of a specific odigos source of type daemonset, which can be used to troubleshoot issues`,
Aliases: []string{"ds", "daemonsets", "ds.apps", "daemonset.apps", "daemonsets.apps"},
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
client, err := kube.CreateClient(cmd)
if err != nil {
kube.PrintClientErrorAndExit(err)
}

workloadObj, namespace, instrumentationConfig, instrumentedApplication, podTemplate, instrumentationInstances, pods, err := getRelevantResources(ctx, client, ns, kind, name)
ctx := cmd.Context()
name := args[0]
ns := cmd.Flag("namespace").Value.String()
ds, err := client.AppsV1().DaemonSets(ns).Get(ctx, name, metav1.GetOptions{})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
workloadObj := &K8sSourceObject{
Kind: "daemonset",
ObjectMeta: ds.ObjectMeta,
PodTemplateSpec: &ds.Spec.Template,
LabelSelector: ds.Spec.Selector,
}
printDescribeSource(ctx, client, workloadObj)
},
}

instrumented := printWorkloadManifestInfo(workloadObj, namespace)
printInstrumentationConfigInfo(instrumentationConfig, instrumented)
printInstrumentedApplicationInfo(instrumentedApplication, instrumented)
containerNameToExpectedDevices := printAppliedInstrumentationDeviceInfo(workloadObj, instrumentedApplication, podTemplate, instrumented)
printPodsInfo(pods, instrumentationInstances, containerNameToExpectedDevices)
var describeSourceStatefulSetCmd = &cobra.Command{
Use: "statefulset <name>",
Short: "Show details of a specific odigos source of type statefulset",
Long: `Print detailed description of a specific odigos source of type statefulset, which can be used to troubleshoot issues`,
Aliases: []string{"sts", "statefulsets", "sts.apps", "statefulset.apps", "statefulsets.apps"},
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
client, err := kube.CreateClient(cmd)
if err != nil {
kube.PrintClientErrorAndExit(err)
}

ctx := cmd.Context()
name := args[0]
ns := cmd.Flag("namespace").Value.String()
sts, err := client.AppsV1().StatefulSets(ns).Get(ctx, name, metav1.GetOptions{})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
workloadObj := &K8sSourceObject{
Kind: "statefulset",
ObjectMeta: sts.ObjectMeta,
PodTemplateSpec: &sts.Spec.Template,
LabelSelector: sts.Spec.Selector,
}
printDescribeSource(ctx, client, workloadObj)
},
}

func init() {

// describe
rootCmd.AddCommand(describeCmd)
describeCmd.Flags().StringVarP(&describeNamespaceFlag, "namespace", "n", "default", "namespace of the resource being described")

// source
describeCmd.AddCommand(describeSourceCmd)
describeSourceCmd.PersistentFlags().StringVarP(&describeNamespaceFlag, "namespace", "n", "default", "namespace of the source being described")

// source kinds
describeSourceCmd.AddCommand(describeSourceDeploymentCmd)
describeSourceCmd.AddCommand(describeSourceDaemonSetCmd)
describeSourceCmd.AddCommand(describeSourceStatefulSetCmd)
}
2 changes: 1 addition & 1 deletion cli/cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,5 @@ func init() {
rootCmd.AddCommand(versionCmd)

versionCmd.Flags().Bool(cliFlag, false, "prints only the CLI version")
versionCmd.Flags().Bool(clusterFlag, false, "prints only the Cluster version")
versionCmd.Flags().Bool(clusterFlag, false, "prints only the version of odigos deployed in the cluster")
}
8 changes: 4 additions & 4 deletions docs/architecture/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ It is recommended to use the `odigos describe` command to get detailed informati
The odigos describe command will show the status of the `odigos-instrumentation` label.

```bash
odigos describe deployment myservice -n default
odigos describe source deployment myservice -n default

Labels:
Instrumented: false
Expand Down Expand Up @@ -94,7 +94,7 @@ If the programming language is not detected:
The odigos describe command will show the instrumentation devices added to the pod spec for the workload.

```bash
odigos describe deployment myservice -n default
odigos describe source deployment myservice -n default

Instrumentation Device:
Status: Successfully applied instrumentation device to pod template
Expand Down Expand Up @@ -124,7 +124,7 @@ kubectl get deployment myservice -o jsonpath='{.spec.template.spec.containers[*]
The odigos describe command will show the pods for this workload and the instrumentation devices resource they have.

```bash
odigos describe deployment myservice -n default
odigos describe source deployment myservice -n default

Pods (Total 1, Running 1):

Expand All @@ -149,7 +149,7 @@ Instrumentation instances are currently available only for go, node.js, and pyth
They describe the status of the OpenTelemetry SDK agent running in a pod container.

```bash
odigos describe deployment myservice -n default
odigos describe source deployment myservice -n default

Pods (Total 1, Running 1):

Expand Down
1 change: 1 addition & 0 deletions docs/cli/odigos.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ odigos [flags]
## SEE ALSO

* [odigos cloud](/cli/odigos_cloud) - Manage the connection of the cluster to Odigos cloud
* [odigos describe](/cli/odigos_describe) - Describe a source in your kubernetes cluster
* [odigos install](/cli/odigos_install) - Install Odigos in your kubernetes cluster
* [odigos ui](/cli/odigos_ui) - Open the Odigos UI in your browser
* [odigos uninstall](/cli/odigos_uninstall) - Uninstall Odigos from your kubernetes cluster
Expand Down
Loading
Loading