diff --git a/api/k8sconsts/odigosdeployment.go b/api/k8sconsts/odigosdeployment.go index d30751b801..51da045acf 100644 --- a/api/k8sconsts/odigosdeployment.go +++ b/api/k8sconsts/odigosdeployment.go @@ -5,9 +5,12 @@ import ( ) const ( - OdigosDeploymentConfigMapName = "odigos-deployment" - OdigosDeploymentConfigMapVersionKey = consts.OdigosVersionEnvVarName - OdigosDeploymentConfigMapTierKey = consts.OdigosTierEnvVarName - OdigosDeploymentConfigMapInstallationMethodKey = "installation-method" - OdigosDeploymentConfigMapKubernetesVersionKey = "kubernetes-version" + OdigosDeploymentConfigMapName = "odigos-deployment" + OdigosDeploymentConfigMapVersionKey = consts.OdigosVersionEnvVarName + OdigosDeploymentConfigMapTierKey = consts.OdigosTierEnvVarName + OdigosDeploymentConfigMapInstallationMethodKey = "installation-method" + OdigosDeploymentConfigMapKubernetesVersionKey = "kubernetes-version" + OdigosDeploymentConfigMapOnPremTokenAudKey = "onprem-token-audience" + OdigosDeploymentConfigMapOnPremTokenExpKey = "onprem-token-expiration" + OdigosDeploymentConfigMapOnPremClientProfilesKey = "onprem-profiles" ) diff --git a/k8sutils/pkg/consts/consts.go b/k8sutils/pkg/consts/consts.go new file mode 100644 index 0000000000..2e3021f0af --- /dev/null +++ b/k8sutils/pkg/consts/consts.go @@ -0,0 +1,114 @@ +package consts + +import ( + "k8s.io/apimachinery/pkg/util/version" + + commonconsts "github.com/odigos-io/odigos/common/consts" +) + +var ( + DefaultIgnoredNamespaces = []string{"kube-system", "local-path-storage", "istio-system", "linkerd", "kube-node-lease"} + DefaultIgnoredContainers = []string{"istio-proxy", "vault-agent", "filebeat", "linkerd-proxy", "fluentd", "akeyless-init"} +) + +type CollectorRole string + +const ( + CollectorsRoleClusterGateway CollectorRole = "CLUSTER_GATEWAY" + CollectorsRoleNodeCollector CollectorRole = "NODE_COLLECTOR" +) + +const ( + // OdigosInjectInstrumentationLabel is the label used to enable the mutating webhook. + OdigosInjectInstrumentationLabel = "odigos.io/inject-instrumentation" + // OdigosCollectorRoleLabel is the label used to identify the role of the Odigos collector. + OdigosCollectorRoleLabel = "odigos.io/collector-role" + + // used to label resources created by profiles with the hash that created them. + // when a new profiles is reconciled, we will apply them with a new hash + // and use the label to identify the resources that needs to be deleted. + OdigosProfilesHashLabel = "odigos.io/profiles-hash" + + // for resources auto created by a profile, this annotation will record + // the name of the profile that created them. + OdigosProfileAnnotation = "odigos.io/profile" +) + +const ( + OdigosDeploymentConfigMapName = "odigos-deployment" + OdigosDeploymentConfigMapVersionKey = commonconsts.OdigosVersionEnvVarName + OdigosDeploymentConfigMapTierKey = commonconsts.OdigosTierEnvVarName + OdigosDeploymentConfigMapInstallationMethodKey = "installation-method" + OdigosDeploymentConfigMapOnPremTokenAudKey = "onprem-token-audience" + OdigosDeploymentConfigMapOnPremTokenExpKey = "onprem-token-expiration" + OdigosDeploymentConfigMapOnPremClientProfilesKey = "onprem-profiles" +) + +const ( + OdigosClusterCollectorDeploymentName = "odigos-gateway" + OdigosClusterCollectorConfigMapName = OdigosClusterCollectorDeploymentName + OdigosClusterCollectorServiceName = OdigosClusterCollectorDeploymentName + + OdigosClusterCollectorCollectorGroupName = OdigosClusterCollectorDeploymentName + OdigosClusterCollectorConfigMapKey = "collector-conf" + + // The cluster gateway collector runs as a deployment and the pod is exposed as a service. + // Thus it cannot collide with other ports on the same node, and we can use an handy default port. + OdigosClusterCollectorOwnTelemetryPortDefault = int32(8888) +) + +const ( + OdigosNodeCollectorDaemonSetName = "odigos-data-collection" + OdigosNodeCollectorConfigMapName = OdigosNodeCollectorDaemonSetName + OdigosNodeCollectorCollectorGroupName = OdigosNodeCollectorDaemonSetName + OdigosNodeCollectorOwnTelemetryPortDefault = int32(55682) + + OdigosNodeCollectorConfigMapKey = "conf" // this key is different than the cluster collector value. not sure why +) + +const ( + OdigosProSecretName = "odigos-pro" + OdigosProSecretTokenKeyName = "odigos-onprem-token" +) + +const ( + OdigosEnvVarNamespace = "ODIGOS_WORKLOAD_NAMESPACE" + OdigosEnvVarContainerName = "ODIGOS_CONTAINER_NAME" + OdigosEnvVarPodName = "ODIGOS_POD_NAME" +) + +func OdigosInjectedEnvVars() []string { + return []string{ + OdigosEnvVarNamespace, + OdigosEnvVarContainerName, + OdigosEnvVarPodName, + } +} + +var ( + // MinK8SVersionForInstallation is the minimum Kubernetes version required for Odigos installation + // this value must be in sync with the one defined in the kubeVersion field in Chart.yaml + MinK8SVersionForInstallation = version.MustParse("v1.20.15-0") +) + +const ( + // StartLangDetectionFinalizer is used for Workload exclusion Sources. When a Workload exclusion Source + // is deleted, we want to go to the startlangdetection controller. There, we will check if the Workload should + // start inheriting Namespace instrumentation. + StartLangDetectionFinalizer = "odigos.io/source-startlangdetection-finalizer" + // DeleteInstrumentationConfigFinalizer is used for all non-exclusion (normal) Sources. When a normal Source + // is deleted, we want to go to the deleteinstrumentationconfig controller to un-instrument the workload/namespace. + DeleteInstrumentationConfigFinalizer = "odigos.io/source-deleteinstrumentationconfig-finalizer" + + WorkloadNameLabel = "odigos.io/workload-name" + WorkloadNamespaceLabel = "odigos.io/workload-namespace" + WorkloadKindLabel = "odigos.io/workload-kind" + + OdigosCloudApiKeySecretKey = "odigos-cloud-api-key" + OdigosOnpremTokenSecretKey = "odigos-onprem-token" +) + +const ( + OdigosUiServiceName = "ui" + OdigosUiServicePort = 3000 +) diff --git a/k8sutils/pkg/describe/odigos.go b/k8sutils/pkg/describe/odigos.go index b8e770718f..4c14132999 100644 --- a/k8sutils/pkg/describe/odigos.go +++ b/k8sutils/pkg/describe/odigos.go @@ -46,6 +46,16 @@ func printOdigosPipeline(analyze *odigos.OdigosAnalyze, sb *strings.Builder) { printNodeCollectorStatus(analyze, sb) } +func printOdigosPro(analyze *odigos.OdigosAnalyze, sb *strings.Builder) { + if !(analyze.OdigosPro == odigos.OdigosPro{}) { + describeText(sb, 0, "Odigos Pro:") + printProperty(sb, 1, &analyze.OdigosPro.OnpremTokenAud) + printProperty(sb, 1, &analyze.OdigosPro.OnpremTokenExpiration) + printProperty(sb, 1, &analyze.OdigosPro.OdigosProfiles) + sb.WriteString("\n") + } +} + func DescribeOdigosToText(analyze *odigos.OdigosAnalyze) string { var sb strings.Builder @@ -54,6 +64,7 @@ func DescribeOdigosToText(analyze *odigos.OdigosAnalyze) string { printProperty(&sb, 0, &analyze.Tier) printProperty(&sb, 0, &analyze.InstallationMethod) sb.WriteString("\n") + printOdigosPro(analyze, &sb) printOdigosPipeline(analyze, &sb) return sb.String() diff --git a/k8sutils/pkg/describe/odigos/analyze.go b/k8sutils/pkg/describe/odigos/analyze.go index 7d22bdfc98..443256ad13 100644 --- a/k8sutils/pkg/describe/odigos/analyze.go +++ b/k8sutils/pkg/describe/odigos/analyze.go @@ -7,6 +7,7 @@ import ( "github.com/odigos-io/odigos/api/k8sconsts" odigosv1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" + "github.com/odigos-io/odigos/common" "github.com/odigos-io/odigos/k8sutils/pkg/describe/properties" ) @@ -36,6 +37,12 @@ type NodeCollectorAnalyze struct { AvailableNodes *properties.EntityProperty `json:"availableNodes,omitempty"` } +type OdigosPro struct { + OnpremTokenAud properties.EntityProperty `json:"onpremTokenAudience,omitempty"` + OnpremTokenExpiration properties.EntityProperty `json:"onpremTokenExpiration,omitempty"` + OdigosProfiles properties.EntityProperty `json:"odigosProfiles,omitempty"` +} + type OdigosAnalyze struct { OdigosVersion properties.EntityProperty `json:"odigosVersion"` KubernetesVersion properties.EntityProperty `json:"kubernetesVersion"` @@ -45,6 +52,7 @@ type OdigosAnalyze struct { NumberOfSources int `json:"numberOfSources"` ClusterCollector ClusterCollectorAnalyze `json:"clusterCollector"` NodeCollector NodeCollectorAnalyze `json:"nodeCollector"` + OdigosPro OdigosPro `json:"odigosPro,omitempty"` // is settled is true if all resources are created and ready IsSettled bool `json:"isSettled"` @@ -334,6 +342,37 @@ func analyzeNodeCollector(resources *OdigosResources) NodeCollectorAnalyze { } } +func analyzePro(resources *OdigosResources) OdigosPro { + odigosDeployment := resources.OdigosDeployment + tokenAud := odigosDeployment.Data[k8sconsts.OdigosDeploymentConfigMapOnPremTokenAudKey] + tokenExp := odigosDeployment.Data[k8sconsts.OdigosDeploymentConfigMapOnPremTokenExpKey] + profiles := odigosDeployment.Data[k8sconsts.OdigosDeploymentConfigMapOnPremClientProfilesKey] + + tokenAudProperty := properties.EntityProperty{ + Name: "OnPrem Token Audience", + Value: tokenAud, + Explain: "the audience of the on-prem token used to authenticate the odigos pro", + } + + tokenExpProperty := properties.EntityProperty{ + Name: "OnPrem Token Expiration Date", + Value: tokenExp, + Explain: "the expiration time of the on-prem token used to authenticate the odigos pro", + } + + profilesProperty := properties.EntityProperty{ + Name: "OnPrem Client Profiles", + Value: profiles, + Explain: "the Odigos profiles that are used to configure the odigos pro", + } + + return OdigosPro{ + OnpremTokenAud: tokenAudProperty, + OnpremTokenExpiration: tokenExpProperty, + OdigosProfiles: profilesProperty, + } +} + func summarizeStatus(clusterCollector *ClusterCollectorAnalyze, nodeCollector *NodeCollectorAnalyze) (bool, bool) { isSettled := true // everything is settled, unless we find property with status transitioning hasErrors := false // there is no error, unless we find property with status error @@ -383,6 +422,7 @@ func AnalyzeOdigos(resources *OdigosResources) *OdigosAnalyze { odigosVersion := resources.OdigosDeployment.Data[k8sconsts.OdigosDeploymentConfigMapVersionKey] tier := resources.OdigosDeployment.Data[k8sconsts.OdigosDeploymentConfigMapTierKey] + installationMethod := resources.OdigosDeployment.Data[k8sconsts.OdigosDeploymentConfigMapInstallationMethodKey] k8sVersion := resources.OdigosDeployment.Data[k8sconsts.OdigosDeploymentConfigMapKubernetesVersionKey] @@ -410,7 +450,7 @@ func AnalyzeOdigos(resources *OdigosResources) *OdigosAnalyze { Explain: "the version of kubernetes cluster where odigos is deployed", } - return &OdigosAnalyze{ + odigosAnalyze := &OdigosAnalyze{ OdigosVersion: odigosVersionProperty, KubernetesVersion: k8sVersionProperty, Tier: odigosTierProperty, @@ -419,8 +459,13 @@ func AnalyzeOdigos(resources *OdigosResources) *OdigosAnalyze { NumberOfSources: len(resources.InstrumentationConfigs.Items), ClusterCollector: clusterCollector, NodeCollector: nodeCollector, + IsSettled: isSettled, + HasErrors: hasErrors, + } - IsSettled: isSettled, - HasErrors: hasErrors, + if odigosTierProperty.Value == string(common.OnPremOdigosTier) { + odigosAnalyze.OdigosPro = analyzePro(resources) } + + return odigosAnalyze } diff --git a/k8sutils/pkg/predicate/objectname.go b/k8sutils/pkg/predicate/objectname.go index ac99162491..350d27cd32 100644 --- a/k8sutils/pkg/predicate/objectname.go +++ b/k8sutils/pkg/predicate/objectname.go @@ -99,3 +99,12 @@ var NodeCollectorsDaemonSetPredicate = ObjectNamePredicate{ var ClusterCollectorDeploymentPredicate = ObjectNamePredicate{ AllowedObjectName: k8sconsts.OdigosClusterCollectorDeploymentName, } + +// this predicate will only allow events for the odigos cluster collectors daemon set object. +// this is useful if you only want to reconcile events for the cluster collectors daemon set object and ignore other daemon set objects. +var OdigosProSecretPredicate = ObjectNamePredicate{ + AllowedObjectName: k8sconsts.OdigosProSecretName, +} +var OdigosDeploymentConfigMapPredicate = ObjectNamePredicate{ + AllowedObjectName: k8sconsts.OdigosDeploymentConfigMapName, +} diff --git a/scheduler/controllers/odigospro/manager.go b/scheduler/controllers/odigospro/manager.go new file mode 100644 index 0000000000..09434f6f72 --- /dev/null +++ b/scheduler/controllers/odigospro/manager.go @@ -0,0 +1,37 @@ +package odigospro + +import ( + odigospredicates "github.com/odigos-io/odigos/k8sutils/pkg/predicate" + corev1 "k8s.io/api/core/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +func SetupWithManager(mgr ctrl.Manager) error { + + err := ctrl.NewControllerManagedBy(mgr). + For(&corev1.Secret{}). + Named("odigospro-odigospro"). + WithEventFilter(&odigospredicates.OdigosProSecretPredicate). + Complete(&odigossecretController{ + Client: mgr.GetClient(), + }) + if err != nil { + return err + } + + // it is possbile that the secret was deleted when the controller was down. + // we want to sync the odigos deployment config map with the secret on startup to reconcile any deleted pro info. + err = ctrl.NewControllerManagedBy(mgr). + For(&corev1.ConfigMap{}). + Named("odigospro-odigosdeployment"). + WithEventFilter(predicate.And(&odigospredicates.OdigosDeploymentConfigMapPredicate, &odigospredicates.CreationPredicate{})). + Complete(&odigossecretController{ + Client: mgr.GetClient(), + }) + if err != nil { + return err + } + + return nil +} diff --git a/scheduler/controllers/odigospro/odigospro_controller.go b/scheduler/controllers/odigospro/odigospro_controller.go new file mode 100644 index 0000000000..fa52f6b053 --- /dev/null +++ b/scheduler/controllers/odigospro/odigospro_controller.go @@ -0,0 +1,137 @@ +package odigospro + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/golang-jwt/jwt/v4" + k8sconsts "github.com/odigos-io/odigos/k8sutils/pkg/consts" + "github.com/odigos-io/odigos/k8sutils/pkg/env" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type odigossecretController struct { + client.Client +} + +// TODO: logger +func (r *odigossecretController) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + odigosNs := env.GetCurrentNamespace() + + odigosDeploymentConfig := &corev1.ConfigMap{} + err := r.Client.Get(ctx, types.NamespacedName{Namespace: odigosNs, Name: k8sconsts.OdigosDeploymentConfigMapName}, odigosDeploymentConfig) + if err != nil { + return ctrl.Result{}, reconcile.TerminalError(err) + } + + proSecret := &corev1.Secret{} + err = r.Client.Get(ctx, client.ObjectKey{Namespace: odigosNs, Name: k8sconsts.OdigosProSecretName}, proSecret) + + if apierrors.IsNotFound(err) { + deleted := deleteProInfoFromConfigMap(odigosDeploymentConfig) + if deleted { + logger.V(0).Info("OdigosPro secret not found, deleting pro info from odigos deployment config") + } + } else { + err := updateProInfoInConfigMap(odigosDeploymentConfig, proSecret) + if err != nil { + return ctrl.Result{}, reconcile.TerminalError(err) + } + logger.V(0).Info("Updated pro info in odigos deployment config") + } + + err = r.Client.Update(ctx, odigosDeploymentConfig) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +// delete the pro info and retrun if the info was deleted +func deleteProInfoFromConfigMap(odigosDeploymentConfig *corev1.ConfigMap) bool { + _, foundAudiance := odigosDeploymentConfig.Data[k8sconsts.OdigosDeploymentConfigMapOnPremTokenAudKey] + delete(odigosDeploymentConfig.Data, k8sconsts.OdigosDeploymentConfigMapOnPremTokenAudKey) + delete(odigosDeploymentConfig.Data, k8sconsts.OdigosDeploymentConfigMapOnPremTokenExpKey) + delete(odigosDeploymentConfig.Data, k8sconsts.OdigosDeploymentConfigMapOnPremClientProfilesKey) + return foundAudiance +} + +func updateProInfoInConfigMap(odigosDeploymentConfig *corev1.ConfigMap, proSecret *corev1.Secret) error { + tokenString := proSecret.Data[k8sconsts.OdigosProSecretTokenKeyName] + if tokenString == nil { + return fmt.Errorf("error: token not found in secret") + } + + token, _, err := jwt.NewParser().ParseUnverified(string(tokenString), jwt.MapClaims{}) + if err != nil { + return fmt.Errorf("error: failed to parse JWT token: %w", err) + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return fmt.Errorf("error: failed to parse JWT token claims") + } + + audience, ok := claims["aud"].(string) + if !ok { + return fmt.Errorf("error: failed to parse JWT token audience") + } + + expiry, ok := claims["exp"].(float64) + if !ok { + return fmt.Errorf("error: failed to parse JWT token expiry") + } + + profilesString, profilesExists, err := getProfilesString(claims) + if err != nil { + return err + } + + odigosDeploymentConfig.Data[k8sconsts.OdigosDeploymentConfigMapOnPremTokenAudKey] = audience + odigosDeploymentConfig.Data[k8sconsts.OdigosDeploymentConfigMapOnPremTokenExpKey] = time.Unix(int64(expiry), 0).UTC().Format("02 Jan 2006 03:04:05 PM") + if profilesExists { + odigosDeploymentConfig.Data[k8sconsts.OdigosDeploymentConfigMapOnPremClientProfilesKey] = profilesString + } else { + delete(odigosDeploymentConfig.Data, k8sconsts.OdigosDeploymentConfigMapOnPremClientProfilesKey) + } + + return nil +} + +func getProfilesString(claims jwt.MapClaims) (string, bool, error) { + profiles, ok := claims["profiles"] + if !ok { + return "", false, nil + } + + profilesSlice, ok := profiles.([]interface{}) + if !ok { + return "", false, fmt.Errorf("error: failed to parse JWT token profiles") + } + + if len(profilesSlice) == 0 { + return "", false, nil + } + + profileStrings := make([]string, 0, len(profilesSlice)) + for _, profile := range profilesSlice { + profileString, ok := profile.(string) + if !ok { + return "", false, fmt.Errorf("error: found JWT profile which is not a string") + } + profileStrings = append(profileStrings, profileString) + } + + return strings.Join(profileStrings, ", "), true, nil +} diff --git a/scheduler/go.mod b/scheduler/go.mod index c7cfb01a34..b7bebeabad 100644 --- a/scheduler/go.mod +++ b/scheduler/go.mod @@ -4,6 +4,7 @@ go 1.23.0 require ( github.com/go-logr/zapr v1.3.0 + github.com/golang-jwt/jwt/v4 v4.5.1 github.com/odigos-io/odigos/api v0.0.0 github.com/odigos-io/odigos/common v0.0.0 github.com/odigos-io/odigos/k8sutils v0.0.0 diff --git a/scheduler/go.sum b/scheduler/go.sum index 41d6bdbc72..21a9e07927 100644 --- a/scheduler/go.sum +++ b/scheduler/go.sum @@ -54,6 +54,8 @@ github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I= github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= diff --git a/scheduler/main.go b/scheduler/main.go index 4a4a530efa..793b4cc7c3 100644 --- a/scheduler/main.go +++ b/scheduler/main.go @@ -48,6 +48,7 @@ import ( "github.com/odigos-io/odigos/scheduler/controllers/clustercollectorsgroup" "github.com/odigos-io/odigos/scheduler/controllers/nodecollectorsgroup" "github.com/odigos-io/odigos/scheduler/controllers/odigosconfig" + "github.com/odigos-io/odigos/scheduler/controllers/odigospro" //+kubebuilder:scaffold:imports ) @@ -154,6 +155,11 @@ func main() { setupLog.Error(err, "unable to create controllers for odigos config") os.Exit(1) } + err = odigospro.SetupWithManager(mgr) + if err != nil { + setupLog.Error(err, "unable to create controller for odigos pro") + os.Exit(1) + } if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check")