Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions api/core/v1beta1/connect_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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,
},
} {

Expand All @@ -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()
}

Expand Down
98 changes: 97 additions & 1 deletion api/core/v1beta1/connect_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
5 changes: 5 additions & 0 deletions api/core/v1beta1/site_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
25 changes: 25 additions & 0 deletions api/core/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 44 additions & 30 deletions client-go/applyconfiguration/core/v1beta1/connectspec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading