-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add dynamicLabels to session pod config #113
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
base: main
Are you sure you want to change the base?
Changes from all commits
a83d555
7c9de7c
c70f317
f8c244d
588b8e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import ( | |
| "context" | ||
| "fmt" | ||
| "net/url" | ||
| "regexp" | ||
| "strings" | ||
|
|
||
| "github.com/posit-dev/team-operator/api/templates" | ||
|
|
@@ -32,8 +33,11 @@ type ServiceConfig struct { | |
| // PodConfig is the configuration for session pods | ||
| // +kubebuilder:object:generate=true | ||
| type PodConfig struct { | ||
| Annotations map[string]string `json:"annotations,omitempty"` | ||
| Labels map[string]string `json:"labels,omitempty"` | ||
| Annotations map[string]string `json:"annotations,omitempty"` | ||
| Labels map[string]string `json:"labels,omitempty"` | ||
| // DynamicLabels defines rules for generating pod labels from runtime session data. | ||
| // Requires template version 2.5.0 or later; ignored by older templates. | ||
| DynamicLabels []DynamicLabelRule `json:"dynamicLabels,omitempty"` | ||
| ServiceAccountName string `json:"serviceAccountName,omitempty"` | ||
| Volumes []corev1.Volume `json:"volumes,omitempty"` | ||
| VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` | ||
|
|
@@ -60,12 +64,70 @@ type JobConfig struct { | |
| Labels map[string]string `json:"labels,omitempty"` | ||
| } | ||
|
|
||
| // DynamicLabelRule defines a rule for generating pod labels from runtime session data. | ||
| // Each rule references a field from the .Job template object and either maps it directly | ||
| // to a label (using labelKey) or extracts multiple labels via regex (using match). | ||
| // +kubebuilder:object:generate=true | ||
| type DynamicLabelRule struct { | ||
| // Field is the name of a top-level .Job field to read (e.g., "user", "args"). | ||
| // Any .Job field is addressable — this relies on CRD write access being a privileged | ||
| // operation. Field values may appear as pod labels visible to anyone with pod read access. | ||
| // +kubebuilder:validation:MinLength=1 | ||
| Field string `json:"field"` | ||
| // LabelKey is the label key for direct single-value mapping. | ||
| // Mutually exclusive with match/labelPrefix. | ||
| // +kubebuilder:validation:MaxLength=63 | ||
| LabelKey string `json:"labelKey,omitempty"` | ||
| // Match is a regex pattern applied to the field value. Each match produces a label. | ||
| // For array fields (like "args"), elements are joined with spaces before matching. | ||
| // Mutually exclusive with labelKey. | ||
| // +kubebuilder:validation:MaxLength=256 | ||
| Match string `json:"match,omitempty"` | ||
| // TrimPrefix is stripped from each regex match before forming the label key suffix. | ||
| TrimPrefix string `json:"trimPrefix,omitempty"` | ||
| // LabelPrefix is prepended to the cleaned match to form the label key. | ||
| // Required when match is set. | ||
| // +kubebuilder:validation:MaxLength=253 | ||
| LabelPrefix string `json:"labelPrefix,omitempty"` | ||
| // LabelValue is the static value for all matched labels. Defaults to "true". | ||
| // +kubebuilder:validation:MaxLength=63 | ||
| LabelValue string `json:"labelValue,omitempty"` | ||
| } | ||
|
|
||
| // ValidateDynamicLabelRules validates a slice of DynamicLabelRule, checking for | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validation strategy: Mutual exclusivity ( |
||
| // regex compilation errors and mutual exclusivity of labelKey vs match/labelPrefix. | ||
| func ValidateDynamicLabelRules(rules []DynamicLabelRule) error { | ||
| for i, rule := range rules { | ||
| if rule.LabelKey != "" && rule.Match != "" { | ||
| return fmt.Errorf("dynamicLabels[%d]: labelKey and match are mutually exclusive", i) | ||
| } | ||
| if rule.LabelKey == "" && rule.Match == "" { | ||
| return fmt.Errorf("dynamicLabels[%d]: one of labelKey or match is required", i) | ||
| } | ||
| if rule.Match != "" && rule.LabelPrefix == "" { | ||
| return fmt.Errorf("dynamicLabels[%d]: labelPrefix is required when match is set", i) | ||
| } | ||
| if rule.Match != "" { | ||
| if _, err := regexp.Compile(rule.Match); err != nil { | ||
| return fmt.Errorf("dynamicLabels[%d]: invalid regex in match: %w", i, err) | ||
| } | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| type wrapperTemplateData struct { | ||
| Name string `json:"name"` | ||
| Value *SessionConfig `json:"value"` | ||
| } | ||
|
|
||
| func (s *SessionConfig) GenerateSessionConfigTemplate() (string, error) { | ||
| if s.Pod != nil && len(s.Pod.DynamicLabels) > 0 { | ||
| if err := ValidateDynamicLabelRules(s.Pod.DynamicLabels); err != nil { | ||
| return "", err | ||
| } | ||
| } | ||
|
|
||
| // build wrapper struct | ||
| w := wrapperTemplateData{ | ||
| Name: "rstudio-library.templates.data", | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,6 +78,22 @@ spec: | |
| {{ $key }}: {{ toYaml $val | indent 8 | trimPrefix (repeat 8 " ") }} | ||
| {{- end }} | ||
| {{- end }} | ||
| {{- with $templateData.pod.dynamicLabels }} | ||
| {{- range $rule := . }} | ||
| {{- if hasKey $.Job $rule.field }} | ||
| {{- $val := index $.Job $rule.field }} | ||
| {{- if $rule.labelKey }} | ||
| {{ $rule.labelKey }}: {{ $val | toString | quote }} | ||
| {{- else if $rule.match }} | ||
| {{- $str := (kindIs "slice" $val) | ternary ($val | join " ") ($val | toString) }} | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Scalar vs array handling: Uses |
||
| {{- $matches := regexFindAll $rule.match $str -1 }} | ||
| {{- range $match := $matches }} | ||
| {{ trimPrefix ($rule.trimPrefix | default "") $match | lower | replace " " "_" | trunc 63 | printf "%s%s" $rule.labelPrefix }}: {{ $rule.labelValue | default "true" | quote }} | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Design choice: The |
||
| {{- end }} | ||
| {{- end }} | ||
| {{- end }} | ||
| {{- end }} | ||
| {{- end }} | ||
| generateName: {{ toYaml .Job.generateName }} | ||
| spec: | ||
| {{- if .Job.host }} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trust model:
fieldaccepts any top-level.Jobkey, which means CRD authors can surface any launcher job field as a pod label. This is acceptable because CRD write access is already a privileged operation (cluster admin or namespace admin). Documented this explicitly in the field comment.