From 690150f7d3a7f3623ba97c64ea9480998fa99352 Mon Sep 17 00:00:00 2001 From: Steve Nolen Date: Tue, 3 Feb 2026 12:53:37 -0500 Subject: [PATCH 1/2] add basedomain to each product enables full differences between products without requiring full url specification. not sure it is the best path, but it keeps backwards compat with the existing prefix tooling and implementers don't have to deal with use cases where some fields are ignored. --- api/core/v1beta1/site_types.go | 15 +++ .../core/v1beta1/internalconnectspec.go | 9 ++ .../v1beta1/internalpackagemanagerspec.go | 9 ++ .../core/v1beta1/internalworkbenchspec.go | 9 ++ config/crd/bases/core.posit.team_sites.yaml | 15 +++ flightdeck/html/home.go | 17 ++- internal/controller/core/site_controller.go | 25 ++++- .../core/site_controller_workbench.go | 8 +- internal/controller/core/site_test.go | 102 ++++++++++++++++++ 9 files changed, 200 insertions(+), 9 deletions(-) diff --git a/api/core/v1beta1/site_types.go b/api/core/v1beta1/site_types.go index da24cada..c48dd202 100644 --- a/api/core/v1beta1/site_types.go +++ b/api/core/v1beta1/site_types.go @@ -208,6 +208,11 @@ type InternalPackageManagerSpec struct { // +kubebuilder:default=packagemanager DomainPrefix string `json:"domainPrefix,omitempty"` + // BaseDomain overrides site.Spec.Domain for this product's URL construction. + // When set, the product URL will be: domainPrefix.baseDomain + // +optional + BaseDomain string `json:"baseDomain,omitempty"` + // GitSSHKeys defines SSH key configurations for Git authentication in Package Manager // These SSH keys will be made available to Package Manager for Git Builders // +optional @@ -248,6 +253,11 @@ type InternalConnectSpec struct { // +kubebuilder:default=connect DomainPrefix string `json:"domainPrefix,omitempty"` + // BaseDomain overrides site.Spec.Domain for this product's URL construction. + // When set, the product URL will be: domainPrefix.baseDomain + // +optional + BaseDomain string `json:"baseDomain,omitempty"` + // GPUSettings allows configuring GPU resource requests and limits GPUSettings *GPUSettings `json:"gpuSettings,omitempty"` @@ -366,6 +376,11 @@ type InternalWorkbenchSpec struct { // +kubebuilder:default=workbench DomainPrefix string `json:"domainPrefix,omitempty"` + // BaseDomain overrides site.Spec.Domain for this product's URL construction. + // When set, the product URL will be: domainPrefix.baseDomain + // +optional + BaseDomain string `json:"baseDomain,omitempty"` + // Workbench Auth/Login Landing Page Customization HTML AuthLoginPageHtml string `json:"authLoginPageHtml,omitempty"` diff --git a/client-go/applyconfiguration/core/v1beta1/internalconnectspec.go b/client-go/applyconfiguration/core/v1beta1/internalconnectspec.go index 81dfc18e..79b63ea0 100644 --- a/client-go/applyconfiguration/core/v1beta1/internalconnectspec.go +++ b/client-go/applyconfiguration/core/v1beta1/internalconnectspec.go @@ -27,6 +27,7 @@ type InternalConnectSpecApplyConfiguration struct { 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"` @@ -162,6 +163,14 @@ func (b *InternalConnectSpecApplyConfiguration) WithDomainPrefix(value string) * return b } +// WithBaseDomain sets the BaseDomain 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 BaseDomain field is set to the value of the last call. +func (b *InternalConnectSpecApplyConfiguration) WithBaseDomain(value string) *InternalConnectSpecApplyConfiguration { + b.BaseDomain = &value + return b +} + // WithGPUSettings sets the GPUSettings 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 GPUSettings field is set to the value of the last call. diff --git a/client-go/applyconfiguration/core/v1beta1/internalpackagemanagerspec.go b/client-go/applyconfiguration/core/v1beta1/internalpackagemanagerspec.go index 1a0a1c37..32cdab2e 100644 --- a/client-go/applyconfiguration/core/v1beta1/internalpackagemanagerspec.go +++ b/client-go/applyconfiguration/core/v1beta1/internalpackagemanagerspec.go @@ -22,6 +22,7 @@ type InternalPackageManagerSpecApplyConfiguration struct { S3Bucket *string `json:"s3Bucket,omitempty"` Replicas *int `json:"replicas,omitempty"` DomainPrefix *string `json:"domainPrefix,omitempty"` + BaseDomain *string `json:"baseDomain,omitempty"` GitSSHKeys []SSHKeyConfigApplyConfiguration `json:"gitSSHKeys,omitempty"` AzureFiles *AzureFilesConfigApplyConfiguration `json:"azureFiles,omitempty"` } @@ -116,6 +117,14 @@ func (b *InternalPackageManagerSpecApplyConfiguration) WithDomainPrefix(value st return b } +// WithBaseDomain sets the BaseDomain 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 BaseDomain field is set to the value of the last call. +func (b *InternalPackageManagerSpecApplyConfiguration) WithBaseDomain(value string) *InternalPackageManagerSpecApplyConfiguration { + b.BaseDomain = &value + return b +} + // WithGitSSHKeys adds the given value to the GitSSHKeys 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 GitSSHKeys field. diff --git a/client-go/applyconfiguration/core/v1beta1/internalworkbenchspec.go b/client-go/applyconfiguration/core/v1beta1/internalworkbenchspec.go index 23ee6ebc..f6b305d8 100644 --- a/client-go/applyconfiguration/core/v1beta1/internalworkbenchspec.go +++ b/client-go/applyconfiguration/core/v1beta1/internalworkbenchspec.go @@ -41,6 +41,7 @@ type InternalWorkbenchSpecApplyConfiguration struct { VSCodeSettings *VSCodeConfigApplyConfiguration `json:"vsCodeConfig,omitempty"` ApiSettings *ApiSettingsConfigApplyConfiguration `json:"apiSettings,omitempty"` DomainPrefix *string `json:"domainPrefix,omitempty"` + BaseDomain *string `json:"baseDomain,omitempty"` AuthLoginPageHtml *string `json:"authLoginPageHtml,omitempty"` JupyterConfig *WorkbenchJupyterConfigApplyConfiguration `json:"jupyterConfig,omitempty"` } @@ -305,6 +306,14 @@ func (b *InternalWorkbenchSpecApplyConfiguration) WithDomainPrefix(value string) return b } +// WithBaseDomain sets the BaseDomain 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 BaseDomain field is set to the value of the last call. +func (b *InternalWorkbenchSpecApplyConfiguration) WithBaseDomain(value string) *InternalWorkbenchSpecApplyConfiguration { + b.BaseDomain = &value + return b +} + // WithAuthLoginPageHtml sets the AuthLoginPageHtml 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 AuthLoginPageHtml 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 0524fcb6..d4ab8ffc 100644 --- a/config/crd/bases/core.posit.team_sites.yaml +++ b/config/crd/bases/core.posit.team_sites.yaml @@ -127,6 +127,11 @@ spec: type: string type: array type: object + baseDomain: + description: |- + BaseDomain overrides site.Spec.Domain for this product's URL construction. + When set, the product URL will be: domainPrefix.baseDomain + type: string databaseSettings: properties: instrumentationSchema: @@ -576,6 +581,11 @@ spec: StorageClass that uses the Azure Files CSI driver type: string type: object + baseDomain: + description: |- + BaseDomain overrides site.Spec.Domain for this product's URL construction. + When set, the product URL will be: domainPrefix.baseDomain + type: string domainPrefix: default: packagemanager type: string @@ -906,6 +916,11 @@ spec: authLoginPageHtml: description: Workbench Auth/Login Landing Page Customization HTML type: string + baseDomain: + description: |- + BaseDomain overrides site.Spec.Domain for this product's URL construction. + When set, the product URL will be: domainPrefix.baseDomain + type: string createUsersAutomatically: type: boolean databricks: diff --git a/flightdeck/html/home.go b/flightdeck/html/home.go index eda30647..fc8194c4 100644 --- a/flightdeck/html/home.go +++ b/flightdeck/html/home.go @@ -9,8 +9,17 @@ import ( . "maragu.dev/gomponents/html" ) +func getEffectiveBaseDomain(baseDomain, fallbackDomain string) string { + if baseDomain != "" { + return baseDomain + } + return fallbackDomain +} + func HomePage(site positcov1beta1.Site, config *internal.ServerConfig) Node { - baseUrl := site.Spec.Domain + workbenchBaseUrl := getEffectiveBaseDomain(site.Spec.Workbench.BaseDomain, site.Spec.Domain) + connectBaseUrl := getEffectiveBaseDomain(site.Spec.Connect.BaseDomain, site.Spec.Domain) + packageManagerBaseUrl := getEffectiveBaseDomain(site.Spec.PackageManager.BaseDomain, site.Spec.Domain) return page("Home", config, Main( Class("max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8"), @@ -28,17 +37,17 @@ func HomePage(site positcov1beta1.Site, config *internal.ServerConfig) Node { Div( Class("grid grid-cols-1 md:grid-cols-3 gap-6 max-w-6xl mx-auto"), If(!internal.IsEmptyStruct(site.Spec.Workbench), - productCard("/static/logo-workbench.svg", "Posit Workbench", site.Spec.Workbench.DomainPrefix, baseUrl, + productCard("/static/logo-workbench.svg", "Posit Workbench", site.Spec.Workbench.DomainPrefix, workbenchBaseUrl, "Manage your environments with integrated tools like JupyterLab, RStudio, VS Code and Positron. "+ "Self-service workspaces provide a secure solution for both on-premises and cloud deployments"), ), If(!internal.IsEmptyStruct(site.Spec.Connect), - productCard("/static/logo-connect.svg", "Posit Connect", site.Spec.Connect.DomainPrefix, baseUrl, + productCard("/static/logo-connect.svg", "Posit Connect", site.Spec.Connect.DomainPrefix, connectBaseUrl, "Share your interactive applications, dashboards, and reports built with R and Python. "+ "Manage access, and deliver real-time insights to your stakeholders."), ), If(!internal.IsEmptyStruct(site.Spec.PackageManager), - productCard("/static/logo-packagemanager.svg", "Posit Package Manager", site.Spec.PackageManager.DomainPrefix, baseUrl, + productCard("/static/logo-packagemanager.svg", "Posit Package Manager", site.Spec.PackageManager.DomainPrefix, packageManagerBaseUrl, "Securely manage your R and Python packages from public and internal sources, ensuring consistent versions for reproducibility. "+ "Strengthen your security with vulnerability reporting and air-gapped deployments."), ), diff --git a/internal/controller/core/site_controller.go b/internal/controller/core/site_controller.go index 1c8ea043..fdb59c44 100644 --- a/internal/controller/core/site_controller.go +++ b/internal/controller/core/site_controller.go @@ -89,6 +89,13 @@ func prefixDomain(prefix, domain string, domainType positcov1beta1.SiteDomainTyp return fmt.Sprintf("%s.%s", prefix, domain) } +func getEffectiveBaseDomain(baseDomain, fallbackDomain string) string { + if baseDomain != "" { + return baseDomain + } + return fallbackDomain +} + func (r *SiteReconciler) reconcileResources(ctx context.Context, req ctrl.Request, site *positcov1beta1.Site) (ctrl.Result, error) { l := r.GetLogger(ctx).WithValues( @@ -125,9 +132,21 @@ func (r *SiteReconciler) reconcileResources(ctx context.Context, req ctrl.Reques // Default to subdomain type since SiteHome is removed domainType := positcov1beta1.SiteSubDomain - packageManagerUrl := prefixDomain(site.Spec.PackageManager.DomainPrefix, site.Spec.Domain, domainType) - connectUrl := prefixDomain(site.Spec.Connect.DomainPrefix, site.Spec.Domain, domainType) - workbenchUrl := prefixDomain(site.Spec.Workbench.DomainPrefix, site.Spec.Domain, domainType) + packageManagerUrl := prefixDomain( + site.Spec.PackageManager.DomainPrefix, + getEffectiveBaseDomain(site.Spec.PackageManager.BaseDomain, site.Spec.Domain), + domainType, + ) + connectUrl := prefixDomain( + site.Spec.Connect.DomainPrefix, + getEffectiveBaseDomain(site.Spec.Connect.BaseDomain, site.Spec.Domain), + domainType, + ) + workbenchUrl := prefixDomain( + site.Spec.Workbench.DomainPrefix, + getEffectiveBaseDomain(site.Spec.Workbench.BaseDomain, site.Spec.Domain), + domainType, + ) packageManagerRepoUrl := fmt.Sprintf("https://%s/cran/__linux__/jammy/latest", packageManagerUrl) // TODO: don't hardcode OS if site.Spec.PackageManagerUrl != "" { diff --git a/internal/controller/core/site_controller_workbench.go b/internal/controller/core/site_controller_workbench.go index b844aa61..e5ad5b3f 100644 --- a/internal/controller/core/site_controller_workbench.go +++ b/internal/controller/core/site_controller_workbench.go @@ -161,8 +161,12 @@ func (r *SiteReconciler) reconcileWorkbench( WorkbenchSessionIniConfig: v1beta1.WorkbenchSessionIniConfig{ RSession: &v1beta1.WorkbenchRSessionConfig{ // TODO: need TLS to be configurable... for plaintext sites... - DefaultRSConnectServer: "https://" + prefixDomain(site.Spec.Connect.DomainPrefix, site.Spec.Domain, v1beta1.SiteSubDomain), - CopilotEnabled: 1, + DefaultRSConnectServer: "https://" + prefixDomain( + site.Spec.Connect.DomainPrefix, + getEffectiveBaseDomain(site.Spec.Connect.BaseDomain, site.Spec.Domain), + v1beta1.SiteSubDomain, + ), + CopilotEnabled: 1, }, // TODO: configure the expected package manager repositories...? Repos: &v1beta1.WorkbenchRepoConfig{ diff --git a/internal/controller/core/site_test.go b/internal/controller/core/site_test.go index 211e5f54..4db8baf5 100644 --- a/internal/controller/core/site_test.go +++ b/internal/controller/core/site_test.go @@ -939,3 +939,105 @@ func TestSiteReconciler_WorkbenchSessionImagePullPolicyNever(t *testing.T) { testWorkbench := getWorkbench(t, cli, siteNamespace, siteName) assert.Equal(t, corev1.PullNever, testWorkbench.Spec.SessionConfig.Pod.ImagePullPolicy) } + +func TestSiteReconciler_BaseDomainNotSet(t *testing.T) { + siteName := "base-domain-not-set" + siteNamespace := "posit-team" + site := defaultSite(siteName) + site.Spec.Domain = "example.com" + site.Spec.Connect.DomainPrefix = "connect" + site.Spec.Workbench.DomainPrefix = "workbench" + site.Spec.PackageManager.DomainPrefix = "packagemanager" + + cli, _, err := runFakeSiteReconciler(t, siteNamespace, siteName, site) + assert.Nil(t, err) + + // Verify default behavior is preserved when BaseDomain is not set + testConnect := getConnect(t, cli, siteNamespace, siteName) + assert.Equal(t, "connect.example.com", testConnect.Spec.Url) + + testWorkbench := getWorkbench(t, cli, siteNamespace, siteName) + assert.Equal(t, "workbench.example.com", testWorkbench.Spec.Url) + // Verify DefaultRSConnectServer uses site domain when BaseDomain not set + assert.Equal(t, "https://connect.example.com", testWorkbench.Spec.Config.RSession.DefaultRSConnectServer) + + testPackageManager := getPackageManager(t, cli, siteNamespace, siteName) + assert.Equal(t, "packagemanager.example.com", testPackageManager.Spec.Url) +} + +func TestSiteReconciler_BaseDomainConnectOnly(t *testing.T) { + siteName := "base-domain-connect-only" + siteNamespace := "posit-team" + site := defaultSite(siteName) + site.Spec.Domain = "example.com" + site.Spec.Connect.DomainPrefix = "connect" + site.Spec.Connect.BaseDomain = "connect-custom.com" + site.Spec.Workbench.DomainPrefix = "workbench" + site.Spec.PackageManager.DomainPrefix = "packagemanager" + + cli, _, err := runFakeSiteReconciler(t, siteNamespace, siteName, site) + assert.Nil(t, err) + + // Verify Connect uses custom BaseDomain + testConnect := getConnect(t, cli, siteNamespace, siteName) + assert.Equal(t, "connect.connect-custom.com", testConnect.Spec.Url) + + // Verify Workbench uses site domain (not custom) + testWorkbench := getWorkbench(t, cli, siteNamespace, siteName) + assert.Equal(t, "workbench.example.com", testWorkbench.Spec.Url) + // Verify DefaultRSConnectServer uses Connect's BaseDomain + assert.Equal(t, "https://connect.connect-custom.com", testWorkbench.Spec.Config.RSession.DefaultRSConnectServer) + + // Verify PackageManager uses site domain (not custom) + testPackageManager := getPackageManager(t, cli, siteNamespace, siteName) + assert.Equal(t, "packagemanager.example.com", testPackageManager.Spec.Url) +} + +func TestSiteReconciler_BaseDomainAllProducts(t *testing.T) { + siteName := "base-domain-all-products" + siteNamespace := "posit-team" + site := defaultSite(siteName) + site.Spec.Domain = "example.com" + site.Spec.Connect.DomainPrefix = "connect" + site.Spec.Connect.BaseDomain = "connect-domain.com" + site.Spec.Workbench.DomainPrefix = "workbench" + site.Spec.Workbench.BaseDomain = "workbench-domain.com" + site.Spec.PackageManager.DomainPrefix = "packagemanager" + site.Spec.PackageManager.BaseDomain = "pm-domain.com" + + cli, _, err := runFakeSiteReconciler(t, siteNamespace, siteName, site) + assert.Nil(t, err) + + // Verify all products use their custom BaseDomains + testConnect := getConnect(t, cli, siteNamespace, siteName) + assert.Equal(t, "connect.connect-domain.com", testConnect.Spec.Url) + + testWorkbench := getWorkbench(t, cli, siteNamespace, siteName) + assert.Equal(t, "workbench.workbench-domain.com", testWorkbench.Spec.Url) + // Verify DefaultRSConnectServer uses Connect's BaseDomain + assert.Equal(t, "https://connect.connect-domain.com", testWorkbench.Spec.Config.RSession.DefaultRSConnectServer) + + testPackageManager := getPackageManager(t, cli, siteNamespace, siteName) + assert.Equal(t, "packagemanager.pm-domain.com", testPackageManager.Spec.Url) +} + +func TestSiteReconciler_BaseDomainWithCustomPrefix(t *testing.T) { + siteName := "base-domain-custom-prefix" + siteNamespace := "posit-team" + site := defaultSite(siteName) + site.Spec.Domain = "example.com" + site.Spec.Connect.DomainPrefix = "rsc" + site.Spec.Connect.BaseDomain = "custom-domain.com" + site.Spec.Workbench.DomainPrefix = "workbench" + + cli, _, err := runFakeSiteReconciler(t, siteNamespace, siteName, site) + assert.Nil(t, err) + + // Verify custom prefix is preserved with BaseDomain + testConnect := getConnect(t, cli, siteNamespace, siteName) + assert.Equal(t, "rsc.custom-domain.com", testConnect.Spec.Url) + + // Verify Workbench's DefaultRSConnectServer uses custom prefix and BaseDomain + testWorkbench := getWorkbench(t, cli, siteNamespace, siteName) + assert.Equal(t, "https://rsc.custom-domain.com", testWorkbench.Spec.Config.RSession.DefaultRSConnectServer) +} From ec76694a59e1b47956215d0b779024730629851b Mon Sep 17 00:00:00 2001 From: Steve Nolen Date: Tue, 3 Feb 2026 13:03:12 -0500 Subject: [PATCH 2/2] fix tests --- .../templates/crd/core.posit.team_sites.yaml | 15 +++++++++++++++ .../templates/rbac/auth_proxy_service.yaml | 17 +++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100755 dist/chart/templates/rbac/auth_proxy_service.yaml diff --git a/dist/chart/templates/crd/core.posit.team_sites.yaml b/dist/chart/templates/crd/core.posit.team_sites.yaml index 31f8dbd9..b406133c 100755 --- a/dist/chart/templates/crd/core.posit.team_sites.yaml +++ b/dist/chart/templates/crd/core.posit.team_sites.yaml @@ -148,6 +148,11 @@ spec: type: string type: array type: object + baseDomain: + description: |- + BaseDomain overrides site.Spec.Domain for this product's URL construction. + When set, the product URL will be: domainPrefix.baseDomain + type: string databaseSettings: properties: instrumentationSchema: @@ -597,6 +602,11 @@ spec: StorageClass that uses the Azure Files CSI driver type: string type: object + baseDomain: + description: |- + BaseDomain overrides site.Spec.Domain for this product's URL construction. + When set, the product URL will be: domainPrefix.baseDomain + type: string domainPrefix: default: packagemanager type: string @@ -927,6 +937,11 @@ spec: authLoginPageHtml: description: Workbench Auth/Login Landing Page Customization HTML type: string + baseDomain: + description: |- + BaseDomain overrides site.Spec.Domain for this product's URL construction. + When set, the product URL will be: domainPrefix.baseDomain + type: string createUsersAutomatically: type: boolean databricks: diff --git a/dist/chart/templates/rbac/auth_proxy_service.yaml b/dist/chart/templates/rbac/auth_proxy_service.yaml new file mode 100755 index 00000000..b665f0dc --- /dev/null +++ b/dist/chart/templates/rbac/auth_proxy_service.yaml @@ -0,0 +1,17 @@ +{{- if .Values.rbac.enable }} +apiVersion: v1 +kind: Service +metadata: + labels: + {{- include "chart.labels" . | nindent 4 }} + name: {{ .Values.controllerManager.serviceAccountName }}-metrics-service + namespace: {{ .Release.Namespace }} +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager +{{- end -}}