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/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" 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: