Skip to content

Commit

Permalink
Merge pull request #78 from seungsoo-lee/expand-cel
Browse files Browse the repository at this point in the history
feat: Add various CEL to label conversions
  • Loading branch information
seungsoo-lee authored Mar 12, 2024
2 parents 359d519 + 493d578 commit 317cb20
Show file tree
Hide file tree
Showing 3 changed files with 253 additions and 32 deletions.
23 changes: 23 additions & 0 deletions examples/env/httpd-deploy.yaml
Original file line number Diff line number Diff line change
@@ -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
22 changes: 19 additions & 3 deletions examples/namespaced/cel-multi-si-sib-namespaced.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.*")'"


240 changes: 211 additions & 29 deletions pkg/processor/policybuilder/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package policybuilder
import (
"context"
"fmt"
"regexp"
"strings"

"github.com/google/cel-go/cel"
Expand Down Expand Up @@ -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())
Expand All @@ -67,57 +68,238 @@ 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 {
// 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 += "'"
}

// Simplified extraction logic for basic "key == value" expressions
return expr
}

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)
Expand Down

0 comments on commit 317cb20

Please sign in to comment.