diff --git a/api/core/v1beta1/connect_types.go b/api/core/v1beta1/connect_types.go index dafca154..cfbcb6b8 100644 --- a/api/core/v1beta1/connect_types.go +++ b/api/core/v1beta1/connect_types.go @@ -16,6 +16,48 @@ import ( v1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1" ) +// DefaultRuntimeImageRepo is the default container image repository for Connect runtime images +const DefaultRuntimeImageRepo = "ghcr.io/rstudio/content-pro" + +// ConnectRuntimeImageSpec defines a runtime image for Connect off-host execution +type ConnectRuntimeImageSpec struct { + // RVersion is the R version (e.g., "4.5.2") + // +kubebuilder:validation:MinLength=1 + RVersion string `json:"rVersion"` + + // PyVersion is the Python version (e.g., "3.13.9") + // +kubebuilder:validation:MinLength=1 + PyVersion string `json:"pyVersion"` + + // OSVersion is the OS version (e.g., "ubuntu2204") + // +kubebuilder:validation:MinLength=1 + OSVersion string `json:"osVersion"` + + // QuartoVersion is the Quarto version (e.g., "1.8.25") + // +kubebuilder:validation:MinLength=1 + QuartoVersion string `json:"quartoVersion"` + + // Repo is the container image repository (e.g., "ghcr.io/rstudio/content-pro") + // +kubebuilder:default="ghcr.io/rstudio/content-pro" + // +optional + Repo string `json:"repo,omitempty"` +} + +// ToProductDefinition converts a CRD spec to the internal product type +func (s *ConnectRuntimeImageSpec) ToProductDefinition() product.ConnectRuntimeImageDefinition { + repo := s.Repo + if repo == "" { + repo = DefaultRuntimeImageRepo + } + return product.ConnectRuntimeImageDefinition{ + PyVersion: s.PyVersion, + RVersion: s.RVersion, + OSVersion: s.OSVersion, + QuartoVersion: s.QuartoVersion, + Repo: repo, + } +} + // ConnectSpec defines the desired state of Connect type ConnectSpec struct { License product.LicenseSpec `json:"license,omitempty"` @@ -46,6 +88,11 @@ type ConnectSpec struct { OffHostExecution bool `json:"offHostExecution,omitempty"` + // AdditionalRuntimeImages specifies additional runtime images to append to the defaults + // for Connect off-host execution. These are added after the built-in default images. + // +optional + AdditionalRuntimeImages []ConnectRuntimeImageSpec `json:"additionalRuntimeImages,omitempty"` + Image string `json:"image,omitempty"` ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` @@ -230,19 +277,26 @@ func (c *Connect) DefaultRuntimeYAML() (string, error) { } for _, img := range []product.ConnectRuntimeImageDefinition{ + { + PyVersion: "3.13.9", + RVersion: "4.5.2", + OSVersion: "ubuntu2204", + QuartoVersion: "1.8.25", + Repo: DefaultRuntimeImageRepo, + }, { PyVersion: "3.12.4", RVersion: "4.4.1", OSVersion: "ubuntu2204", QuartoVersion: "1.4.557", - Repo: "ghcr.io/rstudio/content-pro", + Repo: DefaultRuntimeImageRepo, }, { PyVersion: "3.11.3", RVersion: "4.2.2", OSVersion: "ubuntu2204", QuartoVersion: "1.3.340", - Repo: "ghcr.io/rstudio/content-pro", + Repo: DefaultRuntimeImageRepo, }, } { @@ -254,6 +308,16 @@ func (c *Connect) DefaultRuntimeYAML() (string, error) { def.Images = append(def.Images, imgEntry) } + // Append any additional runtime images from the spec + for _, additionalImg := range c.Spec.AdditionalRuntimeImages { + imgDef := additionalImg.ToProductDefinition() + imgEntry, err := imgDef.GenerateImageEntry() + if err != nil { + return "", err + } + def.Images = append(def.Images, imgEntry) + } + return def.BuildDefaultRuntimeYAML() } diff --git a/api/core/v1beta1/connect_types_test.go b/api/core/v1beta1/connect_types_test.go index bbd9777b..2149229f 100644 --- a/api/core/v1beta1/connect_types_test.go +++ b/api/core/v1beta1/connect_types_test.go @@ -300,13 +300,109 @@ func TestConnect_DefaultRuntimeYAML(t *testing.T) { r.NoError(yaml.Unmarshal([]byte(runtimeYaml), parsedRuntimeYaml)) - r.Len(parsedRuntimeYaml.Images, 2) + r.Len(parsedRuntimeYaml.Images, 3) repo, _, ok := strings.Cut(parsedRuntimeYaml.Images[0].Name, ":") r.True(ok) r.Equal(repo, "ghcr.io/rstudio/content-pro") } +func TestConnect_DefaultRuntimeYAML_WithAdditionalImages(t *testing.T) { + r := require.New(t) + + con := &Connect{ + ObjectMeta: v1.ObjectMeta{ + Name: "additional-images", + Namespace: "posit-team", + }, + Spec: ConnectSpec{ + AdditionalRuntimeImages: []ConnectRuntimeImageSpec{ + { + RVersion: "4.6.0", + PyVersion: "3.14.0", + OSVersion: "ubuntu2204", + QuartoVersion: "1.9.0", + Repo: "ghcr.io/rstudio/content-pro", + }, + }, + }, + } + + runtimeYaml, err := con.DefaultRuntimeYAML() + r.NoError(err) + r.NotEqual(runtimeYaml, "") + + parsedRuntimeYaml := &product.ConnectRuntimeDefinition{} + r.NoError(yaml.Unmarshal([]byte(runtimeYaml), parsedRuntimeYaml)) + + // 3 defaults + 1 additional + r.Len(parsedRuntimeYaml.Images, 4) + + // Verify the additional image is last + lastImage := parsedRuntimeYaml.Images[3] + r.Contains(lastImage.Name, "r4.6.0-py3.14.0-ubuntu2204") + r.Equal("4.6.0", lastImage.R.Installations[0].Version) + r.Equal("3.14.0", lastImage.Python.Installations[0].Version) +} + +func TestConnect_DefaultRuntimeYAML_AdditionalImagesDefaultRepo(t *testing.T) { + r := require.New(t) + + con := &Connect{ + ObjectMeta: v1.ObjectMeta{ + Name: "default-repo", + Namespace: "posit-team", + }, + Spec: ConnectSpec{ + AdditionalRuntimeImages: []ConnectRuntimeImageSpec{ + { + RVersion: "4.6.0", + PyVersion: "3.14.0", + OSVersion: "ubuntu2204", + QuartoVersion: "1.9.0", + // No repo specified - should default + }, + }, + }, + } + + runtimeYaml, err := con.DefaultRuntimeYAML() + r.NoError(err) + + parsedRuntimeYaml := &product.ConnectRuntimeDefinition{} + r.NoError(yaml.Unmarshal([]byte(runtimeYaml), parsedRuntimeYaml)) + + r.Len(parsedRuntimeYaml.Images, 4) + + // Verify it uses default repo + lastImage := parsedRuntimeYaml.Images[3] + r.Contains(lastImage.Name, "ghcr.io/rstudio/content-pro:") +} + +func TestConnect_DefaultRuntimeYAML_AdditionalImageInvalid(t *testing.T) { + r := require.New(t) + + con := &Connect{ + ObjectMeta: v1.ObjectMeta{ + Name: "invalid-image", + Namespace: "posit-team", + }, + Spec: ConnectSpec{ + AdditionalRuntimeImages: []ConnectRuntimeImageSpec{ + { + RVersion: "4.6.0", + // Missing PyVersion - should cause GenerateImageEntry to fail + OSVersion: "ubuntu2204", + QuartoVersion: "1.9.0", + }, + }, + }, + } + + _, err := con.DefaultRuntimeYAML() + r.Error(err) +} + func TestConnect_CreateSessionVolumeFactory(t *testing.T) { con := &Connect{ ObjectMeta: v1.ObjectMeta{ diff --git a/api/core/v1beta1/site_types.go b/api/core/v1beta1/site_types.go index c48dd202..c14e1cda 100644 --- a/api/core/v1beta1/site_types.go +++ b/api/core/v1beta1/site_types.go @@ -266,6 +266,11 @@ type InternalConnectSpec struct { // +kubebuilder:default=2 // +kubebuilder:validation:Minimum=0 ScheduleConcurrency int `json:"scheduleConcurrency,omitempty"` + + // AdditionalRuntimeImages specifies additional runtime images to append to the defaults + // for Connect off-host execution + // +optional + AdditionalRuntimeImages []ConnectRuntimeImageSpec `json:"additionalRuntimeImages,omitempty"` } type DatabaseSettings struct { diff --git a/api/core/v1beta1/zz_generated.deepcopy.go b/api/core/v1beta1/zz_generated.deepcopy.go index 9471a100..b9831e4b 100644 --- a/api/core/v1beta1/zz_generated.deepcopy.go +++ b/api/core/v1beta1/zz_generated.deepcopy.go @@ -714,6 +714,21 @@ func (in *ConnectRConfig) DeepCopy() *ConnectRConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectRuntimeImageSpec) DeepCopyInto(out *ConnectRuntimeImageSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectRuntimeImageSpec. +func (in *ConnectRuntimeImageSpec) DeepCopy() *ConnectRuntimeImageSpec { + if in == nil { + return nil + } + out := new(ConnectRuntimeImageSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConnectSamlConfig) DeepCopyInto(out *ConnectSamlConfig) { *out = *in @@ -802,6 +817,11 @@ func (in *ConnectSpec) DeepCopyInto(out *ConnectSpec) { (*out)[key] = val } } + if in.AdditionalRuntimeImages != nil { + in, out := &in.AdditionalRuntimeImages, &out.AdditionalRuntimeImages + *out = make([]ConnectRuntimeImageSpec, len(*in)) + copy(*out, *in) + } if in.AdditionalVolumes != nil { in, out := &in.AdditionalVolumes, &out.AdditionalVolumes *out = make([]product.VolumeSpec, len(*in)) @@ -1112,6 +1132,11 @@ func (in *InternalConnectSpec) DeepCopyInto(out *InternalConnectSpec) { *out = new(DatabaseSettings) **out = **in } + if in.AdditionalRuntimeImages != nil { + in, out := &in.AdditionalRuntimeImages, &out.AdditionalRuntimeImages + *out = make([]ConnectRuntimeImageSpec, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalConnectSpec. diff --git a/client-go/applyconfiguration/core/v1beta1/connectspec.go b/client-go/applyconfiguration/core/v1beta1/connectspec.go index 1f80ea80..31cff3e5 100644 --- a/client-go/applyconfiguration/core/v1beta1/connectspec.go +++ b/client-go/applyconfiguration/core/v1beta1/connectspec.go @@ -13,36 +13,37 @@ import ( // ConnectSpecApplyConfiguration represents a declarative configuration of the ConnectSpec type for use // with apply. type ConnectSpecApplyConfiguration struct { - License *product.LicenseSpec `json:"license,omitempty"` - Config *ConnectConfigApplyConfiguration `json:"config,omitempty"` - SessionConfig *product.SessionConfig `json:"sessionConfig,omitempty"` - Volume *product.VolumeSpec `json:"volume,omitempty"` - SecretType *product.SiteSecretType `json:"secretType,omitempty"` - Auth *AuthSpecApplyConfiguration `json:"auth,omitempty"` - Url *string `json:"url,omitempty"` - DatabaseConfig *PostgresDatabaseConfigApplyConfiguration `json:"databaseConfig,omitempty"` - IngressClass *string `json:"ingressClass,omitempty"` - IngressAnnotations map[string]string `json:"ingressAnnotations,omitempty"` - ImagePullSecrets []string `json:"imagePullSecrets,omitempty"` - NodeSelector map[string]string `json:"nodeSelector,omitempty"` - AddEnv map[string]string `json:"addEnv,omitempty"` - OffHostExecution *bool `json:"offHostExecution,omitempty"` - Image *string `json:"image,omitempty"` - ImagePullPolicy *v1.PullPolicy `json:"imagePullPolicy,omitempty"` - Sleep *bool `json:"sleep,omitempty"` - SessionImage *string `json:"sessionImage,omitempty"` - AwsAccountId *string `json:"awsAccountId,omitempty"` - ClusterDate *string `json:"clusterDate,omitempty"` - WorkloadCompoundName *string `json:"workloadCompoundName,omitempty"` - ChronicleAgentImage *string `json:"chronicleImage,omitempty"` - AdditionalVolumes []product.VolumeSpec `json:"additionalVolumes,omitempty"` - Secret *SecretConfigApplyConfiguration `json:"secret,omitempty"` - WorkloadSecret *SecretConfigApplyConfiguration `json:"workloadSecret,omitempty"` - MainDatabaseCredentialSecret *SecretConfigApplyConfiguration `json:"mainDatabaseCredentialSecret,omitempty"` - Debug *bool `json:"debug,omitempty"` - Replicas *int `json:"replicas,omitempty"` - DsnSecret *string `json:"dsnSecret,omitempty"` - ChronicleSidecarProductApiKeyEnabled *bool `json:"chronicleSidecarProductApiKeyEnabled,omitempty"` + License *product.LicenseSpec `json:"license,omitempty"` + Config *ConnectConfigApplyConfiguration `json:"config,omitempty"` + SessionConfig *product.SessionConfig `json:"sessionConfig,omitempty"` + Volume *product.VolumeSpec `json:"volume,omitempty"` + SecretType *product.SiteSecretType `json:"secretType,omitempty"` + Auth *AuthSpecApplyConfiguration `json:"auth,omitempty"` + Url *string `json:"url,omitempty"` + DatabaseConfig *PostgresDatabaseConfigApplyConfiguration `json:"databaseConfig,omitempty"` + IngressClass *string `json:"ingressClass,omitempty"` + IngressAnnotations map[string]string `json:"ingressAnnotations,omitempty"` + ImagePullSecrets []string `json:"imagePullSecrets,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + AddEnv map[string]string `json:"addEnv,omitempty"` + OffHostExecution *bool `json:"offHostExecution,omitempty"` + AdditionalRuntimeImages []ConnectRuntimeImageSpecApplyConfiguration `json:"additionalRuntimeImages,omitempty"` + Image *string `json:"image,omitempty"` + ImagePullPolicy *v1.PullPolicy `json:"imagePullPolicy,omitempty"` + Sleep *bool `json:"sleep,omitempty"` + SessionImage *string `json:"sessionImage,omitempty"` + AwsAccountId *string `json:"awsAccountId,omitempty"` + ClusterDate *string `json:"clusterDate,omitempty"` + WorkloadCompoundName *string `json:"workloadCompoundName,omitempty"` + ChronicleAgentImage *string `json:"chronicleImage,omitempty"` + AdditionalVolumes []product.VolumeSpec `json:"additionalVolumes,omitempty"` + Secret *SecretConfigApplyConfiguration `json:"secret,omitempty"` + WorkloadSecret *SecretConfigApplyConfiguration `json:"workloadSecret,omitempty"` + MainDatabaseCredentialSecret *SecretConfigApplyConfiguration `json:"mainDatabaseCredentialSecret,omitempty"` + Debug *bool `json:"debug,omitempty"` + Replicas *int `json:"replicas,omitempty"` + DsnSecret *string `json:"dsnSecret,omitempty"` + ChronicleSidecarProductApiKeyEnabled *bool `json:"chronicleSidecarProductApiKeyEnabled,omitempty"` } // ConnectSpecApplyConfiguration constructs a declarative configuration of the ConnectSpec type for use with @@ -183,6 +184,19 @@ func (b *ConnectSpecApplyConfiguration) WithOffHostExecution(value bool) *Connec return b } +// WithAdditionalRuntimeImages adds the given value to the AdditionalRuntimeImages 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 AdditionalRuntimeImages field. +func (b *ConnectSpecApplyConfiguration) WithAdditionalRuntimeImages(values ...*ConnectRuntimeImageSpecApplyConfiguration) *ConnectSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithAdditionalRuntimeImages") + } + b.AdditionalRuntimeImages = append(b.AdditionalRuntimeImages, *values[i]) + } + return b +} + // WithImage sets the Image 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 Image field is set to the value of the last call. diff --git a/client-go/applyconfiguration/core/v1beta1/internalconnectspec.go b/client-go/applyconfiguration/core/v1beta1/internalconnectspec.go index 79b63ea0..3d88320c 100644 --- a/client-go/applyconfiguration/core/v1beta1/internalconnectspec.go +++ b/client-go/applyconfiguration/core/v1beta1/internalconnectspec.go @@ -13,24 +13,25 @@ import ( // InternalConnectSpecApplyConfiguration represents a declarative configuration of the InternalConnectSpec type for use // with apply. type InternalConnectSpecApplyConfiguration struct { - License *product.LicenseSpec `json:"license,omitempty"` - Volume *product.VolumeSpec `json:"volume,omitempty"` - NodeSelector map[string]string `json:"nodeSelector,omitempty"` - Auth *AuthSpecApplyConfiguration `json:"auth,omitempty"` - AddEnv map[string]string `json:"addEnv,omitempty"` - Image *string `json:"image,omitempty"` - SessionImage *string `json:"sessionImage,omitempty"` - ImagePullPolicy *v1.PullPolicy `json:"imagePullPolicy,omitempty"` - Databricks *DatabricksConfigApplyConfiguration `json:"databricks,omitempty"` - LoggedInWarning *string `json:"loggedInWarning,omitempty"` - PublicWarning *string `json:"publicWarning,omitempty"` - Replicas *int `json:"replicas,omitempty"` - ExperimentalFeatures *InternalConnectExperimentalFeaturesApplyConfiguration `json:"experimentalFeatures,omitempty"` - DomainPrefix *string `json:"domainPrefix,omitempty"` - BaseDomain *string `json:"baseDomain,omitempty"` - GPUSettings *GPUSettingsApplyConfiguration `json:"gpuSettings,omitempty"` - DatabaseSettings *DatabaseSettingsApplyConfiguration `json:"databaseSettings,omitempty"` - ScheduleConcurrency *int `json:"scheduleConcurrency,omitempty"` + License *product.LicenseSpec `json:"license,omitempty"` + Volume *product.VolumeSpec `json:"volume,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + Auth *AuthSpecApplyConfiguration `json:"auth,omitempty"` + AddEnv map[string]string `json:"addEnv,omitempty"` + Image *string `json:"image,omitempty"` + SessionImage *string `json:"sessionImage,omitempty"` + ImagePullPolicy *v1.PullPolicy `json:"imagePullPolicy,omitempty"` + Databricks *DatabricksConfigApplyConfiguration `json:"databricks,omitempty"` + LoggedInWarning *string `json:"loggedInWarning,omitempty"` + PublicWarning *string `json:"publicWarning,omitempty"` + Replicas *int `json:"replicas,omitempty"` + ExperimentalFeatures *InternalConnectExperimentalFeaturesApplyConfiguration `json:"experimentalFeatures,omitempty"` + DomainPrefix *string `json:"domainPrefix,omitempty"` + BaseDomain *string `json:"baseDomain,omitempty"` + GPUSettings *GPUSettingsApplyConfiguration `json:"gpuSettings,omitempty"` + DatabaseSettings *DatabaseSettingsApplyConfiguration `json:"databaseSettings,omitempty"` + ScheduleConcurrency *int `json:"scheduleConcurrency,omitempty"` + AdditionalRuntimeImages []ConnectRuntimeImageSpecApplyConfiguration `json:"additionalRuntimeImages,omitempty"` } // InternalConnectSpecApplyConfiguration constructs a declarative configuration of the InternalConnectSpec type for use with @@ -194,3 +195,16 @@ func (b *InternalConnectSpecApplyConfiguration) WithScheduleConcurrency(value in b.ScheduleConcurrency = &value return b } + +// WithAdditionalRuntimeImages adds the given value to the AdditionalRuntimeImages 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 AdditionalRuntimeImages field. +func (b *InternalConnectSpecApplyConfiguration) WithAdditionalRuntimeImages(values ...*ConnectRuntimeImageSpecApplyConfiguration) *InternalConnectSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithAdditionalRuntimeImages") + } + b.AdditionalRuntimeImages = append(b.AdditionalRuntimeImages, *values[i]) + } + return b +} diff --git a/client-go/applyconfiguration/utils.go b/client-go/applyconfiguration/utils.go index 44cfcaa7..22670ecf 100644 --- a/client-go/applyconfiguration/utils.go +++ b/client-go/applyconfiguration/utils.go @@ -77,6 +77,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &corev1beta1.ConnectQuartoConfigApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("ConnectRConfig"): return &corev1beta1.ConnectRConfigApplyConfiguration{} + case v1beta1.SchemeGroupVersion.WithKind("ConnectRuntimeImageSpec"): + return &corev1beta1.ConnectRuntimeImageSpecApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("ConnectSamlConfig"): return &corev1beta1.ConnectSamlConfigApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("ConnectSchedulerConfig"): diff --git a/config/crd/bases/core.posit.team_connects.yaml b/config/crd/bases/core.posit.team_connects.yaml index 082c54d1..1374cf2e 100644 --- a/config/crd/bases/core.posit.team_connects.yaml +++ b/config/crd/bases/core.posit.team_connects.yaml @@ -48,6 +48,41 @@ spec: description: AddEnv adds arbitrary environment variables to the container env type: object + additionalRuntimeImages: + description: |- + AdditionalRuntimeImages specifies additional runtime images to append to the defaults + for Connect off-host execution. These are added after the built-in default images. + items: + description: ConnectRuntimeImageSpec defines a runtime image for + Connect off-host execution + properties: + osVersion: + description: OSVersion is the OS version (e.g., "ubuntu2204") + minLength: 1 + type: string + pyVersion: + description: PyVersion is the Python version (e.g., "3.13.9") + minLength: 1 + type: string + quartoVersion: + description: QuartoVersion is the Quarto version (e.g., "1.8.25") + minLength: 1 + type: string + rVersion: + description: RVersion is the R version (e.g., "4.5.2") + minLength: 1 + type: string + repo: + default: ghcr.io/rstudio/content-pro + description: Repo is the container image repository (e.g., "ghcr.io/rstudio/content-pro") + type: string + required: + - osVersion + - pyVersion + - quartoVersion + - rVersion + type: object + type: array additionalVolumes: description: AdditionalVolumes represents additional VolumeSpec's that can be defined diff --git a/config/crd/bases/core.posit.team_sites.yaml b/config/crd/bases/core.posit.team_sites.yaml index d4ab8ffc..701b84c9 100644 --- a/config/crd/bases/core.posit.team_sites.yaml +++ b/config/crd/bases/core.posit.team_sites.yaml @@ -76,6 +76,43 @@ spec: additionalProperties: type: string type: object + additionalRuntimeImages: + description: |- + AdditionalRuntimeImages specifies additional runtime images to append to the defaults + for Connect off-host execution + items: + description: ConnectRuntimeImageSpec defines a runtime image + for Connect off-host execution + properties: + osVersion: + description: OSVersion is the OS version (e.g., "ubuntu2204") + minLength: 1 + type: string + pyVersion: + description: PyVersion is the Python version (e.g., "3.13.9") + minLength: 1 + type: string + quartoVersion: + description: QuartoVersion is the Quarto version (e.g., + "1.8.25") + minLength: 1 + type: string + rVersion: + description: RVersion is the R version (e.g., "4.5.2") + minLength: 1 + type: string + repo: + default: ghcr.io/rstudio/content-pro + description: Repo is the container image repository (e.g., + "ghcr.io/rstudio/content-pro") + type: string + required: + - osVersion + - pyVersion + - quartoVersion + - rVersion + type: object + type: array auth: properties: administratorRoleMapping: diff --git a/dist/chart/templates/crd/core.posit.team_connects.yaml b/dist/chart/templates/crd/core.posit.team_connects.yaml index 0542131e..d91ce9df 100755 --- a/dist/chart/templates/crd/core.posit.team_connects.yaml +++ b/dist/chart/templates/crd/core.posit.team_connects.yaml @@ -69,6 +69,41 @@ spec: description: AddEnv adds arbitrary environment variables to the container env type: object + additionalRuntimeImages: + description: |- + AdditionalRuntimeImages specifies additional runtime images to append to the defaults + for Connect off-host execution. These are added after the built-in default images. + items: + description: ConnectRuntimeImageSpec defines a runtime image for + Connect off-host execution + properties: + osVersion: + description: OSVersion is the OS version (e.g., "ubuntu2204") + minLength: 1 + type: string + pyVersion: + description: PyVersion is the Python version (e.g., "3.13.9") + minLength: 1 + type: string + quartoVersion: + description: QuartoVersion is the Quarto version (e.g., "1.8.25") + minLength: 1 + type: string + rVersion: + description: RVersion is the R version (e.g., "4.5.2") + minLength: 1 + type: string + repo: + default: ghcr.io/rstudio/content-pro + description: Repo is the container image repository (e.g., "ghcr.io/rstudio/content-pro") + type: string + required: + - osVersion + - pyVersion + - quartoVersion + - rVersion + type: object + type: array additionalVolumes: description: AdditionalVolumes represents additional VolumeSpec's that can be defined diff --git a/dist/chart/templates/crd/core.posit.team_sites.yaml b/dist/chart/templates/crd/core.posit.team_sites.yaml index b406133c..d40af799 100755 --- a/dist/chart/templates/crd/core.posit.team_sites.yaml +++ b/dist/chart/templates/crd/core.posit.team_sites.yaml @@ -97,6 +97,43 @@ spec: additionalProperties: type: string type: object + additionalRuntimeImages: + description: |- + AdditionalRuntimeImages specifies additional runtime images to append to the defaults + for Connect off-host execution + items: + description: ConnectRuntimeImageSpec defines a runtime image + for Connect off-host execution + properties: + osVersion: + description: OSVersion is the OS version (e.g., "ubuntu2204") + minLength: 1 + type: string + pyVersion: + description: PyVersion is the Python version (e.g., "3.13.9") + minLength: 1 + type: string + quartoVersion: + description: QuartoVersion is the Quarto version (e.g., + "1.8.25") + minLength: 1 + type: string + rVersion: + description: RVersion is the R version (e.g., "4.5.2") + minLength: 1 + type: string + repo: + default: ghcr.io/rstudio/content-pro + description: Repo is the container image repository (e.g., + "ghcr.io/rstudio/content-pro") + type: string + required: + - osVersion + - pyVersion + - quartoVersion + - rVersion + type: object + type: array auth: properties: administratorRoleMapping: diff --git a/internal/controller/core/site_controller_connect.go b/internal/controller/core/site_controller_connect.go index b2a460da..92b29780 100644 --- a/internal/controller/core/site_controller_connect.go +++ b/internal/controller/core/site_controller_connect.go @@ -144,12 +144,13 @@ func (r *SiteReconciler) reconcileConnect( NodeSelector: site.Spec.Connect.NodeSelector, AddEnv: site.Spec.Connect.AddEnv, // default to true... - OffHostExecution: true, - Auth: site.Spec.Connect.Auth, - Secret: site.Spec.Secret, - WorkloadSecret: site.Spec.WorkloadSecret, - Debug: connectDebugLog, - Replicas: product.PassDefaultReplicas(site.Spec.Connect.Replicas, 1), + OffHostExecution: true, + AdditionalRuntimeImages: site.Spec.Connect.AdditionalRuntimeImages, + Auth: site.Spec.Connect.Auth, + Secret: site.Spec.Secret, + WorkloadSecret: site.Spec.WorkloadSecret, + Debug: connectDebugLog, + Replicas: product.PassDefaultReplicas(site.Spec.Connect.Replicas, 1), }, }