Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add various CEL to label conversions #78

Merged
merged 2 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading