From 06f421ad74ebb2c114a0ac279e8fdd6853b0998d Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Thu, 17 Oct 2024 17:07:16 +0300 Subject: [PATCH] feat: odigos describe command (#1595) Changes: - Add `odigos describe` command to describe the general status of odigos itself. Currently implemented Cluster Collector Information - Add conditions to collectors group status to record if the collectors deployment failed to succeed. For example - this can happen in the present of validation webhook that rejects the deployment if the image is not signed according to some requirements - move some code from `cli` to `k8sutils` to make it available for all modules in the code. - some cleanups on logging, so if the deployment fails to succeed, it is only printed once to autoscaler logs --- .../crd/bases/odigos.io_collectorsgroups.yaml | 62 ++++++ .../odigos/v1alpha1/collectorsgroupstatus.go | 19 +- api/odigos/v1alpha1/collectorsgroup_types.go | 8 + api/odigos/v1alpha1/zz_generated.deepcopy.go | 19 +- autoscaler/controllers/gateway/deployment.go | 55 ++--- autoscaler/controllers/gateway/root.go | 50 ++++- cli/cmd/describe.go | 60 ++++- cli/cmd/install.go | 3 +- cli/cmd/login.go | 3 +- cli/cmd/logout.go | 3 +- cli/cmd/profile.go | 5 +- cli/cmd/resources/autoscaler.go | 3 +- cli/cmd/resources/keyvalproxy.go | 3 +- cli/cmd/resources/odigosdeployment.go | 7 +- cli/cmd/resources/ui.go | 10 + cli/cmd/upgrade.go | 3 +- cli/cmd/version.go | 30 +-- frontend/endpoints/describe.go | 8 + frontend/main.go | 3 + helm/odigos/templates/ui/role.yaml | 7 + k8sutils/pkg/consts/consts.go | 4 + k8sutils/pkg/describe/common.go | 28 +++ k8sutils/pkg/describe/odigos.go | 210 ++++++++++++++++++ k8sutils/pkg/describe/source.go | 20 +- k8sutils/pkg/getters/odigosversion.go | 31 +++ 25 files changed, 548 insertions(+), 106 deletions(-) create mode 100644 k8sutils/pkg/describe/common.go create mode 100644 k8sutils/pkg/describe/odigos.go create mode 100644 k8sutils/pkg/getters/odigosversion.go diff --git a/api/config/crd/bases/odigos.io_collectorsgroups.yaml b/api/config/crd/bases/odigos.io_collectorsgroups.yaml index 099a49d81..922bdace2 100644 --- a/api/config/crd/bases/odigos.io_collectorsgroups.yaml +++ b/api/config/crd/bases/odigos.io_collectorsgroups.yaml @@ -55,6 +55,68 @@ spec: status: description: CollectorsGroupStatus defines the observed state of Collector properties: + conditions: + description: |- + Represents the observations of a collectorsroup's current state. + Known .status.conditions.type are: "Available", "Progressing" + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map ready: type: boolean receiverSignals: diff --git a/api/generated/odigos/applyconfiguration/odigos/v1alpha1/collectorsgroupstatus.go b/api/generated/odigos/applyconfiguration/odigos/v1alpha1/collectorsgroupstatus.go index 9c3909d16..23e105a1b 100644 --- a/api/generated/odigos/applyconfiguration/odigos/v1alpha1/collectorsgroupstatus.go +++ b/api/generated/odigos/applyconfiguration/odigos/v1alpha1/collectorsgroupstatus.go @@ -19,13 +19,15 @@ package v1alpha1 import ( common "github.com/odigos-io/odigos/common" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" ) // CollectorsGroupStatusApplyConfiguration represents a declarative configuration of the CollectorsGroupStatus type for use // with apply. type CollectorsGroupStatusApplyConfiguration struct { - Ready *bool `json:"ready,omitempty"` - ReceiverSignals []common.ObservabilitySignal `json:"receiverSignals,omitempty"` + Ready *bool `json:"ready,omitempty"` + ReceiverSignals []common.ObservabilitySignal `json:"receiverSignals,omitempty"` + Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"` } // CollectorsGroupStatusApplyConfiguration constructs a declarative configuration of the CollectorsGroupStatus type for use with @@ -51,3 +53,16 @@ func (b *CollectorsGroupStatusApplyConfiguration) WithReceiverSignals(values ... } return b } + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *CollectorsGroupStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *CollectorsGroupStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithConditions") + } + b.Conditions = append(b.Conditions, *values[i]) + } + return b +} diff --git a/api/odigos/v1alpha1/collectorsgroup_types.go b/api/odigos/v1alpha1/collectorsgroup_types.go index a5f37780e..15d673128 100644 --- a/api/odigos/v1alpha1/collectorsgroup_types.go +++ b/api/odigos/v1alpha1/collectorsgroup_types.go @@ -45,6 +45,14 @@ type CollectorsGroupStatus struct { // this is used to determine if a workload should export each signal or not. // this list is calculated based on the odigos destinations that were configured ReceiverSignals []common.ObservabilitySignal `json:"receiverSignals,omitempty"` + + // Represents the observations of a collectorsroup's current state. + // Known .status.conditions.type are: "Available", "Progressing" + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` } //+genclient diff --git a/api/odigos/v1alpha1/zz_generated.deepcopy.go b/api/odigos/v1alpha1/zz_generated.deepcopy.go index c6866d96a..d3faa7f41 100644 --- a/api/odigos/v1alpha1/zz_generated.deepcopy.go +++ b/api/odigos/v1alpha1/zz_generated.deepcopy.go @@ -24,8 +24,8 @@ import ( "github.com/odigos-io/odigos/api/odigos/v1alpha1/instrumentationrules" "github.com/odigos-io/odigos/common" "github.com/odigos-io/odigos/k8sutils/pkg/workload" - "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -176,6 +176,13 @@ func (in *CollectorsGroupStatus) DeepCopyInto(out *CollectorsGroupStatus) { *out = make([]common.ObservabilitySignal, len(*in)) copy(*out, *in) } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CollectorsGroupStatus. @@ -274,7 +281,7 @@ func (in *DestinationSpec) DeepCopyInto(out *DestinationSpec) { } if in.SecretRef != nil { in, out := &in.SecretRef, &out.SecretRef - *out = new(v1.LocalObjectReference) + *out = new(corev1.LocalObjectReference) **out = **in } if in.Signals != nil { @@ -299,7 +306,7 @@ func (in *DestinationStatus) DeepCopyInto(out *DestinationStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -824,7 +831,7 @@ func (in *InstrumentationRuleStatus) DeepCopyInto(out *InstrumentationRuleStatus *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -934,7 +941,7 @@ func (in *InstrumentedApplicationStatus) DeepCopyInto(out *InstrumentedApplicati *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } diff --git a/autoscaler/controllers/gateway/deployment.go b/autoscaler/controllers/gateway/deployment.go index aac22fc68..1a23eae84 100644 --- a/autoscaler/controllers/gateway/deployment.go +++ b/autoscaler/controllers/gateway/deployment.go @@ -6,6 +6,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "errors" + "github.com/odigos-io/odigos/autoscaler/utils" "github.com/odigos-io/odigos/k8sutils/pkg/consts" @@ -37,49 +39,37 @@ func syncDeployment(dests *odigosv1.DestinationList, gateway *odigosv1.Collector secretsVersionHash, err := destinationsSecretsVersionsHash(ctx, c, dests) if err != nil { - logger.Error(err, "Failed to get secrets hash") - return nil, err + return nil, errors.Join(err, errors.New("failed to get secrets hash")) } // Calculate the hash of the config data and the secrets version hash, this is used to make sure the gateway will restart when the config changes configDataHash := common.Sha256Hash(fmt.Sprintf("%s-%s", configData, secretsVersionHash)) desiredDeployment, err := getDesiredDeployment(dests, configDataHash, gateway, scheme, imagePullSecrets, odigosVersion, memConfig) if err != nil { - logger.Error(err, "Failed to get desired deployment") - return nil, err + return nil, errors.Join(err, errors.New("failed to get desired deployment")) } - existing := &appsv1.Deployment{} - if err := c.Get(ctx, client.ObjectKey{Name: gateway.Name, Namespace: gateway.Namespace}, existing); err != nil { - if apierrors.IsNotFound(err) { - logger.V(0).Info("Creating deployment") - newDeployment, err := createDeployment(desiredDeployment, ctx, c) - if err != nil { - logger.Error(err, "failed to create deployment") - return nil, err - } - return newDeployment, nil - } else { - logger.Error(err, "failed to get deployment") - return nil, err - } + existingDeployment := &appsv1.Deployment{} + getError := c.Get(ctx, client.ObjectKey{Name: gateway.Name, Namespace: gateway.Namespace}, existingDeployment) + if getError != nil && !apierrors.IsNotFound(getError) { + return nil, errors.Join(getError, errors.New("failed to get gateway deployment")) } - logger.V(0).Info("Patching deployment") - newDep, err := patchDeployment(existing, desiredDeployment, ctx, c) - if err != nil { - logger.Error(err, "failed to patch deployment") - return nil, err - } - - return newDep, nil -} - -func createDeployment(desired *appsv1.Deployment, ctx context.Context, c client.Client) (*appsv1.Deployment, error) { - if err := c.Create(ctx, desired); err != nil { - return nil, err + if apierrors.IsNotFound(getError) { + logger.V(0).Info("Creating new gateway deployment") + err := c.Create(ctx, desiredDeployment) + if err != nil { + return nil, errors.Join(err, errors.New("failed to create gateway deployment")) + } + return desiredDeployment, nil + } else { + logger.V(0).Info("Patching existing gateway deployment") + newDep, err := patchDeployment(existingDeployment, desiredDeployment, ctx, c) + if err != nil { + return nil, errors.Join(err, errors.New("failed to patch gateway deployment")) + } + return newDep, nil } - return desired, nil } func patchDeployment(existing *appsv1.Deployment, desired *appsv1.Deployment, ctx context.Context, c client.Client) (*appsv1.Deployment, error) { @@ -90,7 +80,6 @@ func patchDeployment(existing *appsv1.Deployment, desired *appsv1.Deployment, ct }) if err != nil { - logger.Error(err, "Failed to patch deployment") return nil, err } diff --git a/autoscaler/controllers/gateway/root.go b/autoscaler/controllers/gateway/root.go index 72f79ff6e..e7492cd03 100644 --- a/autoscaler/controllers/gateway/root.go +++ b/autoscaler/controllers/gateway/root.go @@ -2,6 +2,8 @@ package gateway import ( "context" + "encoding/json" + "time" appsv1 "k8s.io/api/apps/v1" @@ -11,6 +13,7 @@ import ( k8sconsts "github.com/odigos-io/odigos/k8sutils/pkg/consts" "github.com/odigos-io/odigos/k8sutils/pkg/env" "github.com/odigos-io/odigos/k8sutils/pkg/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -23,6 +26,42 @@ var ( } ) +func getCollectorsGroupDeployedConditionsPatch(err error) string { + + status := metav1.ConditionTrue + if err != nil { + status = metav1.ConditionFalse + } + + message := "Gateway collector is deployed in the cluster" + if err != nil { + message = err.Error() + } + + reason := "GatewayDeployedCreatedSuccessfully" + if err != nil { + // in the future, we can be more specific and break it down to + // more detailed reasons about what exactly failed + reason = "GatewayDeployedCreationFailed" + } + + patch := map[string]interface{}{ + "status": map[string]interface{}{ + "conditions": []metav1.Condition{{ + Type: "Deployed", + Status: status, + Reason: reason, + Message: message, + LastTransitionTime: metav1.NewTime(time.Now()), + }}, + }, + } + + patchData, _ := json.Marshal(patch) + // marshal error is ignored as it is not expected to happen + return string(patchData) +} + func Sync(ctx context.Context, k8sClient client.Client, scheme *runtime.Scheme, imagePullSecrets []string, odigosVersion string) error { logger := log.FromContext(ctx) @@ -30,6 +69,8 @@ func Sync(ctx context.Context, k8sClient client.Client, scheme *runtime.Scheme, var gatewayCollectorGroup odigosv1.CollectorsGroup err := k8sClient.Get(ctx, client.ObjectKey{Namespace: odigosNs, Name: k8sconsts.OdigosClusterCollectorConfigMapName}, &gatewayCollectorGroup) if err != nil { + // collectors group is created by the scheduler, after the first destination is added. + // it is however possible that some reconciler (like deployment) triggered and the collectors group will be created shortly. return client.IgnoreNotFound(err) } @@ -53,7 +94,14 @@ func Sync(ctx context.Context, k8sClient client.Client, scheme *runtime.Scheme, return err } - return syncGateway(&dests, &processors, &gatewayCollectorGroup, ctx, k8sClient, scheme, imagePullSecrets, odigosVersion, &odigosConfig) + err = syncGateway(&dests, &processors, &gatewayCollectorGroup, ctx, k8sClient, scheme, imagePullSecrets, odigosVersion, &odigosConfig) + statusPatchString := getCollectorsGroupDeployedConditionsPatch(err) + statusErr := k8sClient.Status().Patch(ctx, &gatewayCollectorGroup, client.RawPatch(types.MergePatchType, []byte(statusPatchString))) + if statusErr != nil { + logger.Error(statusErr, "Failed to patch collectors group status") + // just log the error, do not fail the reconciliation + } + return err } func syncGateway(dests *odigosv1.DestinationList, processors *odigosv1.ProcessorList, diff --git a/cli/cmd/describe.go b/cli/cmd/describe.go index de82c0b16..9426ea595 100644 --- a/cli/cmd/describe.go +++ b/cli/cmd/describe.go @@ -18,8 +18,34 @@ var ( 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`, + Short: "Show details on odigos deployment", + Long: `Print detailed description odigos deployment, which can be used to troubleshoot issues`, + Run: func(cmd *cobra.Command, args []string) { + + client, err := kube.CreateClient(cmd) + if err != nil { + kube.PrintClientErrorAndExit(err) + } + ctx := cmd.Context() + + odigosNs, err := resources.GetOdigosNamespace(client, ctx) + if err != nil { + if resources.IsErrNoOdigosNamespaceFound(err) { + fmt.Println("\033[31mERROR\033[0m Odigos is NOT yet installed in the current cluster") + } else { + fmt.Println("\033[31mERROR\033[0m Error detecting Odigos namespace in the current cluster") + } + return + } + + var describeText string + if describeRemoteFlag { + describeText = executeRemoteOdigosDescribe(ctx, client, odigosNs) + } else { + describeText = describe.DescribeOdigos(ctx, client, client.OdigosClient, odigosNs) + } + fmt.Println(describeText) + }, } var describeSourceCmd = &cobra.Command{ @@ -45,7 +71,7 @@ var describeSourceDeploymentCmd = &cobra.Command{ var describeText string if describeRemoteFlag { - describeText = executeRemoteDescribe(ctx, client, "deployment", ns, name) + describeText = executeRemoteSourceDescribe(ctx, client, "deployment", ns, name) } else { describeText = describe.DescribeDeployment(ctx, client.Interface, client.OdigosClient, ns, name) } @@ -71,7 +97,7 @@ var describeSourceDaemonSetCmd = &cobra.Command{ var describeText string if describeRemoteFlag { - describeText = executeRemoteDescribe(ctx, client, "daemonset", ns, name) + describeText = executeRemoteSourceDescribe(ctx, client, "daemonset", ns, name) } else { describeText = describe.DescribeDaemonSet(ctx, client.Interface, client.OdigosClient, ns, name) } @@ -97,7 +123,7 @@ var describeSourceStatefulSetCmd = &cobra.Command{ var describeText string if describeRemoteFlag { - describeText = executeRemoteDescribe(ctx, client, "statefulset", ns, name) + describeText = executeRemoteSourceDescribe(ctx, client, "statefulset", ns, name) } else { describeText = describe.DescribeStatefulSet(ctx, client.Interface, client.OdigosClient, ns, name) } @@ -105,8 +131,8 @@ var describeSourceStatefulSetCmd = &cobra.Command{ }, } -func executeRemoteDescribe(ctx context.Context, client *kube.Client, workloadKind string, workloadNs string, workloadName string) string { - uiSvcProxyEndpoint := getUiServiceEndpoint(ctx, client, workloadKind, workloadNs, workloadName) +func executeRemoteOdigosDescribe(ctx context.Context, client *kube.Client, odigosNs string) string { + uiSvcProxyEndpoint := getUiServiceOdigosEndpoint(ctx, client, odigosNs) request := client.Clientset.RESTClient().Get().AbsPath(uiSvcProxyEndpoint).Do(ctx) response, err := request.Raw() if err != nil { @@ -116,7 +142,25 @@ func executeRemoteDescribe(ctx context.Context, client *kube.Client, workloadKin } } -func getUiServiceEndpoint(ctx context.Context, client *kube.Client, workloadKind string, workloadNs string, workloadName string) string { +func executeRemoteSourceDescribe(ctx context.Context, client *kube.Client, workloadKind string, workloadNs string, workloadName string) string { + uiSvcProxyEndpoint := getUiServiceSourceEndpoint(ctx, client, workloadKind, workloadNs, workloadName) + request := client.Clientset.RESTClient().Get().AbsPath(uiSvcProxyEndpoint).Do(ctx) + response, err := request.Raw() + if err != nil { + return "Remote describe failed: " + err.Error() + } else { + return string(response) + } +} + +func getUiServiceOdigosEndpoint(ctx context.Context, client *kube.Client, odigosNs string) string { + uiServiceName := "ui" + uiServicePort := 3000 + + return fmt.Sprintf("/api/v1/namespaces/%s/services/%s:%d/proxy/api/describe/odigos", odigosNs, uiServiceName, uiServicePort) +} + +func getUiServiceSourceEndpoint(ctx context.Context, client *kube.Client, workloadKind string, workloadNs string, workloadName string) string { ns, err := resources.GetOdigosNamespace(client, ctx) if resources.IsErrNoOdigosNamespaceFound(err) { fmt.Println("\033[31mERROR\033[0m no odigos installation found in the current cluster. use \"odigos install\" to install odigos in the cluster or check that kubeconfig is pointing to the correct cluster.") diff --git a/cli/cmd/install.go b/cli/cmd/install.go index d898295f2..9d318fb75 100644 --- a/cli/cmd/install.go +++ b/cli/cmd/install.go @@ -15,6 +15,7 @@ import ( "github.com/odigos-io/odigos/common" "github.com/odigos-io/odigos/common/consts" "github.com/odigos-io/odigos/common/utils" + k8sconsts "github.com/odigos-io/odigos/k8sutils/pkg/consts" "github.com/odigos-io/odigos/cli/cmd/resources" "github.com/odigos-io/odigos/cli/pkg/kube" @@ -62,7 +63,7 @@ This command will install k8s components that will auto-instrument your applicat ns := cmd.Flag("namespace").Value.String() // Check if Odigos already installed - cm, err := client.CoreV1().ConfigMaps(ns).Get(ctx, resources.OdigosDeploymentConfigMapName, metav1.GetOptions{}) + cm, err := client.CoreV1().ConfigMaps(ns).Get(ctx, k8sconsts.OdigosDeploymentConfigMapName, metav1.GetOptions{}) if err == nil && cm != nil { fmt.Printf("\033[31mERROR\033[0m Odigos is already installed in namespace\n") os.Exit(1) diff --git a/cli/cmd/login.go b/cli/cmd/login.go index 9291156ec..fa3f008b8 100644 --- a/cli/cmd/login.go +++ b/cli/cmd/login.go @@ -13,6 +13,7 @@ import ( "github.com/odigos-io/odigos/cli/pkg/labels" "github.com/odigos-io/odigos/cli/pkg/log" "github.com/odigos-io/odigos/common" + "github.com/odigos-io/odigos/k8sutils/pkg/getters" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -60,7 +61,7 @@ func updateApiKey(cmd *cobra.Command, args []string) { os.Exit(1) } - currentOdigosVersion, err := getOdigosVersionInClusterFromConfigMap(ctx, client, ns) + currentOdigosVersion, err := getters.GetOdigosVersionInClusterFromConfigMap(ctx, client.Clientset, ns) if err != nil { fmt.Println("Odigos cloud login failed - unable to read the current Odigos version.") os.Exit(1) diff --git a/cli/cmd/logout.go b/cli/cmd/logout.go index 7fc041c0c..ef51f9f30 100644 --- a/cli/cmd/logout.go +++ b/cli/cmd/logout.go @@ -9,6 +9,7 @@ import ( "github.com/odigos-io/odigos/cli/pkg/confirm" "github.com/odigos-io/odigos/cli/pkg/kube" "github.com/odigos-io/odigos/common" + "github.com/odigos-io/odigos/k8sutils/pkg/getters" "github.com/spf13/cobra" ) @@ -37,7 +38,7 @@ var logoutCmd = &cobra.Command{ os.Exit(1) } - currentOdigosVersion, err := getOdigosVersionInClusterFromConfigMap(ctx, client, ns) + currentOdigosVersion, err := getters.GetOdigosVersionInClusterFromConfigMap(ctx, client.Clientset, ns) if err != nil { fmt.Println("Odigos cloud login failed - unable to read the current Odigos version.") os.Exit(1) diff --git a/cli/cmd/profile.go b/cli/cmd/profile.go index ce1ce7366..3293ed4bb 100644 --- a/cli/cmd/profile.go +++ b/cli/cmd/profile.go @@ -8,6 +8,7 @@ import ( "github.com/odigos-io/odigos/cli/cmd/resources/odigospro" "github.com/odigos-io/odigos/cli/pkg/kube" "github.com/odigos-io/odigos/common" + "github.com/odigos-io/odigos/k8sutils/pkg/getters" "github.com/spf13/cobra" ) @@ -92,7 +93,7 @@ var addProfileCmd = &cobra.Command{ os.Exit(1) } - currentOdigosVersion, err := getOdigosVersionInClusterFromConfigMap(ctx, client, ns) + currentOdigosVersion, err := getters.GetOdigosVersionInClusterFromConfigMap(ctx, client.Clientset, ns) if err != nil { fmt.Println("Odigos cloud login failed - unable to read the current Odigos version.") os.Exit(1) @@ -178,7 +179,7 @@ var removeProfileCmd = &cobra.Command{ os.Exit(1) } - currentOdigosVersion, err := getOdigosVersionInClusterFromConfigMap(ctx, client, ns) + currentOdigosVersion, err := getters.GetOdigosVersionInClusterFromConfigMap(ctx, client.Clientset, ns) if err != nil { fmt.Println("Odigos cloud login failed - unable to read the current Odigos version.") os.Exit(1) diff --git a/cli/cmd/resources/autoscaler.go b/cli/cmd/resources/autoscaler.go index 69c1aa99c..c6e99ba96 100644 --- a/cli/cmd/resources/autoscaler.go +++ b/cli/cmd/resources/autoscaler.go @@ -7,6 +7,7 @@ import ( "github.com/odigos-io/odigos/cli/pkg/containers" "github.com/odigos-io/odigos/cli/pkg/kube" "github.com/odigos-io/odigos/common" + "github.com/odigos-io/odigos/k8sutils/pkg/consts" "sigs.k8s.io/controller-runtime/pkg/client" appsv1 "k8s.io/api/apps/v1" @@ -459,7 +460,7 @@ func NewAutoscalerDeployment(ns string, version string, imagePrefix string, imag { ConfigMapRef: &corev1.ConfigMapEnvSource{ LocalObjectReference: corev1.LocalObjectReference{ - Name: OdigosDeploymentConfigMapName, + Name: consts.OdigosDeploymentConfigMapName, }, }, }, diff --git a/cli/cmd/resources/keyvalproxy.go b/cli/cmd/resources/keyvalproxy.go index 57904abee..77bbdb765 100644 --- a/cli/cmd/resources/keyvalproxy.go +++ b/cli/cmd/resources/keyvalproxy.go @@ -8,6 +8,7 @@ import ( "github.com/odigos-io/odigos/cli/pkg/containers" "github.com/odigos-io/odigos/cli/pkg/kube" "github.com/odigos-io/odigos/common" + "github.com/odigos-io/odigos/k8sutils/pkg/consts" "sigs.k8s.io/controller-runtime/pkg/client" appsv1 "k8s.io/api/apps/v1" @@ -324,7 +325,7 @@ func NewKeyvalProxyDeployment(version string, ns string, imagePrefix string) *ap { ConfigMapRef: &corev1.ConfigMapEnvSource{ LocalObjectReference: corev1.LocalObjectReference{ - Name: OdigosDeploymentConfigMapName, + Name: consts.OdigosDeploymentConfigMapName, }, }, }, diff --git a/cli/cmd/resources/odigosdeployment.go b/cli/cmd/resources/odigosdeployment.go index 36733c8df..9f2ae9b4b 100644 --- a/cli/cmd/resources/odigosdeployment.go +++ b/cli/cmd/resources/odigosdeployment.go @@ -7,6 +7,7 @@ import ( "github.com/odigos-io/odigos/cli/cmd/resources/resourcemanager" "github.com/odigos-io/odigos/cli/pkg/kube" "github.com/odigos-io/odigos/common" + "github.com/odigos-io/odigos/k8sutils/pkg/consts" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -14,10 +15,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -const ( - OdigosDeploymentConfigMapName = "odigos-deployment" -) - func NewOdigosDeploymentConfigMap(ns string, odigosVersion string) *corev1.ConfigMap { return &corev1.ConfigMap{ TypeMeta: v1.TypeMeta{ @@ -25,7 +22,7 @@ func NewOdigosDeploymentConfigMap(ns string, odigosVersion string) *corev1.Confi APIVersion: "v1", }, ObjectMeta: v1.ObjectMeta{ - Name: OdigosDeploymentConfigMapName, + Name: consts.OdigosDeploymentConfigMapName, Namespace: ns, }, Data: map[string]string{ diff --git a/cli/cmd/resources/ui.go b/cli/cmd/resources/ui.go index 6cd464c23..f0bcfc750 100644 --- a/cli/cmd/resources/ui.go +++ b/cli/cmd/resources/ui.go @@ -165,6 +165,16 @@ func NewUIRole(ns string) *rbacv1.Role { "pods", }, }, + { + Verbs: []string{ + "get", + "list", + }, + APIGroups: []string{"apps"}, + Resources: []string{ + "replicasets", + }, + }, { Verbs: []string{ "get", diff --git a/cli/cmd/upgrade.go b/cli/cmd/upgrade.go index 6b30f9242..6b00a2bb1 100644 --- a/cli/cmd/upgrade.go +++ b/cli/cmd/upgrade.go @@ -14,6 +14,7 @@ import ( "github.com/odigos-io/odigos/cli/pkg/kube" "github.com/odigos-io/odigos/common/consts" "github.com/odigos-io/odigos/common/utils" + k8sconsts "github.com/odigos-io/odigos/k8sutils/pkg/consts" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -47,7 +48,7 @@ and apply any required migrations and adaptations.`, skipVersionCheckFlag := cmd.Flag("skip-version-check") if skipVersionCheckFlag == nil || !cmd.Flag("skip-version-check").Changed { - cm, err := client.CoreV1().ConfigMaps(ns).Get(ctx, resources.OdigosDeploymentConfigMapName, metav1.GetOptions{}) + cm, err := client.CoreV1().ConfigMaps(ns).Get(ctx, k8sconsts.OdigosDeploymentConfigMapName, metav1.GetOptions{}) if err != nil { fmt.Println("Odigos upgrade failed - unable to read the current Odigos version for migration") os.Exit(1) diff --git a/cli/cmd/version.go b/cli/cmd/version.go index ad06ce733..348b50cc0 100644 --- a/cli/cmd/version.go +++ b/cli/cmd/version.go @@ -4,13 +4,12 @@ Copyright © 2022 NAME HERE package cmd import ( - "context" "fmt" "github.com/odigos-io/odigos/cli/cmd/resources" "github.com/odigos-io/odigos/cli/pkg/kube" + "github.com/odigos-io/odigos/k8sutils/pkg/getters" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( @@ -20,7 +19,7 @@ const ( var ( OdigosVersion string - OdigosClusterVersion string + odigosClusterVersion string OdigosCommit string OdigosDate string ) @@ -37,7 +36,7 @@ var versionCmd = &cobra.Command{ fmt.Printf("%s\n", OdigosVersion) } - OdigosClusterVersion, err := getOdigosVersionInCluster(cmd, clusterFlag) + OdigosClusterVersion, err := getOdigosVersionInCluster(cmd) if clusterFlag && err == nil { fmt.Printf("%s\n", OdigosClusterVersion) @@ -57,32 +56,13 @@ var versionCmd = &cobra.Command{ }, } -func getOdigosVersionInCluster(cmd *cobra.Command, flag bool) (string, error) { +func getOdigosVersionInCluster(cmd *cobra.Command) (string, error) { client, ns, err := getOdigosKubeClientAndNamespace(cmd) if err != nil { return "", err } - OdigosClusterVersion, err = getOdigosVersionInClusterFromConfigMap(cmd.Context(), client, ns) - if err != nil { - return "", err - } - - return OdigosClusterVersion, nil -} - -func getOdigosVersionInClusterFromConfigMap(ctx context.Context, client *kube.Client, ns string) (string, error) { - cm, err := client.CoreV1().ConfigMaps(ns).Get(ctx, resources.OdigosDeploymentConfigMapName, metav1.GetOptions{}) - if err != nil { - return "", fmt.Errorf("error detecting Odigos version in the current cluster") - } - - odigosVersion, ok := cm.Data["ODIGOS_VERSION"] - if !ok || odigosVersion == "" { - return "", fmt.Errorf("error detecting Odigos version in the current cluster") - } - - return odigosVersion, nil + return getters.GetOdigosVersionInClusterFromConfigMap(cmd.Context(), client.Clientset, ns) } func getOdigosKubeClientAndNamespace(cmd *cobra.Command) (*kube.Client, string, error) { diff --git a/frontend/endpoints/describe.go b/frontend/endpoints/describe.go index dc637d73e..c67e4ef4a 100644 --- a/frontend/endpoints/describe.go +++ b/frontend/endpoints/describe.go @@ -4,8 +4,16 @@ import ( "github.com/gin-gonic/gin" "github.com/odigos-io/odigos/frontend/kube" "github.com/odigos-io/odigos/k8sutils/pkg/describe" + "github.com/odigos-io/odigos/k8sutils/pkg/env" ) +func DescribeOdigos(c *gin.Context) { + ctx := c.Request.Context() + odiogosNs := env.GetCurrentNamespace() + describeText := describe.DescribeOdigos(ctx, kube.DefaultClient, kube.DefaultClient.OdigosClient, odiogosNs) + c.Writer.WriteString(describeText) +} + func DescribeSource(c *gin.Context, ns string, kind string, name string) { ctx := c.Request.Context() switch kind { diff --git a/frontend/main.go b/frontend/main.go index ea86f1193..64777fa68 100644 --- a/frontend/main.go +++ b/frontend/main.go @@ -128,6 +128,9 @@ func startHTTPServer(flags *Flags, odigosMetrics *collectormetrics.OdigosMetrics apis.PUT("/instrumentation-rules/:id", func(c *gin.Context) { endpoints.UpdateInstrumentationRule(c, flags.Namespace, c.Param("id")) }) // Describe + apis.GET("/describe/odigos", func(c *gin.Context) { + endpoints.DescribeOdigos(c) + }) apis.GET("/describe/source/namespace/:namespace/kind/:kind/name/:name", func(c *gin.Context) { endpoints.DescribeSource(c, c.Param("namespace"), c.Param("kind"), c.Param("name")) }) diff --git a/helm/odigos/templates/ui/role.yaml b/helm/odigos/templates/ui/role.yaml index aff6e7071..9da7f8e41 100644 --- a/helm/odigos/templates/ui/role.yaml +++ b/helm/odigos/templates/ui/role.yaml @@ -23,6 +23,13 @@ rules: - get - list - watch + - apiGroups: + - "apps" + resources: + - replicasets + verbs: + - get + - list - apiGroups: - "odigos.io" resources: diff --git a/k8sutils/pkg/consts/consts.go b/k8sutils/pkg/consts/consts.go index bb9c11d11..f27b4c7c5 100644 --- a/k8sutils/pkg/consts/consts.go +++ b/k8sutils/pkg/consts/consts.go @@ -10,6 +10,10 @@ const ( // OdigosCollectorRoleLabel is the label used to identify the role of the Odigos collector. const OdigosCollectorRoleLabel = "odigos.io/collector-role" +const ( + OdigosDeploymentConfigMapName = "odigos-deployment" +) + const ( OdigosClusterCollectorDeploymentName = "odigos-gateway" OdigosClusterCollectorConfigMapName = OdigosClusterCollectorDeploymentName diff --git a/k8sutils/pkg/describe/common.go b/k8sutils/pkg/describe/common.go new file mode 100644 index 000000000..3fe026154 --- /dev/null +++ b/k8sutils/pkg/describe/common.go @@ -0,0 +1,28 @@ +package describe + +import ( + "fmt" + "strings" +) + +func wrapTextInRed(text string) string { + return "\033[31m" + text + "\033[0m" +} + +func wrapTextInGreen(text string) string { + return "\033[32m" + text + "\033[0m" +} + +func wrapTextSuccessOfFailure(text string, success bool) string { + if success { + return wrapTextInGreen(text) + } else { + return wrapTextInRed(text) + } +} + +func describeText(sb *strings.Builder, indent int, printftext string, args ...interface{}) { + indentText := strings.Repeat(" ", indent) + lineText := fmt.Sprintf(printftext, args...) + sb.WriteString(fmt.Sprintf("%s%s\n", indentText, lineText)) +} diff --git a/k8sutils/pkg/describe/odigos.go b/k8sutils/pkg/describe/odigos.go new file mode 100644 index 000000000..e80ab76ea --- /dev/null +++ b/k8sutils/pkg/describe/odigos.go @@ -0,0 +1,210 @@ +package describe + +import ( + "context" + "fmt" + "strings" + + odigosclientset "github.com/odigos-io/odigos/api/generated/odigos/clientset/versioned/typed/odigos/v1alpha1" + odigosv1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" + "github.com/odigos-io/odigos/k8sutils/pkg/consts" + "github.com/odigos-io/odigos/k8sutils/pkg/getters" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +type clusterCollectorResources struct { + CollectorsGroup *odigosv1.CollectorsGroup + Deployment *appsv1.Deployment + LatestRevisionPods *corev1.PodList +} + +func getClusterCollectorResources(ctx context.Context, kubeClient kubernetes.Interface, odigosClient odigosclientset.OdigosV1alpha1Interface, odigosNs string) (clusterCollector clusterCollectorResources, err error) { + + clusterCollector = clusterCollectorResources{} + + clusterCollector.CollectorsGroup, err = odigosClient.CollectorsGroups(odigosNs).Get(ctx, consts.OdigosClusterCollectorCollectorGroupName, metav1.GetOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return + } + + clusterCollector.Deployment, err = kubeClient.AppsV1().Deployments(odigosNs).Get(ctx, consts.OdigosClusterCollectorDeploymentName, metav1.GetOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return + } + + var clusterRoleRevision string + if clusterCollector.Deployment != nil { + revisionAnnotation, found := clusterCollector.Deployment.Annotations["deployment.kubernetes.io/revision"] + if found { + clusterRoleRevision = revisionAnnotation + } + } + + // get only cluster role replicasets + if clusterRoleRevision != "" { + deploymentRs, errRs := kubeClient.AppsV1().ReplicaSets(odigosNs).List(ctx, metav1.ListOptions{ + LabelSelector: metav1.FormatLabelSelector(clusterCollector.Deployment.Spec.Selector), + }) + if errRs != nil { + err = errRs + return + } + + var latestRevisionReplicaSet *appsv1.ReplicaSet + for i := range deploymentRs.Items { + rs := &deploymentRs.Items[i] + if rs.Annotations["deployment.kubernetes.io/revision"] == clusterRoleRevision { + latestRevisionReplicaSet = rs + break + } + } + + if latestRevisionReplicaSet != nil { + podTemplateHash := latestRevisionReplicaSet.Labels["pod-template-hash"] + clusterCollector.LatestRevisionPods, err = kubeClient.CoreV1().Pods(odigosNs).List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("pod-template-hash=%s", podTemplateHash), + }) + if err != nil { + return + } + } + } + + return +} + +func getRelevantOdigosResources(ctx context.Context, kubeClient kubernetes.Interface, odigosClient odigosclientset.OdigosV1alpha1Interface, odigosNs string) (clusterCollector clusterCollectorResources, destinations *odigosv1.DestinationList, instrumentationConfigs *odigosv1.InstrumentationConfigList, err error) { + + clusterCollector, err = getClusterCollectorResources(ctx, kubeClient, odigosClient, odigosNs) + if err != nil { + return + } + + destinations, err = odigosClient.Destinations(odigosNs).List(ctx, metav1.ListOptions{}) + if err != nil { + return + } + + instrumentationConfigs, err = odigosClient.InstrumentationConfigs("").List(ctx, metav1.ListOptions{}) + if err != nil { + return + } + + return +} + +func printOdigosVersion(odigosVersion string, sb *strings.Builder) { + describeText(sb, 0, "Odigos Version: %s", odigosVersion) +} + +func printOdigosPipelineStatus(numInstrumentationConfigs, numDestinations int, expectingPipeline bool, sb *strings.Builder) { + if expectingPipeline { + describeText(sb, 1, "Status: there are %d sources and %d destinations so pipeline will be deployed\n", numInstrumentationConfigs, numDestinations) + } else { + describeText(sb, 1, "Status: no sources or destinations found so pipeline will not be deployed") + } +} + +func printClusterGatewayStatus(clusterCollector clusterCollectorResources, expectingPipeline bool, sb *strings.Builder) { + describeText(sb, 1, "Cluster Gateway:") + if clusterCollector.CollectorsGroup == nil { + describeText(sb, 2, wrapTextSuccessOfFailure("Collectors Group Not Created", !expectingPipeline)) + return + } + + describeText(sb, 2, wrapTextSuccessOfFailure("Collectors Group Created", expectingPipeline)) + + var deployedCondition *metav1.Condition + for _, condition := range clusterCollector.CollectorsGroup.Status.Conditions { + if condition.Type == "Deployed" { + deployedCondition = &condition + break + } + } + if deployedCondition == nil { + describeText(sb, 2, wrapTextInRed("Deployed: Status Unavailable")) + } else { + if deployedCondition.Status == metav1.ConditionTrue { + describeText(sb, 2, wrapTextInGreen("Deployed: True")) + } else { + describeText(sb, 2, wrapTextInRed("Deployed: False")) + describeText(sb, 2, wrapTextInRed(fmt.Sprintf("Reason: %s", deployedCondition.Message))) + } + } + + if clusterCollector.LatestRevisionPods == nil || clusterCollector.Deployment == nil { + describeText(sb, 2, wrapTextInRed("Number of Replicas: Status Unavailable")) + } else { + runningReplicas := 0 + failureReplicas := 0 + var failureText string + for _, pod := range clusterCollector.LatestRevisionPods.Items { + var condition *corev1.PodCondition + for i := range pod.Status.Conditions { + c := pod.Status.Conditions[i] + if c.Type == corev1.PodReady { + condition = &c + break + } + } + if condition == nil { + failureReplicas++ + } else { + if condition.Status == corev1.ConditionTrue { + runningReplicas++ + } else { + failureReplicas++ + failureText = condition.Message + } + } + } + desiredReplicas := *clusterCollector.Deployment.Spec.Replicas + describeText(sb, 2, fmt.Sprintf("Desired Replicas: %d", desiredReplicas)) + podReplicasText := fmt.Sprintf("Actual Replicas: %d running, %d failed", runningReplicas, failureReplicas) + deploymentSuccessful := runningReplicas == int(desiredReplicas) + describeText(sb, 2, wrapTextSuccessOfFailure(podReplicasText, deploymentSuccessful)) + if !deploymentSuccessful { + describeText(sb, 2, wrapTextInRed(fmt.Sprintf("Replicas Not Ready Reason: %s", failureText))) + } + } +} + +func printOdigosPipeline(clusterCollector clusterCollectorResources, destinations *odigosv1.DestinationList, instrumentationConfigs *odigosv1.InstrumentationConfigList, sb *strings.Builder) { + describeText(sb, 0, "Odigos Pipeline:") + numDestinations := len(destinations.Items) + numInstrumentationConfigs := len(instrumentationConfigs.Items) + // odigos will only initiate pipeline if there are any sources or destinations + expectingPipeline := numDestinations > 0 || numInstrumentationConfigs > 0 + + printOdigosPipelineStatus(numInstrumentationConfigs, numDestinations, expectingPipeline, sb) + printClusterGatewayStatus(clusterCollector, expectingPipeline, sb) +} + +func printDescribeOdigos(odigosVersion string, clusterCollector clusterCollectorResources, destinations *odigosv1.DestinationList, instrumentationConfigs *odigosv1.InstrumentationConfigList) string { + var sb strings.Builder + + printOdigosVersion(odigosVersion, &sb) + sb.WriteString("\n") + printOdigosPipeline(clusterCollector, destinations, instrumentationConfigs, &sb) + + return sb.String() +} + +func DescribeOdigos(ctx context.Context, kubeClient kubernetes.Interface, odigosClient odigosclientset.OdigosV1alpha1Interface, odigosNs string) string { + + odigosVersion, err := getters.GetOdigosVersionInClusterFromConfigMap(ctx, kubeClient, odigosNs) + if err != nil { + return fmt.Sprintf("Error: %v\n", err) + } + + clusterCollector, destinations, instrumentationConfigs, err := getRelevantOdigosResources(ctx, kubeClient, odigosClient, odigosNs) + if err != nil { + return fmt.Sprintf("Error: %v\n", err) + } + + return printDescribeOdigos(odigosVersion, clusterCollector, destinations, instrumentationConfigs) +} diff --git a/k8sutils/pkg/describe/source.go b/k8sutils/pkg/describe/source.go index aadb0f9bc..b615c17d3 100644 --- a/k8sutils/pkg/describe/source.go +++ b/k8sutils/pkg/describe/source.go @@ -25,22 +25,6 @@ type K8sSourceObject struct { LabelSelector *metav1.LabelSelector } -func wrapTextInRed(text string) string { - return "\033[31m" + text + "\033[0m" -} - -func wrapTextInGreen(text string) string { - return "\033[32m" + text + "\033[0m" -} - -func wrapTextSuccessOfFailure(text string, success bool) string { - if success { - return wrapTextInGreen(text) - } else { - return wrapTextInRed(text) - } -} - func getInstrumentationLabelTexts(workload *K8sSourceObject, ns *corev1.Namespace) (workloadText, nsText, decisionText string, instrumented bool) { workloadLabel, workloadFound := workload.GetLabels()[consts.OdigosInstrumentationLabel] nsLabel, nsFound := ns.GetLabels()[consts.OdigosInstrumentationLabel] @@ -80,7 +64,7 @@ func getInstrumentationLabelTexts(workload *K8sSourceObject, ns *corev1.Namespac return } -func getRelevantResources(ctx context.Context, kubeClient kubernetes.Interface, odigosClient odigosclientset.OdigosV1alpha1Interface, workloadObj *K8sSourceObject) (namespace *corev1.Namespace, instrumentationConfig *odigosv1.InstrumentationConfig, instrumentedApplication *odigosv1.InstrumentedApplication, instrumentationInstances *odigosv1.InstrumentationInstanceList, pods *corev1.PodList, err error) { +func getRelevantSourceResources(ctx context.Context, kubeClient kubernetes.Interface, odigosClient odigosclientset.OdigosV1alpha1Interface, workloadObj *K8sSourceObject) (namespace *corev1.Namespace, instrumentationConfig *odigosv1.InstrumentationConfig, instrumentedApplication *odigosv1.InstrumentedApplication, instrumentationInstances *odigosv1.InstrumentationInstanceList, pods *corev1.PodList, err error) { ns := workloadObj.GetNamespace() namespace, err = kubeClient.CoreV1().Namespaces().Get(ctx, ns, metav1.GetOptions{}) @@ -427,7 +411,7 @@ func printPodsInfo(pods *corev1.PodList, instrumentationInstances *odigosv1.Inst func PrintDescribeSource(ctx context.Context, kubeClient kubernetes.Interface, odigosClient odigosclientset.OdigosV1alpha1Interface, workloadObj *K8sSourceObject) string { var sb strings.Builder - namespace, instrumentationConfig, instrumentedApplication, instrumentationInstances, pods, err := getRelevantResources(ctx, kubeClient, odigosClient, workloadObj) + namespace, instrumentationConfig, instrumentedApplication, instrumentationInstances, pods, err := getRelevantSourceResources(ctx, kubeClient, odigosClient, workloadObj) if err != nil { sb.WriteString(fmt.Sprintf("Error: %v\n", err)) return sb.String() diff --git a/k8sutils/pkg/getters/odigosversion.go b/k8sutils/pkg/getters/odigosversion.go new file mode 100644 index 000000000..6ecbdd4b3 --- /dev/null +++ b/k8sutils/pkg/getters/odigosversion.go @@ -0,0 +1,31 @@ +package getters + +import ( + "context" + "errors" + + "github.com/odigos-io/odigos/k8sutils/pkg/consts" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +var ( + ErrNoOdigosDeploymentConfigMap = errors.New("odigos deployment config map not found in cluster") + ErrMissingVersionInConfigMap = errors.New("odigos version not found in deployment config map") +) + +// Return the Odigos version installed in the cluster. +// The function assumes odigos is installed, and will return an error if it is not or if the version cannot be detected (not expected in normal operation). +func GetOdigosVersionInClusterFromConfigMap(ctx context.Context, client kubernetes.Interface, ns string) (string, error) { + cm, err := client.CoreV1().ConfigMaps(ns).Get(ctx, consts.OdigosDeploymentConfigMapName, metav1.GetOptions{}) + if err != nil { + return "", ErrNoOdigosDeploymentConfigMap + } + + odigosVersion, ok := cm.Data["ODIGOS_VERSION"] + if !ok || odigosVersion == "" { + return "", ErrMissingVersionInConfigMap + } + + return odigosVersion, nil +}