From a83d555a2b2b7c35e5b6940e02889952c89d6ee8 Mon Sep 17 00:00:00 2001 From: ian-flores Date: Wed, 4 Mar 2026 14:10:31 -0800 Subject: [PATCH 1/4] feat: add dynamicLabels to session pod config Add DynamicLabelRule struct and dynamicLabels field on PodConfig, enabling per-session pod labels derived from runtime .Job data. Supports direct field mapping (labelKey) and regex pattern extraction (match + labelPrefix) for use cases like Entra group cost tracking. --- api/product/session_config.go | 24 ++++++++++ api/product/session_config_test.go | 46 +++++++++++++++++++ api/product/zz_generated.deepcopy.go | 20 ++++++++ api/templates/2.5.0/job.tpl | 15 ++++++ .../crd/bases/core.posit.team_connects.yaml | 39 ++++++++++++++++ .../bases/core.posit.team_workbenches.yaml | 39 ++++++++++++++++ 6 files changed, 183 insertions(+) diff --git a/api/product/session_config.go b/api/product/session_config.go index 7b251394..d9a4b7dc 100644 --- a/api/product/session_config.go +++ b/api/product/session_config.go @@ -34,6 +34,7 @@ type ServiceConfig struct { type PodConfig struct { Annotations map[string]string `json:"annotations,omitempty"` Labels map[string]string `json:"labels,omitempty"` + DynamicLabels []DynamicLabelRule `json:"dynamicLabels,omitempty"` ServiceAccountName string `json:"serviceAccountName,omitempty"` Volumes []corev1.Volume `json:"volumes,omitempty"` VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` @@ -60,6 +61,29 @@ 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"). + Field string `json:"field"` + // LabelKey is the label key for direct single-value mapping. + // Mutually exclusive with match/labelPrefix. + 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. + 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. + LabelPrefix string `json:"labelPrefix,omitempty"` + // LabelValue is the static value for all matched labels. Defaults to "true". + LabelValue string `json:"labelValue,omitempty"` +} + type wrapperTemplateData struct { Name string `json:"name"` Value *SessionConfig `json:"value"` diff --git a/api/product/session_config_test.go b/api/product/session_config_test.go index 77f4a76d..fe4cd792 100644 --- a/api/product/session_config_test.go +++ b/api/product/session_config_test.go @@ -63,6 +63,52 @@ func TestSessionConfig_GenerateSessionConfigTemplate(t *testing.T) { require.Contains(t, str, "\"mountPath\":\"/mnt/tmp\"") } +func TestSessionConfig_DynamicLabels(t *testing.T) { + t.Run("direct mapping rule serializes correctly", func(t *testing.T) { + config := SessionConfig{ + Pod: &PodConfig{ + DynamicLabels: []DynamicLabelRule{ + { + Field: "user", + LabelKey: "session.posit.team/user", + }, + }, + }, + } + + str, err := config.GenerateSessionConfigTemplate() + require.Nil(t, err) + require.Contains(t, str, "\"dynamicLabels\"") + require.Contains(t, str, "\"field\":\"user\"") + require.Contains(t, str, "\"labelKey\":\"session.posit.team/user\"") + }) + + t.Run("pattern extraction rule serializes correctly", func(t *testing.T) { + config := SessionConfig{ + Pod: &PodConfig{ + DynamicLabels: []DynamicLabelRule{ + { + Field: "args", + Match: "--ext-[a-z]+", + TrimPrefix: "--ext-", + LabelPrefix: "session.posit.team/ext.", + LabelValue: "enabled", + }, + }, + }, + } + + str, err := config.GenerateSessionConfigTemplate() + require.Nil(t, err) + require.Contains(t, str, "\"dynamicLabels\"") + require.Contains(t, str, "\"field\":\"args\"") + require.Contains(t, str, "\"match\":\"--ext-[a-z]+\"") + require.Contains(t, str, "\"trimPrefix\":\"--ext-\"") + require.Contains(t, str, "\"labelPrefix\":\"session.posit.team/ext.\"") + require.Contains(t, str, "\"labelValue\":\"enabled\"") + }) +} + func TestSiteSessionVaultName(t *testing.T) { t.Skip("Need to create a TestProduct struct to test this behavior") } diff --git a/api/product/zz_generated.deepcopy.go b/api/product/zz_generated.deepcopy.go index 04e9a010..1c0c2de4 100644 --- a/api/product/zz_generated.deepcopy.go +++ b/api/product/zz_generated.deepcopy.go @@ -11,6 +11,21 @@ import ( "k8s.io/api/core/v1" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DynamicLabelRule) DeepCopyInto(out *DynamicLabelRule) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynamicLabelRule. +func (in *DynamicLabelRule) DeepCopy() *DynamicLabelRule { + if in == nil { + return nil + } + out := new(DynamicLabelRule) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JobConfig) DeepCopyInto(out *JobConfig) { *out = *in @@ -57,6 +72,11 @@ func (in *PodConfig) DeepCopyInto(out *PodConfig) { (*out)[key] = val } } + if in.DynamicLabels != nil { + in, out := &in.DynamicLabels, &out.DynamicLabels + *out = make([]DynamicLabelRule, len(*in)) + copy(*out, *in) + } if in.Volumes != nil { in, out := &in.Volumes, &out.Volumes *out = make([]v1.Volume, len(*in)) diff --git a/api/templates/2.5.0/job.tpl b/api/templates/2.5.0/job.tpl index 865c2980..d849cf14 100644 --- a/api/templates/2.5.0/job.tpl +++ b/api/templates/2.5.0/job.tpl @@ -78,6 +78,21 @@ spec: {{ $key }}: {{ toYaml $val | indent 8 | trimPrefix (repeat 8 " ") }} {{- end }} {{- end }} + {{- with $templateData.pod.dynamicLabels }} + {{- range $rule := . }} + {{- if hasKey $.Job $rule.field }} + {{- if $rule.labelKey }} + {{ $rule.labelKey }}: {{ index $.Job $rule.field | toString | quote }} + {{- else if $rule.match }} + {{- $str := index $.Job $rule.field | join " " }} + {{- $matches := regexFindAll $rule.match $str -1 }} + {{- range $match := $matches }} + {{ $rule.labelPrefix }}{{ trimPrefix ($rule.trimPrefix | default "") $match | lower | replace " " "_" | replace "-" "_" }}: {{ $rule.labelValue | default "true" | quote }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} generateName: {{ toYaml .Job.generateName }} spec: {{- if .Job.host }} diff --git a/config/crd/bases/core.posit.team_connects.yaml b/config/crd/bases/core.posit.team_connects.yaml index 94495a6e..ee0b7b46 100644 --- a/config/crd/bases/core.posit.team_connects.yaml +++ b/config/crd/bases/core.posit.team_connects.yaml @@ -1857,6 +1857,45 @@ spec: type: string type: object type: object + dynamicLabels: + items: + description: |- + 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). + properties: + field: + description: Field is the name of a top-level .Job field + to read (e.g., "user", "args"). + type: string + labelKey: + description: |- + LabelKey is the label key for direct single-value mapping. + Mutually exclusive with match/labelPrefix. + type: string + labelPrefix: + description: |- + LabelPrefix is prepended to the cleaned match to form the label key. + Required when match is set. + type: string + labelValue: + description: LabelValue is the static value for all + matched labels. Defaults to "true". + type: string + match: + description: |- + 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. + type: string + trimPrefix: + description: TrimPrefix is stripped from each regex + match before forming the label key suffix. + type: string + required: + - field + type: object + type: array env: items: description: EnvVar represents an environment variable present diff --git a/config/crd/bases/core.posit.team_workbenches.yaml b/config/crd/bases/core.posit.team_workbenches.yaml index d047931a..f41c92db 100644 --- a/config/crd/bases/core.posit.team_workbenches.yaml +++ b/config/crd/bases/core.posit.team_workbenches.yaml @@ -2088,6 +2088,45 @@ spec: type: string type: object type: object + dynamicLabels: + items: + description: |- + 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). + properties: + field: + description: Field is the name of a top-level .Job field + to read (e.g., "user", "args"). + type: string + labelKey: + description: |- + LabelKey is the label key for direct single-value mapping. + Mutually exclusive with match/labelPrefix. + type: string + labelPrefix: + description: |- + LabelPrefix is prepended to the cleaned match to form the label key. + Required when match is set. + type: string + labelValue: + description: LabelValue is the static value for all + matched labels. Defaults to "true". + type: string + match: + description: |- + 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. + type: string + trimPrefix: + description: TrimPrefix is stripped from each regex + match before forming the label key suffix. + type: string + required: + - field + type: object + type: array env: items: description: EnvVar represents an environment variable present From 7c9de7cb34ae6ce98e787170858d4d53ae6fdb39 Mon Sep 17 00:00:00 2001 From: ian-flores Date: Wed, 4 Mar 2026 14:10:31 -0800 Subject: [PATCH 2/4] Address review findings (job 704) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Add `ValidateDynamicLabelRules()` function that validates regex compilation (ReDoS prevention via Go's RE2 engine) and enforces mutual exclusivity between `labelKey` and `match`/`labelPrefix` - Call validation from `GenerateSessionConfigTemplate()` to reject invalid rules before template rendering - Fix template to handle scalar fields in the `match` branch using `kindIs "slice"` type check instead of unconditional `join` - Add `trunc 63` to label key suffix in template to enforce Kubernetes label value length limits - Remove lossy `replace "-" "_"` transformation — hyphens are valid in label keys - Add kubebuilder `MaxLength`/`MinLength` validation markers to `DynamicLabelRule` fields - Add validation unit tests covering mutual exclusivity, missing fields, invalid regex, and generation-time rejection --- .../affected-repos.txt | 1 + .../edited-files.log | 6 ++ api/product/session_config.go | 34 ++++++++ api/product/session_config_test.go | 79 +++++++++++++++++++ api/templates/2.5.0/job.tpl | 7 +- 5 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 .claude/tsc-cache/f778da44-ae5e-4fd3-bf14-bd8daaa50ef1/affected-repos.txt create mode 100644 .claude/tsc-cache/f778da44-ae5e-4fd3-bf14-bd8daaa50ef1/edited-files.log diff --git a/.claude/tsc-cache/f778da44-ae5e-4fd3-bf14-bd8daaa50ef1/affected-repos.txt b/.claude/tsc-cache/f778da44-ae5e-4fd3-bf14-bd8daaa50ef1/affected-repos.txt new file mode 100644 index 00000000..eedd89b4 --- /dev/null +++ b/.claude/tsc-cache/f778da44-ae5e-4fd3-bf14-bd8daaa50ef1/affected-repos.txt @@ -0,0 +1 @@ +api diff --git a/.claude/tsc-cache/f778da44-ae5e-4fd3-bf14-bd8daaa50ef1/edited-files.log b/.claude/tsc-cache/f778da44-ae5e-4fd3-bf14-bd8daaa50ef1/edited-files.log new file mode 100644 index 00000000..d72ed9ce --- /dev/null +++ b/.claude/tsc-cache/f778da44-ae5e-4fd3-bf14-bd8daaa50ef1/edited-files.log @@ -0,0 +1,6 @@ +1772662497:/private/var/folders/n9/gx_3rrzs6kbbx833881fxrkm0000gn/T/roborev-refine-668328644/api/templates/2.5.0/job.tpl:api +1772662512:/private/var/folders/n9/gx_3rrzs6kbbx833881fxrkm0000gn/T/roborev-refine-668328644/api/product/session_config.go:api +1772662519:/private/var/folders/n9/gx_3rrzs6kbbx833881fxrkm0000gn/T/roborev-refine-668328644/api/product/session_config.go:api +1772662525:/private/var/folders/n9/gx_3rrzs6kbbx833881fxrkm0000gn/T/roborev-refine-668328644/api/product/session_config.go:api +1772662544:/private/var/folders/n9/gx_3rrzs6kbbx833881fxrkm0000gn/T/roborev-refine-668328644/api/product/session_config.go:api +1772662562:/private/var/folders/n9/gx_3rrzs6kbbx833881fxrkm0000gn/T/roborev-refine-668328644/api/product/session_config_test.go:api diff --git a/api/product/session_config.go b/api/product/session_config.go index d9a4b7dc..94238867 100644 --- a/api/product/session_config.go +++ b/api/product/session_config.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/url" + "regexp" "strings" "github.com/posit-dev/team-operator/api/templates" @@ -67,29 +68,62 @@ type JobConfig struct { // +kubebuilder:object:generate=true type DynamicLabelRule struct { // Field is the name of a top-level .Job field to read (e.g., "user", "args"). + // +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 +// 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", diff --git a/api/product/session_config_test.go b/api/product/session_config_test.go index fe4cd792..825b39bd 100644 --- a/api/product/session_config_test.go +++ b/api/product/session_config_test.go @@ -109,6 +109,85 @@ func TestSessionConfig_DynamicLabels(t *testing.T) { }) } +func TestValidateDynamicLabelRules(t *testing.T) { + t.Run("valid direct mapping", func(t *testing.T) { + err := ValidateDynamicLabelRules([]DynamicLabelRule{ + {Field: "user", LabelKey: "session.posit.team/user"}, + }) + require.Nil(t, err) + }) + + t.Run("valid regex mapping", func(t *testing.T) { + err := ValidateDynamicLabelRules([]DynamicLabelRule{ + {Field: "args", Match: "--ext-[a-z]+", LabelPrefix: "session.posit.team/ext."}, + }) + require.Nil(t, err) + }) + + t.Run("rejects labelKey and match both set", func(t *testing.T) { + err := ValidateDynamicLabelRules([]DynamicLabelRule{ + {Field: "user", LabelKey: "foo", Match: "bar", LabelPrefix: "baz"}, + }) + require.ErrorContains(t, err, "mutually exclusive") + }) + + t.Run("rejects neither labelKey nor match", func(t *testing.T) { + err := ValidateDynamicLabelRules([]DynamicLabelRule{ + {Field: "user"}, + }) + require.ErrorContains(t, err, "one of labelKey or match is required") + }) + + t.Run("rejects match without labelPrefix", func(t *testing.T) { + err := ValidateDynamicLabelRules([]DynamicLabelRule{ + {Field: "args", Match: "--ext-[a-z]+"}, + }) + require.ErrorContains(t, err, "labelPrefix is required") + }) + + t.Run("rejects invalid regex", func(t *testing.T) { + err := ValidateDynamicLabelRules([]DynamicLabelRule{ + {Field: "args", Match: "(unclosed", LabelPrefix: "prefix."}, + }) + require.ErrorContains(t, err, "invalid regex") + }) + + t.Run("rejects catastrophic backtracking regex", func(t *testing.T) { + // Go's regexp package uses RE2 which doesn't support backreferences, + // so patterns like (a+)+$ are safe. But invalid patterns still fail. + err := ValidateDynamicLabelRules([]DynamicLabelRule{ + {Field: "args", Match: "[a-z]+", LabelPrefix: "prefix."}, + }) + require.Nil(t, err) + }) +} + +func TestGenerateSessionConfigTemplate_DynamicLabels_Validation(t *testing.T) { + t.Run("rejects invalid regex at generation time", func(t *testing.T) { + config := SessionConfig{ + Pod: &PodConfig{ + DynamicLabels: []DynamicLabelRule{ + {Field: "args", Match: "(unclosed", LabelPrefix: "prefix."}, + }, + }, + } + _, err := config.GenerateSessionConfigTemplate() + require.ErrorContains(t, err, "invalid regex") + }) + + t.Run("rejects mutually exclusive fields at generation time", func(t *testing.T) { + config := SessionConfig{ + Pod: &PodConfig{ + DynamicLabels: []DynamicLabelRule{ + {Field: "user", LabelKey: "foo", Match: "bar", LabelPrefix: "baz"}, + }, + }, + } + _, err := config.GenerateSessionConfigTemplate() + require.ErrorContains(t, err, "mutually exclusive") + }) +} + func TestSiteSessionVaultName(t *testing.T) { t.Skip("Need to create a TestProduct struct to test this behavior") } diff --git a/api/templates/2.5.0/job.tpl b/api/templates/2.5.0/job.tpl index d849cf14..0726c456 100644 --- a/api/templates/2.5.0/job.tpl +++ b/api/templates/2.5.0/job.tpl @@ -81,13 +81,14 @@ spec: {{- with $templateData.pod.dynamicLabels }} {{- range $rule := . }} {{- if hasKey $.Job $rule.field }} + {{- $val := index $.Job $rule.field }} {{- if $rule.labelKey }} - {{ $rule.labelKey }}: {{ index $.Job $rule.field | toString | quote }} + {{ $rule.labelKey }}: {{ $val | toString | quote }} {{- else if $rule.match }} - {{- $str := index $.Job $rule.field | join " " }} + {{- $str := (kindIs "slice" $val) | ternary ($val | join " ") ($val | toString) }} {{- $matches := regexFindAll $rule.match $str -1 }} {{- range $match := $matches }} - {{ $rule.labelPrefix }}{{ trimPrefix ($rule.trimPrefix | default "") $match | lower | replace " " "_" | replace "-" "_" }}: {{ $rule.labelValue | default "true" | quote }} + {{ trimPrefix ($rule.trimPrefix | default "") $match | lower | replace " " "_" | trunc 63 | printf "%s%s" $rule.labelPrefix }}: {{ $rule.labelValue | default "true" | quote }} {{- end }} {{- end }} {{- end }} From c70f317abcbfce282d614196600953ee536be4a0 Mon Sep 17 00:00:00 2001 From: ian-flores Date: Wed, 4 Mar 2026 14:47:11 -0800 Subject: [PATCH 3/4] fix: add doc comments and regenerate CRDs with validation markers --- api/product/session_config.go | 4 ++++ config/crd/bases/core.posit.team_connects.yaml | 14 ++++++++++++-- config/crd/bases/core.posit.team_workbenches.yaml | 14 ++++++++++++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/api/product/session_config.go b/api/product/session_config.go index 94238867..3caf4e76 100644 --- a/api/product/session_config.go +++ b/api/product/session_config.go @@ -35,6 +35,8 @@ type ServiceConfig struct { type PodConfig struct { 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"` @@ -68,6 +70,8 @@ type JobConfig struct { // +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. diff --git a/config/crd/bases/core.posit.team_connects.yaml b/config/crd/bases/core.posit.team_connects.yaml index ee0b7b46..3517ddf2 100644 --- a/config/crd/bases/core.posit.team_connects.yaml +++ b/config/crd/bases/core.posit.team_connects.yaml @@ -1858,6 +1858,9 @@ spec: type: object type: object dynamicLabels: + description: |- + DynamicLabels defines rules for generating pod labels from runtime session data. + Requires template version 2.5.0 or later; ignored by older templates. items: description: |- DynamicLabelRule defines a rule for generating pod labels from runtime session data. @@ -1865,28 +1868,35 @@ spec: to a label (using labelKey) or extracts multiple labels via regex (using match). properties: field: - description: Field is the name of a top-level .Job field - to read (e.g., "user", "args"). + description: |- + 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. + minLength: 1 type: string labelKey: description: |- LabelKey is the label key for direct single-value mapping. Mutually exclusive with match/labelPrefix. + maxLength: 63 type: string labelPrefix: description: |- LabelPrefix is prepended to the cleaned match to form the label key. Required when match is set. + maxLength: 253 type: string labelValue: description: LabelValue is the static value for all matched labels. Defaults to "true". + maxLength: 63 type: string match: description: |- 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. + maxLength: 256 type: string trimPrefix: description: TrimPrefix is stripped from each regex diff --git a/config/crd/bases/core.posit.team_workbenches.yaml b/config/crd/bases/core.posit.team_workbenches.yaml index f41c92db..2ae5bb2f 100644 --- a/config/crd/bases/core.posit.team_workbenches.yaml +++ b/config/crd/bases/core.posit.team_workbenches.yaml @@ -2089,6 +2089,9 @@ spec: type: object type: object dynamicLabels: + description: |- + DynamicLabels defines rules for generating pod labels from runtime session data. + Requires template version 2.5.0 or later; ignored by older templates. items: description: |- DynamicLabelRule defines a rule for generating pod labels from runtime session data. @@ -2096,28 +2099,35 @@ spec: to a label (using labelKey) or extracts multiple labels via regex (using match). properties: field: - description: Field is the name of a top-level .Job field - to read (e.g., "user", "args"). + description: |- + 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. + minLength: 1 type: string labelKey: description: |- LabelKey is the label key for direct single-value mapping. Mutually exclusive with match/labelPrefix. + maxLength: 63 type: string labelPrefix: description: |- LabelPrefix is prepended to the cleaned match to form the label key. Required when match is set. + maxLength: 253 type: string labelValue: description: LabelValue is the static value for all matched labels. Defaults to "true". + maxLength: 63 type: string match: description: |- 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. + maxLength: 256 type: string trimPrefix: description: TrimPrefix is stripped from each regex From 588b8e6379f99068f3cc2c4eac06b1ce17d47660 Mon Sep 17 00:00:00 2001 From: Ian Flores Siaca Date: Thu, 5 Mar 2026 11:06:01 -0800 Subject: [PATCH 4/4] chore: sync Helm chart with CRDs and remove tsc-cache files - Merge main and regenerate Helm chart CRDs - Remove accidentally committed .claude/tsc-cache files --- .../affected-repos.txt | 1 - .../edited-files.log | 6 --- api/product/session_config.go | 4 +- .../crd/core.posit.team_connects.yaml | 49 +++++++++++++++++++ .../crd/core.posit.team_workbenches.yaml | 49 +++++++++++++++++++ 5 files changed, 100 insertions(+), 9 deletions(-) delete mode 100644 .claude/tsc-cache/f778da44-ae5e-4fd3-bf14-bd8daaa50ef1/affected-repos.txt delete mode 100644 .claude/tsc-cache/f778da44-ae5e-4fd3-bf14-bd8daaa50ef1/edited-files.log diff --git a/.claude/tsc-cache/f778da44-ae5e-4fd3-bf14-bd8daaa50ef1/affected-repos.txt b/.claude/tsc-cache/f778da44-ae5e-4fd3-bf14-bd8daaa50ef1/affected-repos.txt deleted file mode 100644 index eedd89b4..00000000 --- a/.claude/tsc-cache/f778da44-ae5e-4fd3-bf14-bd8daaa50ef1/affected-repos.txt +++ /dev/null @@ -1 +0,0 @@ -api diff --git a/.claude/tsc-cache/f778da44-ae5e-4fd3-bf14-bd8daaa50ef1/edited-files.log b/.claude/tsc-cache/f778da44-ae5e-4fd3-bf14-bd8daaa50ef1/edited-files.log deleted file mode 100644 index d72ed9ce..00000000 --- a/.claude/tsc-cache/f778da44-ae5e-4fd3-bf14-bd8daaa50ef1/edited-files.log +++ /dev/null @@ -1,6 +0,0 @@ -1772662497:/private/var/folders/n9/gx_3rrzs6kbbx833881fxrkm0000gn/T/roborev-refine-668328644/api/templates/2.5.0/job.tpl:api -1772662512:/private/var/folders/n9/gx_3rrzs6kbbx833881fxrkm0000gn/T/roborev-refine-668328644/api/product/session_config.go:api -1772662519:/private/var/folders/n9/gx_3rrzs6kbbx833881fxrkm0000gn/T/roborev-refine-668328644/api/product/session_config.go:api -1772662525:/private/var/folders/n9/gx_3rrzs6kbbx833881fxrkm0000gn/T/roborev-refine-668328644/api/product/session_config.go:api -1772662544:/private/var/folders/n9/gx_3rrzs6kbbx833881fxrkm0000gn/T/roborev-refine-668328644/api/product/session_config.go:api -1772662562:/private/var/folders/n9/gx_3rrzs6kbbx833881fxrkm0000gn/T/roborev-refine-668328644/api/product/session_config_test.go:api diff --git a/api/product/session_config.go b/api/product/session_config.go index 3caf4e76..febbcb14 100644 --- a/api/product/session_config.go +++ b/api/product/session_config.go @@ -33,8 +33,8 @@ 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"` diff --git a/dist/chart/templates/crd/core.posit.team_connects.yaml b/dist/chart/templates/crd/core.posit.team_connects.yaml index 1a5664de..73ac8982 100755 --- a/dist/chart/templates/crd/core.posit.team_connects.yaml +++ b/dist/chart/templates/crd/core.posit.team_connects.yaml @@ -1878,6 +1878,55 @@ spec: type: string type: object type: object + dynamicLabels: + description: |- + DynamicLabels defines rules for generating pod labels from runtime session data. + Requires template version 2.5.0 or later; ignored by older templates. + items: + description: |- + 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). + properties: + field: + description: |- + 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. + minLength: 1 + type: string + labelKey: + description: |- + LabelKey is the label key for direct single-value mapping. + Mutually exclusive with match/labelPrefix. + maxLength: 63 + type: string + labelPrefix: + description: |- + LabelPrefix is prepended to the cleaned match to form the label key. + Required when match is set. + maxLength: 253 + type: string + labelValue: + description: LabelValue is the static value for all + matched labels. Defaults to "true". + maxLength: 63 + type: string + match: + description: |- + 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. + maxLength: 256 + type: string + trimPrefix: + description: TrimPrefix is stripped from each regex + match before forming the label key suffix. + type: string + required: + - field + type: object + type: array env: items: description: EnvVar represents an environment variable present diff --git a/dist/chart/templates/crd/core.posit.team_workbenches.yaml b/dist/chart/templates/crd/core.posit.team_workbenches.yaml index ff0ed92c..76c8536a 100755 --- a/dist/chart/templates/crd/core.posit.team_workbenches.yaml +++ b/dist/chart/templates/crd/core.posit.team_workbenches.yaml @@ -2109,6 +2109,55 @@ spec: type: string type: object type: object + dynamicLabels: + description: |- + DynamicLabels defines rules for generating pod labels from runtime session data. + Requires template version 2.5.0 or later; ignored by older templates. + items: + description: |- + 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). + properties: + field: + description: |- + 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. + minLength: 1 + type: string + labelKey: + description: |- + LabelKey is the label key for direct single-value mapping. + Mutually exclusive with match/labelPrefix. + maxLength: 63 + type: string + labelPrefix: + description: |- + LabelPrefix is prepended to the cleaned match to form the label key. + Required when match is set. + maxLength: 253 + type: string + labelValue: + description: LabelValue is the static value for all + matched labels. Defaults to "true". + maxLength: 63 + type: string + match: + description: |- + 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. + maxLength: 256 + type: string + trimPrefix: + description: TrimPrefix is stripped from each regex + match before forming the label key suffix. + type: string + required: + - field + type: object + type: array env: items: description: EnvVar represents an environment variable present