diff --git a/README.md b/README.md index cbc7dbe..17d3e2f 100644 --- a/README.md +++ b/README.md @@ -15,23 +15,24 @@ that are not exposed natively In our platform team, we needed to build some useful dashboards to show our developers the status of the pipelines per project to make them compete for the first position on the reliability podium. -There are some metrics that are not exposed to achieve that goal, so we decided to do this little tool +There are some metrics that are not exposed, which are necessary to achieve our goal, +so we decided to create this small tool. ## Flags Every configuration parameter can be defined by flags that can be passed to the CLI. They are described in the following table: -| Name | Description | Default Example | | -|:---------------------|:----------------------------------------------------|:---------------:|------------------------------------------------------------| -| `--log-level` | Define the verbosity of the logs | `info` | `--log-level info` | -| `--disable-trace` | Disable traces from logs | `false` | `--disable-trace true` | -| `--kubeconfig` | Path to kubeconfig | `-` | `--kubeconfig="~/.kube/config"` | -| `--metrics-port` | Port where metrics web-server will run | `2112` | `--metrics-port 9090` | -| `--metrics-host` | Host where metrics web-server will run | `0.0.0.0` | `--metrics-host 10.10.10.1` | -| `--populated-labels` | Comma-separated list of labels populated on metrics | `-` | `--populated-labels "apiVersion,pipelineName,projectName"` | +| Name | Description | Default Example | | +|:---------------------|:------------------------------------------------------------------------|:---------------:|------------------------------------------------------------| +| `--log-level` | Define the verbosity of the logs | `info` | `--log-level info` | +| `--disable-trace` | Disable traces from logs | `false` | `--disable-trace true` | +| `--kubeconfig` | Path to kubeconfig | `-` | `--kubeconfig="~/.kube/config"` | +| `--metrics-port` | Port where metrics web-server will run | `2112` | `--metrics-port 9090` | +| `--metrics-host` | Host where metrics web-server will run | `0.0.0.0` | `--metrics-host 10.10.10.1` | +| `--populated-labels` | (Repeatable or comma-separated list) Object labels populated on metrics | `-` | `--populated-labels "apiVersion,pipelineName,projectName"` | -> For Prometheus SDK it is mandatory to register the metrics before using them. +> For Prometheus SDK, it is mandatory to register the metrics before using them. > Due to this, if you use `--populated-labels` flag and the label is not present in some PipelineRun or TaskRun > the label will be populated with `#` as value diff --git a/internal/cmd/run/run.go b/internal/cmd/run/run.go index fa01d39..2af7507 100644 --- a/internal/cmd/run/run.go +++ b/internal/cmd/run/run.go @@ -49,14 +49,7 @@ func NewCommand() *cobra.Command { // It's declared here for documentation purposes only cmd.Flags().String("kubeconfig", "~/.kube/config", "Path to the kubeconfig file") - cmd.Flags().StringSlice("populated-labels", []string{}, "Comma-separated list of labels populated on metrics") - - //cmd.Flags().Bool("watch-all-namespaces", false, "Enable watching resources on all namespaces") - //cmd.Flags().String("watch-namespace", "default", "Namespace to watch") - - // Conditions - //cmd.MarkFlagsOneRequired("watch-all-namespaces", "watch-namespace") - //cmd.MarkFlagsMutuallyExclusive("watch-all-namespaces", "watch-namespace") + cmd.Flags().StringSlice("populated-labels", []string{}, "(Repeatable or comma-separated list) Object labels populated on metrics") return cmd } @@ -97,23 +90,15 @@ func RunCommand(cmd *cobra.Command, args []string) { log.Fatalf(PopulatedLabelsFlagErrorMessage, err) } - //watchAllNamespacesFlag, err := cmd.Flags().GetBool("watch-all-namespaces") - //if err != nil { - // log.Fatalf(WatchAllNamespacesFlagErrorMessage, err) - //} - // - //watchNamespaceFlag, err := cmd.Flags().GetString("watch-namespace") - //if err != nil { - // log.Fatalf(WatchNamespaceFlagErrorMessage, err) - //} + // Handle a potentially confusing situation: + // Cobra flags' library does not properly parse + // comma-separated lists depending on the environment + // the CLI is running (i.e. Kubernetes), + populatedLabelsFlag = globals.SplitCommaSeparatedValues(populatedLabelsFlag) // Store populated labels in context to use them later globals.ExecContext.Context = context.WithValue(globals.ExecContext.Context, "flag-populated-labels", populatedLabelsFlag) - //globals.ExecContext.Context = context.WithValue(globals.ExecContext.Context, - // "flag-watch-all-namespaces", watchAllNamespacesFlag) - //globals.ExecContext.Context = context.WithValue(globals.ExecContext.Context, - // "flag-watch-namespace", watchNamespaceFlag) // Register metrics into Prometheus Registry metrics.RegisterMetrics(populatedLabelsFlag) @@ -124,7 +109,7 @@ func RunCommand(cmd *cobra.Command, args []string) { // Process PipelineRun resources in the background // TODO: Errors for watcher must be shown inside the watcher as this is a goroutine go func() { - err := kubernetes.WatchPipelineRuns(globals.ExecContext.Context, client) + err := kubernetes.WatchPipelineRuns(&globals.ExecContext.Context, client) if err != nil { } @@ -133,7 +118,7 @@ func RunCommand(cmd *cobra.Command, args []string) { // Process TaskRun resources in the background // TODO: Errors for watcher must be shown inside the watcher as this is a goroutine go func() { - err := kubernetes.WatchTaskRuns(globals.ExecContext.Context, client) + err := kubernetes.WatchTaskRuns(&globals.ExecContext.Context, client) if err != nil { } diff --git a/internal/globals/utils.go b/internal/globals/utils.go new file mode 100644 index 0000000..a7697f1 --- /dev/null +++ b/internal/globals/utils.go @@ -0,0 +1,24 @@ +package globals + +import "strings" + +// CopyMap return a map that is a real copy of the original +// Ref: https://go.dev/blog/maps +func CopyMap(src map[string]interface{}) map[string]interface{} { + m := make(map[string]interface{}, len(src)) + for k, v := range src { + m[k] = v + } + return m +} + +// SplitCommaSeparatedValues get a list of strings and return a new list +// where each element containing commas is divided in separated elements +func SplitCommaSeparatedValues(input []string) []string { + var result []string + for _, item := range input { + parts := strings.Split(item, ",") + result = append(result, parts...) + } + return result +} diff --git a/internal/kubernetes/kubernetes.go b/internal/kubernetes/kubernetes.go index 17ab9bb..3fed3b0 100644 --- a/internal/kubernetes/kubernetes.go +++ b/internal/kubernetes/kubernetes.go @@ -2,7 +2,6 @@ package kubernetes import ( "context" - "fmt" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" "k8s.io/apimachinery/pkg/runtime" @@ -44,7 +43,7 @@ func NewClient() (client *dynamic.DynamicClient, err error) { // GetNamespaces get a list of all namespaces existing in the cluster // TODO: Evaluate if this method is needed -func GetNamespaces(ctx context.Context, client *dynamic.DynamicClient) (namespaces *unstructured.UnstructuredList, err error) { +func GetNamespaces(ctx *context.Context, client *dynamic.DynamicClient) (namespaces *unstructured.UnstructuredList, err error) { resourceId := schema.GroupVersionResource{ Group: "", @@ -52,7 +51,7 @@ func GetNamespaces(ctx context.Context, client *dynamic.DynamicClient) (namespac Resource: "namespaces", } - namespaceList, err := client.Resource(resourceId).List(ctx, metav1.ListOptions{}) + namespaceList, err := client.Resource(resourceId).List(*ctx, metav1.ListOptions{}) if err != nil { return namespaces, err @@ -61,27 +60,8 @@ func GetNamespaces(ctx context.Context, client *dynamic.DynamicClient) (namespac return namespaceList, err } -// GetAllPipelineRuns -// TODO: Evaluate if this method is needed -func GetAllPipelineRuns(ctx context.Context, client *dynamic.DynamicClient) (resources []unstructured.Unstructured, err error) { - - globals.ExecContext.Logger.Info("pepe") - - resourceId := schema.GroupVersionResource{ - Group: "tekton.dev", - Version: "v1", - Resource: "pipelineruns", - } - - list, err := client.Resource(resourceId).Namespace("freeclip").List(ctx, metav1.ListOptions{}) - _ = list - //log.Print(list.Items[0]) - - return resources, nil -} - // WatchPipelineRuns TODO -func WatchPipelineRuns(ctx context.Context, client *dynamic.DynamicClient) (err error) { +func WatchPipelineRuns(ctx *context.Context, client *dynamic.DynamicClient) (err error) { populatedLabels := map[string]string{} calculatedLabels := map[string]string{} @@ -94,7 +74,7 @@ func WatchPipelineRuns(ctx context.Context, client *dynamic.DynamicClient) (err } // TODO - pipelineRunWatcher, err := client.Resource(resourceId).Watch(ctx, metav1.ListOptions{}) + pipelineRunWatcher, err := client.Resource(resourceId).Watch(*ctx, metav1.ListOptions{}) if err != nil { return err } @@ -177,7 +157,7 @@ func WatchPipelineRuns(ctx context.Context, client *dynamic.DynamicClient) (err } // WatchTaskRuns TODO -func WatchTaskRuns(ctx context.Context, client *dynamic.DynamicClient) (err error) { +func WatchTaskRuns(ctx *context.Context, client *dynamic.DynamicClient) (err error) { populatedLabels := map[string]string{} calculatedLabels := map[string]string{} @@ -190,7 +170,7 @@ func WatchTaskRuns(ctx context.Context, client *dynamic.DynamicClient) (err erro } // TODO: Delete the namespace once the controller is fully working - taskRunWatcher, err := client.Resource(resourceId).Namespace("freeclip").Watch(ctx, metav1.ListOptions{}) + taskRunWatcher, err := client.Resource(resourceId).Namespace("freeclip").Watch(*ctx, metav1.ListOptions{}) if err != nil { return err } @@ -278,13 +258,12 @@ func GetObjectLabels(obj *runtime.Object) (labelsMap map[string]string, err erro labelsMap = make(map[string]string) - // Iterar sobre el mapa original y hacer el "casting" de los valores + // Iterate over the original map and cast its values for key, value := range objectLabels { strValue, ok := value.(string) if !ok { - // Manejo del error si el valor no es un string - fmt.Printf("El valor para '%s' no es un string y no puede ser convertido.\n", key) - continue // Opcionalmente, puedes decidir cómo manejar este caso. + globals.ExecContext.Logger.Infof("Value of label '%s' is not a string. Ignoring it", key) + continue } labelsMap[key] = strValue } @@ -294,7 +273,7 @@ func GetObjectLabels(obj *runtime.Object) (labelsMap map[string]string, err erro // GetObjectPopulatedLabels return only user's desired labels from an object of type runtime.Object // Desired labels are defined by flag "--populated-labels" -func GetObjectPopulatedLabels(ctx context.Context, object *runtime.Object) (labelsMap map[string]string, err error) { +func GetObjectPopulatedLabels(ctx *context.Context, object *runtime.Object) (labelsMap map[string]string, err error) { // Read labels from event's resource objectLabels, err := GetObjectLabels(object) @@ -307,9 +286,9 @@ func GetObjectPopulatedLabels(ctx context.Context, object *runtime.Object) (labe return labelsMap, nil } - // TODO + // Recover flag 'populated-labels' from context populatedLabels := map[string]string{} - populatedLabelsFlag := ctx.Value("flag-populated-labels").([]string) + populatedLabelsFlag := (*ctx).Value("flag-populated-labels").([]string) // parsedLabelsMap, _ := metrics.GetProcessedLabels(populatedLabelsFlag) // TODO: Handle error diff --git a/internal/kubernetes/utils.go b/internal/kubernetes/utils.go deleted file mode 100644 index 681effa..0000000 --- a/internal/kubernetes/utils.go +++ /dev/null @@ -1,11 +0,0 @@ -package kubernetes - -// CopyMap return a map that is a real copy of the original -// Ref: https://go.dev/blog/maps -func CopyMap(src map[string]interface{}) map[string]interface{} { - m := make(map[string]interface{}, len(src)) - for k, v := range src { - m[k] = v - } - return m -}