From c09726f8c0fc6c866c9f5540be3757b3844ad43b Mon Sep 17 00:00:00 2001 From: b0m313 Date: Mon, 11 Mar 2024 22:09:36 +0000 Subject: [PATCH 1/2] feat: Add various CEL to label conversions --- examples/env/httpd-deploy.yaml | 23 ++ .../cel-multi-si-sib-namespaced.yaml | 22 +- pkg/processor/policybuilder/common.go | 238 +++++++++++++++--- 3 files changed, 252 insertions(+), 31 deletions(-) create mode 100644 examples/env/httpd-deploy.yaml diff --git a/examples/env/httpd-deploy.yaml b/examples/env/httpd-deploy.yaml new file mode 100644 index 00000000..00b2b811 --- /dev/null +++ b/examples/env/httpd-deploy.yaml @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2023 Authors of Nimbus + +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: httpd + name: httpd +spec: + replicas: 1 + selector: + matchLabels: + app: httpd + template: + metadata: + labels: + app: httpd + spec: + containers: + - image: httpd + imagePullPolicy: Always + name: httpd diff --git a/examples/namespaced/cel-multi-si-sib-namespaced.yaml b/examples/namespaced/cel-multi-si-sib-namespaced.yaml index 107b00f3..2dd5da3c 100644 --- a/examples/namespaced/cel-multi-si-sib-namespaced.yaml +++ b/examples/namespaced/cel-multi-si-sib-namespaced.yaml @@ -40,9 +40,25 @@ spec: selector: cel: - labels["app"] == "nginx" - #- labels["app"].equals("nginx") + + #- labels["app"] == "nginx" + #- "'labels[\"app\"] == \"nginx\"'" + #- labels["app"] in ["nginx", "nginx-2"] #- labels["app"].contains("nginx") - #- labels["app"] in ["nginx"] #- labels["app"].startsWith("nginx") #- labels["app"].endsWith("nginx") - #- labels["group"] == "group-1" + #- labels["app"].matches(".*nginx.*") + + # Because certain characters or phrases are used as reserved words or have special meaning in YAML, + # you can't use the negation operator '!' of the Common Expression Language (CEL) directly + # Represent negation statements as strings + + #- "'labels[\"app\"] != \"nginx\"'" + #- "'!(labels[\"app\"] in [\"nginx\", \"httpd\"])'" + #- "'!(labels[\"app\"] in [\"nginx\", \"nginx-2\"])'" + #- "'!labels[\"app\"].contains(\"nginx\")'" + #- "'!labels[\"app\"].startsWith(\"nginx\")'" + #- "'!labels[\"app\"].endsWith(\"nginx\")'" + #- "'!labels["app"].matches(".*nginx.*")'" + + diff --git a/pkg/processor/policybuilder/common.go b/pkg/processor/policybuilder/common.go index 82a735f2..a8ab4250 100644 --- a/pkg/processor/policybuilder/common.go +++ b/pkg/processor/policybuilder/common.go @@ -6,6 +6,7 @@ package policybuilder import ( "context" "fmt" + "regexp" "strings" "github.com/google/cel-go/cel" @@ -40,11 +41,11 @@ func ProcessCEL(ctx context.Context, k8sClient client.Client, namespace string, return nil, fmt.Errorf("error listing pods: %v", err) } - // Initialize an empty map to store label expressions - labelExpressions := make(map[string]bool) - // Parse and evaluate label expressions for _, expr := range expressions { + isNegated := checkNegation(expr) + expr = PreprocessExpression(expr) + ast, issues := env.Compile(expr) if issues != nil && issues.Err() != nil { return nil, fmt.Errorf("error compiling CEL expression: %v", issues.Err()) @@ -72,52 +73,233 @@ func ProcessCEL(ctx context.Context, k8sClient client.Client, namespace string, if outValue, ok := out.Value().(bool); ok && outValue { // Mark this expression as true for at least one pod - labelExpressions[expr] = true + labels := extractLabelsFromExpression(expr, podList, isNegated) + for k, v := range labels { + matchLabels[k] = v + } } } } + return matchLabels, nil +} - // Extract labels based on true label expressions - for expr, isTrue := range labelExpressions { - if isTrue { - // Extract labels from the expression and add them to matchLabels - labels := extractLabelsFromExpression(expr) - for k, v := range labels { - matchLabels[k] = v +func extractLabelsFromExpression(expr string, podList corev1.PodList, isNegated bool) map[string]string { + labels := make(map[string]string) + + if strings.Contains(expr, "==") || strings.Contains(expr, "!=") { + key, value := parseKeyValueExpression(expr) + labels[key] = value + } else if strings.Contains(expr, ".contains(") { + key, value := parseFunctionExpression(expr, "contains") + if key != "" && value != "" { + labels[key] = value + } + } else if strings.Contains(expr, " in ") { + key, values := parseInExpression(expr) + for _, pod := range podList.Items { + labelValue, exists := pod.Labels[key] + if !exists { + continue + } + if contains(values, labelValue) { + labels[key] = labelValue } } + } else if strings.Contains(expr, ".startsWith(") { + labels = parseStartsWithEndsWithExpression(expr, podList, "startsWith") + } else if strings.Contains(expr, ".endsWith(") { + labels = parseStartsWithEndsWithExpression(expr, podList, "endsWith") + } else if strings.Contains(expr, ".matches(") { + labels = parseMatchesExpression(expr, podList) + } + if isNegated { + labels = excludeLabels(podList, labels) } + return labels +} - return matchLabels, nil +// Helper function to check if expression is negated (!expr) and extract clean expression. +func checkNegation(expr string) bool { + isNegated := strings.HasPrefix(expr, "'!") || strings.HasPrefix(expr, "!") || strings.HasPrefix(expr, "!='") || strings.Contains(expr, " != ") + return isNegated } -// Extracts labels from a CEL expression -func extractLabelsFromExpression(expr string) map[string]string { - // This function is simplified and can be expanded based on specific needs. - labels := make(map[string]string) +func PreprocessExpression(expr string) string { + expr = strings.TrimSpace(expr) + expr = regexp.MustCompile(`^['"]|['"]$`).ReplaceAllString(expr, "") + expr = strings.ReplaceAll(expr, `\"`, `"`) + expr = strings.ReplaceAll(expr, `\'`, `'`) + expr = strings.Replace(expr, `\"`, `"`, -1) + expr = regexp.MustCompile(`^['"]|['"]$`).ReplaceAllString(expr, "") + if strings.Count(expr, "\"")%2 != 0 { + expr += "\"" + } else if strings.Count(expr, "'")%2 != 0 { + expr += "'" + } + + return expr +} - // Simplified extraction logic for basic "key == value" expressions +func parseKeyValueExpression(expr string) (string, string) { + expr = PreprocessExpression(expr) + var operator string if strings.Contains(expr, "==") { - parts := strings.Split(expr, "==") - if len(parts) == 2 { - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) + operator = "==" + } else if strings.Contains(expr, "!=") { + operator = "!=" + } else { + return "", "" + } - // Handle labels["key"] pattern - key = strings.TrimPrefix(key, `labels["`) - key = strings.TrimSuffix(key, `"]`) + parts := strings.SplitN(expr, operator, 2) + if len(parts) != 2 { + return "", "" + } - // Remove quotes from value if present - value = strings.Trim(value, "\"'") + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + key = strings.TrimPrefix(key, "labels[") + key = strings.TrimSuffix(key, "]") + key = strings.Trim(key, `"'`) + value = strings.Trim(value, `"'`) + return key, value +} - // Add the extracted label to the map - labels[key] = value +// Parses function expressions like 'labels["key"].contains("value")' +func parseFunctionExpression(expr string, functionName string) (string, string) { + start := strings.Index(expr, `labels["`) + len(`labels["`) + if start == -1 { + return "", "" // Key not found + } + end := strings.Index(expr[start:], `"]`) + if end == -1 { + return "", "" // Incorrectly formatted expression + } + key := expr[start : start+end] + + functionStart := strings.Index(expr, functionName+"(\"") + len(functionName+"(\"") + functionEnd := strings.LastIndex(expr, "\")") + if functionStart == -1 || functionEnd == -1 || functionStart >= functionEnd { + return "", "" // Function or value not found + } + value := expr[functionStart:functionEnd] + + return key, value +} + +func parseInExpression(expr string) (string, []string) { + start := strings.Index(expr, `labels["`) + len(`labels["`) + if start == -1 { + return "", nil // Key not found + } + end := strings.Index(expr[start:], `"]`) + if end == -1 { + return "", nil // Incorrectly formatted expression + } + key := expr[start : start+end] + + valuesStart := strings.Index(expr, " in [") + len(" in [") + valuesEnd := strings.LastIndex(expr, "]") + if valuesStart == -1 || valuesEnd == -1 || valuesStart >= valuesEnd { + return "", nil // Values not found + } + valuesString := expr[valuesStart:valuesEnd] + valuesParts := strings.Split(valuesString, ",") + + var values []string + for _, part := range valuesParts { + value := strings.TrimSpace(part) + value = strings.Trim(value, "\"'") + values = append(values, value) + } + + return key, values +} + +func parseStartsWithEndsWithExpression(expr string, podList corev1.PodList, functionName string) map[string]string { + labels := make(map[string]string) + key, pattern := parseFunctionExpression(expr, functionName) + + for _, pod := range podList.Items { + labelValue, exists := pod.Labels[key] + if !exists { + continue + } + + var match bool + if functionName == "startsWith" && strings.HasPrefix(labelValue, pattern) { + match = true + } else if functionName == "endsWith" && strings.HasSuffix(labelValue, pattern) { + match = true + } + + if match { + // If a label matches, add it to the labels map + labels[key] = labelValue + } + } + + return labels +} + +func parseMatchesExpression(expr string, podList corev1.PodList) map[string]string { + key, pattern := parseFunctionExpression(expr, "matches") + labels := make(map[string]string) + + regex, _ := regexp.Compile(pattern) + + for _, pod := range podList.Items { + labelValue, exists := pod.Labels[key] + if !exists { + continue + } + + // Check if the label's value matches the pattern + if regex.MatchString(labelValue) { + labels[key] = labelValue } } return labels } +func contains(slice []string, str string) bool { + for _, v := range slice { + if v == str { + return true + } + } + return false +} +func excludeLabels(podList corev1.PodList, excludeMap map[string]string) map[string]string { + remainingLabels := make(map[string]string) + + // Iterate through all pods in the namespace + for _, pod := range podList.Items { + // Check if the pod should be excluded based on the provided labels + exclude := false + for excludeKey, excludeValue := range excludeMap { + podLabelValue, exists := pod.Labels[excludeKey] + if exists && podLabelValue == excludeValue { + exclude = true + break + } + } + + // If the pod is not excluded, add its labels to the remainingLabels map + if !exclude { + for labelKey, labelValue := range pod.Labels { + // Exclude pod-template-hash labels by default + if labelKey != "pod-template-hash" { + remainingLabels[labelKey] = labelValue + } + } + } + } + + return remainingLabels +} + // ProcessMatchLabels processes any/all fields to generate matchLabels. func ProcessMatchLabels(any, all []v1.ResourceFilter) (map[string]string, error) { matchLabels := make(map[string]string) From 493d57868e4754df424eba4e9490735f88967096 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Tue, 12 Mar 2024 19:47:54 +0900 Subject: [PATCH 2/2] Update common.go --- pkg/processor/policybuilder/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/processor/policybuilder/common.go b/pkg/processor/policybuilder/common.go index a8ab4250..e386cd92 100644 --- a/pkg/processor/policybuilder/common.go +++ b/pkg/processor/policybuilder/common.go @@ -68,7 +68,7 @@ func ProcessCEL(ctx context.Context, k8sClient client.Client, namespace string, if err != nil { logger.Info("Error evaluating CEL expression for pod", "PodName", pod.Name, "error", err.Error()) // Instead of returning an error immediately, we log the error and continue. - break + continue } if outValue, ok := out.Value().(bool); ok && outValue {