From 1514fb4bc5bfd07c00aea622ec105e16c7771f93 Mon Sep 17 00:00:00 2001 From: Anna Williamson Date: Mon, 20 Jul 2026 12:00:53 -0700 Subject: [PATCH 1/2] feat: allow Connect content pods to run on tainted session nodes Extends the existing Workbench session toleration/placement mechanism to Connect content (off-host execution) pods via new contentTolerations and contentNodeSelector fields on the Connect site spec. These are applied only to content pods, not the Connect server, mirroring how Workbench session tolerations work. --- api/core/v1beta1/site_types.go | 6 +++ api/core/v1beta1/zz_generated.deepcopy.go | 14 ++++++ .../core/v1beta1/internalconnectspec.go | 26 +++++++++++ config/crd/bases/core.posit.team_sites.yaml | 46 +++++++++++++++++++ .../templates/crd/core.posit.team_sites.yaml | 46 +++++++++++++++++++ .../core/site_controller_connect.go | 2 + .../crdapply/bases/core.posit.team_sites.yaml | 46 +++++++++++++++++++ 7 files changed, 186 insertions(+) diff --git a/api/core/v1beta1/site_types.go b/api/core/v1beta1/site_types.go index 897de7c..9b7864e 100644 --- a/api/core/v1beta1/site_types.go +++ b/api/core/v1beta1/site_types.go @@ -284,6 +284,12 @@ type InternalConnectSpec struct { NodeSelector map[string]string `json:"nodeSelector,omitempty"` + // ContentTolerations are tolerations applied only to Connect content (session) pods, not the Connect server. + ContentTolerations []corev1.Toleration `json:"contentTolerations,omitempty"` + + // ContentNodeSelector is a node selector applied only to Connect content (session) pods, not the Connect server. + ContentNodeSelector map[string]string `json:"contentNodeSelector,omitempty"` + // Resources sets the server container's resource requests/limits. // If unset, product defaults are applied. Setting this replaces the defaults entirely. Resources *corev1.ResourceRequirements `json:"resources,omitempty"` diff --git a/api/core/v1beta1/zz_generated.deepcopy.go b/api/core/v1beta1/zz_generated.deepcopy.go index efe6fca..236007b 100644 --- a/api/core/v1beta1/zz_generated.deepcopy.go +++ b/api/core/v1beta1/zz_generated.deepcopy.go @@ -1227,6 +1227,20 @@ func (in *InternalConnectSpec) DeepCopyInto(out *InternalConnectSpec) { (*out)[key] = val } } + if in.ContentTolerations != nil { + in, out := &in.ContentTolerations, &out.ContentTolerations + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ContentNodeSelector != nil { + in, out := &in.ContentNodeSelector, &out.ContentNodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } if in.Resources != nil { in, out := &in.Resources, &out.Resources *out = new(v1.ResourceRequirements) diff --git a/client-go/applyconfiguration/core/v1beta1/internalconnectspec.go b/client-go/applyconfiguration/core/v1beta1/internalconnectspec.go index a24302e..757acae 100644 --- a/client-go/applyconfiguration/core/v1beta1/internalconnectspec.go +++ b/client-go/applyconfiguration/core/v1beta1/internalconnectspec.go @@ -18,6 +18,8 @@ type InternalConnectSpecApplyConfiguration struct { License *product.LicenseSpec `json:"license,omitempty"` Volume *product.VolumeSpec `json:"volume,omitempty"` NodeSelector map[string]string `json:"nodeSelector,omitempty"` + ContentTolerations []v1.Toleration `json:"contentTolerations,omitempty"` + ContentNodeSelector map[string]string `json:"contentNodeSelector,omitempty"` Resources *v1.ResourceRequirements `json:"resources,omitempty"` Auth *AuthSpecApplyConfiguration `json:"auth,omitempty"` RegisterOnFirstLogin *bool `json:"registerOnFirstLogin,omitempty"` @@ -92,6 +94,30 @@ func (b *InternalConnectSpecApplyConfiguration) WithNodeSelector(entries map[str return b } +// WithContentTolerations adds the given value to the ContentTolerations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the ContentTolerations field. +func (b *InternalConnectSpecApplyConfiguration) WithContentTolerations(values ...v1.Toleration) *InternalConnectSpecApplyConfiguration { + for i := range values { + b.ContentTolerations = append(b.ContentTolerations, values[i]) + } + return b +} + +// WithContentNodeSelector puts the entries into the ContentNodeSelector field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the ContentNodeSelector field, +// overwriting an existing map entries in ContentNodeSelector field with the same key. +func (b *InternalConnectSpecApplyConfiguration) WithContentNodeSelector(entries map[string]string) *InternalConnectSpecApplyConfiguration { + if b.ContentNodeSelector == nil && len(entries) > 0 { + b.ContentNodeSelector = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ContentNodeSelector[k] = v + } + return b +} + // WithResources sets the Resources field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Resources field is set to the value of the last call. diff --git a/config/crd/bases/core.posit.team_sites.yaml b/config/crd/bases/core.posit.team_sites.yaml index f1e124d..6b46d55 100644 --- a/config/crd/bases/core.posit.team_sites.yaml +++ b/config/crd/bases/core.posit.team_sites.yaml @@ -198,6 +198,52 @@ spec: BaseDomain overrides site.Spec.Domain for this product's URL construction. When set, the product URL will be: domainPrefix.baseDomain type: string + contentNodeSelector: + additionalProperties: + type: string + description: ContentNodeSelector is a node selector applied only + to Connect content (session) pods, not the Connect server. + type: object + contentTolerations: + description: ContentTolerations are tolerations applied only to + Connect content (session) pods, not the Connect server. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array databaseSettings: properties: instrumentationSchema: diff --git a/dist/chart/templates/crd/core.posit.team_sites.yaml b/dist/chart/templates/crd/core.posit.team_sites.yaml index a1201d7..8879ed3 100755 --- a/dist/chart/templates/crd/core.posit.team_sites.yaml +++ b/dist/chart/templates/crd/core.posit.team_sites.yaml @@ -219,6 +219,52 @@ spec: BaseDomain overrides site.Spec.Domain for this product's URL construction. When set, the product URL will be: domainPrefix.baseDomain type: string + contentNodeSelector: + additionalProperties: + type: string + description: ContentNodeSelector is a node selector applied only + to Connect content (session) pods, not the Connect server. + type: object + contentTolerations: + description: ContentTolerations are tolerations applied only to + Connect content (session) pods, not the Connect server. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array databaseSettings: properties: instrumentationSchema: diff --git a/internal/controller/core/site_controller_connect.go b/internal/controller/core/site_controller_connect.go index 22786c7..68c67ce 100644 --- a/internal/controller/core/site_controller_connect.go +++ b/internal/controller/core/site_controller_connect.go @@ -63,6 +63,8 @@ func (r *SiteReconciler) reconcileConnect( ImagePullPolicy: site.Spec.Connect.ImagePullPolicy, ImagePullSecrets: product.MakePullSecrets(site.Spec.ImagePullSecrets), ServiceAccountName: fmt.Sprintf("%s-connect-session", req.Name), + Tolerations: site.Spec.Connect.ContentTolerations, + NodeSelector: site.Spec.Connect.ContentNodeSelector, }, }, Config: v1beta1.ConnectConfig{ diff --git a/internal/crdapply/bases/core.posit.team_sites.yaml b/internal/crdapply/bases/core.posit.team_sites.yaml index f1e124d..6b46d55 100644 --- a/internal/crdapply/bases/core.posit.team_sites.yaml +++ b/internal/crdapply/bases/core.posit.team_sites.yaml @@ -198,6 +198,52 @@ spec: BaseDomain overrides site.Spec.Domain for this product's URL construction. When set, the product URL will be: domainPrefix.baseDomain type: string + contentNodeSelector: + additionalProperties: + type: string + description: ContentNodeSelector is a node selector applied only + to Connect content (session) pods, not the Connect server. + type: object + contentTolerations: + description: ContentTolerations are tolerations applied only to + Connect content (session) pods, not the Connect server. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array databaseSettings: properties: instrumentationSchema: From 4022345b3d521e4133ff095293a77f32c626dc42 Mon Sep 17 00:00:00 2001 From: Anna Williamson Date: Mon, 20 Jul 2026 15:13:25 -0700 Subject: [PATCH 2/2] test: cover Connect content tolerations and node selector --- internal/controller/core/site_test.go | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/internal/controller/core/site_test.go b/internal/controller/core/site_test.go index 85fbf3e..3130f32 100644 --- a/internal/controller/core/site_test.go +++ b/internal/controller/core/site_test.go @@ -486,6 +486,37 @@ func TestSiteReconcileWithTolerations(t *testing.T) { assert.Equal(t, "workbench-session", workbench.Spec.SessionConfig.Pod.Tolerations[0].Key) } +func TestSiteReconcileWithContentPlacement(t *testing.T) { + siteName := "content-placement-site" + siteNamespace := "posit-team" + site := defaultSite(siteName) + // ContentTolerations and ContentNodeSelector apply to Connect content (session) pods + site.Spec.Connect.ContentTolerations = []corev1.Toleration{ + { + Key: "workload-type", + Operator: corev1.TolerationOpEqual, + Value: "session", + Effect: corev1.TaintEffectNoSchedule, + }, + } + site.Spec.Connect.ContentNodeSelector = map[string]string{ + "karpenter.sh/nodepool": "test-pool", + } + + cli, _, err := runFakeSiteReconciler(t, siteNamespace, siteName, site) + assert.Nil(t, err) + + // Verify content (session) pods receive the ContentTolerations and ContentNodeSelector + connect := getConnect(t, cli, siteNamespace, siteName) + assert.NotNil(t, connect.Spec.SessionConfig) + assert.NotNil(t, connect.Spec.SessionConfig.Pod) + assert.Len(t, connect.Spec.SessionConfig.Pod.Tolerations, 1) + assert.Equal(t, "workload-type", connect.Spec.SessionConfig.Pod.Tolerations[0].Key) + assert.Equal(t, "session", connect.Spec.SessionConfig.Pod.Tolerations[0].Value) + assert.Equal(t, corev1.TaintEffectNoSchedule, connect.Spec.SessionConfig.Pod.Tolerations[0].Effect) + assert.Equal(t, "test-pool", connect.Spec.SessionConfig.Pod.NodeSelector["karpenter.sh/nodepool"]) +} + func TestSiteReconcileWithSharedDirectory(t *testing.T) { siteName := "shared-site" siteNamespace := "posit-team"