From 93285359f6b51e02f6ae0041c3cf1448e6776dde Mon Sep 17 00:00:00 2001 From: Steve Nolen Date: Tue, 10 Feb 2026 13:47:51 -0500 Subject: [PATCH 1/6] allow completely disabling connect --- api/core/v1beta1/site_types.go | 5 ++ api/core/v1beta1/zz_generated.deepcopy.go | 5 ++ .../core/v1beta1/connectruntimeimagespec.go | 62 +++++++++++++++++++ .../core/v1beta1/internalconnectspec.go | 9 +++ config/crd/bases/core.posit.team_sites.yaml | 5 ++ .../core/site_controller_connect.go | 6 ++ internal/controller/core/site_test.go | 16 +++++ 7 files changed, 108 insertions(+) create mode 100644 client-go/applyconfiguration/core/v1beta1/connectruntimeimagespec.go diff --git a/api/core/v1beta1/site_types.go b/api/core/v1beta1/site_types.go index c14e1cd..f3c1697 100644 --- a/api/core/v1beta1/site_types.go +++ b/api/core/v1beta1/site_types.go @@ -224,6 +224,11 @@ type InternalPackageManagerSpec struct { } type InternalConnectSpec struct { + // Enabled controls whether Connect is deployed. Defaults to true if not specified. + // Set to false to explicitly disable Connect deployment. + // +optional + Enabled *bool `json:"enabled,omitempty"` + License product.LicenseSpec `json:"license,omitempty"` Volume *product.VolumeSpec `json:"volume,omitempty"` diff --git a/api/core/v1beta1/zz_generated.deepcopy.go b/api/core/v1beta1/zz_generated.deepcopy.go index b9831e4..23e7741 100644 --- a/api/core/v1beta1/zz_generated.deepcopy.go +++ b/api/core/v1beta1/zz_generated.deepcopy.go @@ -1091,6 +1091,11 @@ func (in *InternalConnectExperimentalFeatures) DeepCopy() *InternalConnectExperi // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InternalConnectSpec) DeepCopyInto(out *InternalConnectSpec) { *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } out.License = in.License if in.Volume != nil { in, out := &in.Volume, &out.Volume diff --git a/client-go/applyconfiguration/core/v1beta1/connectruntimeimagespec.go b/client-go/applyconfiguration/core/v1beta1/connectruntimeimagespec.go new file mode 100644 index 0000000..29cf31e --- /dev/null +++ b/client-go/applyconfiguration/core/v1beta1/connectruntimeimagespec.go @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2023-2026 Posit Software, PBC + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1beta1 + +// ConnectRuntimeImageSpecApplyConfiguration represents a declarative configuration of the ConnectRuntimeImageSpec type for use +// with apply. +type ConnectRuntimeImageSpecApplyConfiguration struct { + RVersion *string `json:"rVersion,omitempty"` + PyVersion *string `json:"pyVersion,omitempty"` + OSVersion *string `json:"osVersion,omitempty"` + QuartoVersion *string `json:"quartoVersion,omitempty"` + Repo *string `json:"repo,omitempty"` +} + +// ConnectRuntimeImageSpecApplyConfiguration constructs a declarative configuration of the ConnectRuntimeImageSpec type for use with +// apply. +func ConnectRuntimeImageSpec() *ConnectRuntimeImageSpecApplyConfiguration { + return &ConnectRuntimeImageSpecApplyConfiguration{} +} + +// WithRVersion sets the RVersion 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 RVersion field is set to the value of the last call. +func (b *ConnectRuntimeImageSpecApplyConfiguration) WithRVersion(value string) *ConnectRuntimeImageSpecApplyConfiguration { + b.RVersion = &value + return b +} + +// WithPyVersion sets the PyVersion 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 PyVersion field is set to the value of the last call. +func (b *ConnectRuntimeImageSpecApplyConfiguration) WithPyVersion(value string) *ConnectRuntimeImageSpecApplyConfiguration { + b.PyVersion = &value + return b +} + +// WithOSVersion sets the OSVersion 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 OSVersion field is set to the value of the last call. +func (b *ConnectRuntimeImageSpecApplyConfiguration) WithOSVersion(value string) *ConnectRuntimeImageSpecApplyConfiguration { + b.OSVersion = &value + return b +} + +// WithQuartoVersion sets the QuartoVersion 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 QuartoVersion field is set to the value of the last call. +func (b *ConnectRuntimeImageSpecApplyConfiguration) WithQuartoVersion(value string) *ConnectRuntimeImageSpecApplyConfiguration { + b.QuartoVersion = &value + return b +} + +// WithRepo sets the Repo 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 Repo field is set to the value of the last call. +func (b *ConnectRuntimeImageSpecApplyConfiguration) WithRepo(value string) *ConnectRuntimeImageSpecApplyConfiguration { + b.Repo = &value + return b +} diff --git a/client-go/applyconfiguration/core/v1beta1/internalconnectspec.go b/client-go/applyconfiguration/core/v1beta1/internalconnectspec.go index 3d88320..bd7f308 100644 --- a/client-go/applyconfiguration/core/v1beta1/internalconnectspec.go +++ b/client-go/applyconfiguration/core/v1beta1/internalconnectspec.go @@ -13,6 +13,7 @@ import ( // InternalConnectSpecApplyConfiguration represents a declarative configuration of the InternalConnectSpec type for use // with apply. type InternalConnectSpecApplyConfiguration struct { + Enabled *bool `json:"enabled,omitempty"` License *product.LicenseSpec `json:"license,omitempty"` Volume *product.VolumeSpec `json:"volume,omitempty"` NodeSelector map[string]string `json:"nodeSelector,omitempty"` @@ -40,6 +41,14 @@ func InternalConnectSpec() *InternalConnectSpecApplyConfiguration { return &InternalConnectSpecApplyConfiguration{} } +// WithEnabled sets the Enabled 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 Enabled field is set to the value of the last call. +func (b *InternalConnectSpecApplyConfiguration) WithEnabled(value bool) *InternalConnectSpecApplyConfiguration { + b.Enabled = &value + return b +} + // WithLicense sets the License 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 License 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 701b84c..e820e7a 100644 --- a/config/crd/bases/core.posit.team_sites.yaml +++ b/config/crd/bases/core.posit.team_sites.yaml @@ -190,6 +190,11 @@ spec: domainPrefix: default: connect type: string + enabled: + description: |- + Enabled controls whether Connect is deployed. Defaults to true if not specified. + Set to false to explicitly disable Connect deployment. + type: boolean experimentalFeatures: properties: chronicleSidecarProductApiKeyEnabled: diff --git a/internal/controller/core/site_controller_connect.go b/internal/controller/core/site_controller_connect.go index 92b2978..f4618b1 100644 --- a/internal/controller/core/site_controller_connect.go +++ b/internal/controller/core/site_controller_connect.go @@ -28,6 +28,12 @@ func (r *SiteReconciler) reconcileConnect( "event", "reconcile-connect", ) + // Skip Connect reconciliation if explicitly disabled + if site.Spec.Connect.Enabled != nil && !*site.Spec.Connect.Enabled { + l.V(1).Info("skipping Connect reconciliation: explicitly disabled via Site.Spec.Connect.Enabled=false") + return nil + } + connectDebugLog := false if site.Spec.Debug { connectDebugLog = true diff --git a/internal/controller/core/site_test.go b/internal/controller/core/site_test.go index 4db8baf..fb3b15c 100644 --- a/internal/controller/core/site_test.go +++ b/internal/controller/core/site_test.go @@ -1041,3 +1041,19 @@ func TestSiteReconciler_BaseDomainWithCustomPrefix(t *testing.T) { testWorkbench := getWorkbench(t, cli, siteNamespace, siteName) assert.Equal(t, "https://rsc.custom-domain.com", testWorkbench.Spec.Config.RSession.DefaultRSConnectServer) } + +func TestSiteConnectSkippedWhenDisabled(t *testing.T) { + siteName := "disabled-connect" + siteNamespace := "posit-team" + site := defaultSite(siteName) + enabled := false + site.Spec.Connect.Enabled = &enabled + + cli, _, err := runFakeSiteReconciler(t, siteNamespace, siteName, site) + assert.NoError(t, err) + + // Connect CRD should NOT be created when explicitly disabled + connect := &v1beta1.Connect{} + err = cli.Get(context.TODO(), client.ObjectKey{Name: siteName, Namespace: siteNamespace}, connect) + assert.Error(t, err) // Should error because it doesn't exist +} From f8845797f23dbd2c8b66dd9154cecf6263d7425d Mon Sep 17 00:00:00 2001 From: Steve Nolen Date: Tue, 10 Feb 2026 14:08:58 -0500 Subject: [PATCH 2/6] run make helm-generate --- dist/chart/templates/crd/core.posit.team_sites.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dist/chart/templates/crd/core.posit.team_sites.yaml b/dist/chart/templates/crd/core.posit.team_sites.yaml index d40af79..5d869f6 100755 --- a/dist/chart/templates/crd/core.posit.team_sites.yaml +++ b/dist/chart/templates/crd/core.posit.team_sites.yaml @@ -211,6 +211,11 @@ spec: domainPrefix: default: connect type: string + enabled: + description: |- + Enabled controls whether Connect is deployed. Defaults to true if not specified. + Set to false to explicitly disable Connect deployment. + type: boolean experimentalFeatures: properties: chronicleSidecarProductApiKeyEnabled: From 7afa773b6995eca571fc959da9852ebafec5fc04 Mon Sep 17 00:00:00 2001 From: Steve Nolen Date: Tue, 10 Feb 2026 17:21:14 -0500 Subject: [PATCH 3/6] refactor to clean up and handle network policies --- internal/controller/core/site_controller.go | 36 +++++++++++-------- .../core/site_controller_connect.go | 20 +++++++---- .../core/site_controller_networkpolicies.go | 34 +++++++++++++----- 3 files changed, 62 insertions(+), 28 deletions(-) diff --git a/internal/controller/core/site_controller.go b/internal/controller/core/site_controller.go index fdb59c4..4694045 100644 --- a/internal/controller/core/site_controller.go +++ b/internal/controller/core/site_controller.go @@ -296,20 +296,28 @@ func (r *SiteReconciler) reconcileResources(ctx context.Context, req ctrl.Reques workbenchAdditionalVolumes = append(workbenchAdditionalVolumes, site.Spec.Workbench.AdditionalVolumes...) // CONNECT - if err := r.reconcileConnect( - ctx, - req, - site, - dbUrl.Host, - sslMode, - connectVolumeName, - connectStorageClassName, - additionalVolumes, - packageManagerRepoUrl, - connectUrl, - ); err != nil { - l.Error(err, "error reconciling connect") - return ctrl.Result{}, err + if site.Spec.Connect.Enabled == nil || *site.Spec.Connect.Enabled == true { + if err := r.reconcileConnect( + ctx, + req, + site, + dbUrl.Host, + sslMode, + connectVolumeName, + connectStorageClassName, + additionalVolumes, + packageManagerRepoUrl, + connectUrl, + ); err != nil { + l.Error(err, "error reconciling connect") + return ctrl.Result{}, err + } + } else { + // Connect is disabled - clean up any existing Connect resources + if err := r.cleanupConnect(ctx, req, l); err != nil { + l.Error(err, "error cleaning up connect resources") + return ctrl.Result{}, err + } } // PACKAGE MANAGER diff --git a/internal/controller/core/site_controller_connect.go b/internal/controller/core/site_controller_connect.go index f4618b1..dd96225 100644 --- a/internal/controller/core/site_controller_connect.go +++ b/internal/controller/core/site_controller_connect.go @@ -4,11 +4,13 @@ import ( "context" "fmt" + "github.com/go-logr/logr" "github.com/posit-dev/team-operator/api/core/v1beta1" "github.com/posit-dev/team-operator/api/product" "github.com/posit-dev/team-operator/internal" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" controllerruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" ) func (r *SiteReconciler) reconcileConnect( @@ -28,12 +30,6 @@ func (r *SiteReconciler) reconcileConnect( "event", "reconcile-connect", ) - // Skip Connect reconciliation if explicitly disabled - if site.Spec.Connect.Enabled != nil && !*site.Spec.Connect.Enabled { - l.V(1).Info("skipping Connect reconciliation: explicitly disabled via Site.Spec.Connect.Enabled=false") - return nil - } - connectDebugLog := false if site.Spec.Debug { connectDebugLog = true @@ -255,3 +251,15 @@ func (r *SiteReconciler) reconcileConnect( } return nil } + +func (r *SiteReconciler) cleanupConnect(ctx context.Context, req controllerruntime.Request, l logr.Logger) error { + l = l.WithValues("event", "cleanup-connect") + + // Delete Connect CRD if it exists + connectKey := client.ObjectKey{Name: req.Name, Namespace: req.Namespace} + if err := internal.BasicDelete(ctx, r, l, connectKey, &v1beta1.Connect{}); err != nil { + return err + } + + return nil +} diff --git a/internal/controller/core/site_controller_networkpolicies.go b/internal/controller/core/site_controller_networkpolicies.go index 455bd3f..36c2020 100644 --- a/internal/controller/core/site_controller_networkpolicies.go +++ b/internal/controller/core/site_controller_networkpolicies.go @@ -43,14 +43,22 @@ func (r *SiteReconciler) reconcileNetworkPolicies(ctx context.Context, req ctrl. return err } - if err := r.reconcileConnectNetworkPolicy(ctx, req.Namespace, l, site); err != nil { - l.Error(err, "error ensuring connect network policy") - return err - } - - if err := r.reconcileConnectSessionNetworkPolicy(ctx, req.Namespace, l, site); err != nil { - l.Error(err, "error ensuring connect session network policy") - return err + // Connect network policies + if site.Spec.Connect.Enabled == nil || *site.Spec.Connect.Enabled == true { + if err := r.reconcileConnectNetworkPolicy(ctx, req.Namespace, l, site); err != nil { + l.Error(err, "error ensuring connect network policy") + return err + } + if err := r.reconcileConnectSessionNetworkPolicy(ctx, req.Namespace, l, site); err != nil { + l.Error(err, "error ensuring connect session network policy") + return err + } + } else { + // Clean up Connect network policies + if err := r.cleanupConnectNetworkPolicies(ctx, req, l); err != nil { + l.Error(err, "error cleaning up connect network policies") + return err + } } if err := r.reconcileHomeNetworkPolicy(ctx, req.Namespace, l, site); err != nil { @@ -783,3 +791,13 @@ func (r *SiteReconciler) reconcileFlightdeckNetworkPolicy(ctx context.Context, n }) return err } + +func (r *SiteReconciler) cleanupConnectNetworkPolicies(ctx context.Context, req ctrl.Request, l logr.Logger) error { + for _, suffix := range []string{"connect", "connect-session"} { + key := client.ObjectKey{Name: req.Name + "-" + suffix, Namespace: req.Namespace} + if err := internal.BasicDelete(ctx, r, l, key, &networkingv1.NetworkPolicy{}); err != nil { + return err + } + } + return nil +} From c2d266bd69dc2d278aa2e2ded6610d8421be835d Mon Sep 17 00:00:00 2001 From: Steve Nolen Date: Wed, 11 Feb 2026 09:58:53 -0500 Subject: [PATCH 4/6] address more pr feedback --- api/core/v1beta1/site_types.go | 15 +++++ docs/api-reference.md | 1 + docs/guides/connect-configuration.md | 59 +++++++++++++++++++ docs/guides/product-team-site-management.md | 5 ++ flightdeck/html/home.go | 4 +- flightdeck/html/siteconfig.go | 6 +- internal/controller/core/connect.go | 16 +++++ internal/controller/core/site_controller.go | 21 +++++-- .../core/site_controller_connect.go | 15 +++++ .../core/site_controller_workbench.go | 19 ++++-- 10 files changed, 147 insertions(+), 14 deletions(-) diff --git a/api/core/v1beta1/site_types.go b/api/core/v1beta1/site_types.go index f3c1697..8a6e270 100644 --- a/api/core/v1beta1/site_types.go +++ b/api/core/v1beta1/site_types.go @@ -226,6 +226,21 @@ type InternalPackageManagerSpec struct { type InternalConnectSpec struct { // Enabled controls whether Connect is deployed. Defaults to true if not specified. // Set to false to explicitly disable Connect deployment. + // + // WARNING: Disabling Connect (setting Enabled=false) is a DESTRUCTIVE operation that + // permanently deletes all Connect resources including: + // - The Connect database and all its data + // - All secrets (database credentials, provisioning keys, etc.) + // - Persistent volumes and claims + // - All deployed Kubernetes resources (deployments, services, ingress, etc.) + // + // Re-enabling Connect after disabling it will start completely fresh with: + // - A new, empty database + // - New secrets and credentials + // - No content or configuration from the previous deployment + // + // This is intentional behavior, not a bug. Only disable Connect if you intend to + // permanently destroy the Connect instance and all its data. // +optional Enabled *bool `json:"enabled,omitempty"` diff --git a/docs/api-reference.md b/docs/api-reference.md index 72547d7..fbcd45a 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -743,6 +743,7 @@ These types are used within the Site CRD for product configuration. | Field | Type | Description | |-------|------|-------------| +| `.enabled` | `*bool` | Enable/disable Connect deployment (default: true). **WARNING:** Setting to `false` permanently deletes all Connect data including the database, secrets, and volumes. Cannot be reversed. See [Connect Configuration Guide](guides/connect-configuration.md#enablingdisabling-connect) for details. | | `.license` | `LicenseSpec` | License configuration | | `.volume` | `*VolumeSpec` | Data volume | | `.nodeSelector` | `map[string]string` | Node selector | diff --git a/docs/guides/connect-configuration.md b/docs/guides/connect-configuration.md index bbef819..47de644 100644 --- a/docs/guides/connect-configuration.md +++ b/docs/guides/connect-configuration.md @@ -6,6 +6,10 @@ This comprehensive guide covers all configuration options for Posit Connect when 1. [Overview](#overview) 2. [Basic Configuration](#basic-configuration) + - [Enabling/Disabling Connect](#enablingdisabling-connect) + - [Image Configuration](#image-configuration) + - [Resource Scaling](#resource-scaling) + - [Domain and Ingress](#domain-and-ingress) 3. [Authentication Configuration](#authentication-configuration) 4. [Database Configuration](#database-configuration) 5. [Off-Host Execution / Kubernetes Launcher](#off-host-execution--kubernetes-launcher) @@ -56,6 +60,61 @@ When using a Site resource, the Site controller generates and manages the Connec ## Basic Configuration +### Enabling/Disabling Connect + +By default, Connect is enabled when specified in a Site configuration. You can explicitly control whether Connect is deployed: + +```yaml +spec: + connect: + # Enable Connect deployment (default: true) + enabled: true +``` + +#### WARNING: Disabling Connect is Destructive + +**Setting `enabled: false` is a PERMANENT, DESTRUCTIVE operation that cannot be reversed without complete data loss.** + +When you disable Connect by setting `enabled: false`, the Team Operator: + +1. **Deletes the Connect Custom Resource (CR)** from Kubernetes +2. **Triggers finalizers** that destroy all Connect resources, including: + - The Connect PostgreSQL database and **all its data** (published content, users, settings, schedules, etc.) + - All Kubernetes secrets (database credentials, provisioning keys, OAuth secrets) + - Persistent volumes and all stored content + - All Kubernetes deployments, services, ingress routes, and jobs + +**This means:** + +- All published content, applications, reports, and APIs are **permanently deleted** +- All user accounts, permissions, and settings are **permanently deleted** +- All content schedules, email subscriptions, and integrations are **permanently deleted** +- **Re-enabling Connect later will start with a completely fresh, empty instance** with no data from the previous deployment + +**When to use `enabled: false`:** + +- During initial cluster setup when you don't want Connect deployed yet +- When permanently decommissioning Connect and migrating to a different instance +- When you explicitly want to destroy all Connect data and start fresh + +**Never use `enabled: false` for:** + +- Temporarily stopping Connect (use `replicas: 0` instead to scale down without data loss) +- Maintenance windows (scale down replicas instead) +- Troubleshooting (use debug mode or check logs instead) + +**Example of safely scaling down Connect temporarily:** + +```yaml +spec: + connect: + # Scale down Connect pods without deleting data + replicas: 0 + # Keep enabled: true (or omit it, as true is the default) +``` + +--- + ### Image Configuration ```yaml diff --git a/docs/guides/product-team-site-management.md b/docs/guides/product-team-site-management.md index 0c408f3..7494fbf 100644 --- a/docs/guides/product-team-site-management.md +++ b/docs/guides/product-team-site-management.md @@ -243,6 +243,11 @@ spec: ```yaml spec: connect: + # Enable/disable Connect deployment (default: true) + # WARNING: Setting enabled: false permanently deletes all Connect data! + # See the Connect Configuration Guide for details. + enabled: true + image: "ghcr.io/posit-dev/connect:ubuntu22-2024.10.0" imagePullPolicy: IfNotPresent replicas: 1 diff --git a/flightdeck/html/home.go b/flightdeck/html/home.go index fc8194c..c4296bc 100644 --- a/flightdeck/html/home.go +++ b/flightdeck/html/home.go @@ -41,7 +41,9 @@ func HomePage(site positcov1beta1.Site, config *internal.ServerConfig) Node { "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), + // Check Enabled field explicitly for Connect - when Enabled=false, Connect should not appear + // even if other fields like License or Image are set + If((site.Spec.Connect.Enabled == nil || *site.Spec.Connect.Enabled == true) && !internal.IsEmptyStruct(site.Spec.Connect), 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."), diff --git a/flightdeck/html/siteconfig.go b/flightdeck/html/siteconfig.go index fac5d9a..8fc9d15 100644 --- a/flightdeck/html/siteconfig.go +++ b/flightdeck/html/siteconfig.go @@ -21,7 +21,8 @@ func SiteConfigTable(site positcov1beta1.Site) Node { TBody( If(!internal.IsEmptyStruct(site.Spec.Workbench), SiteConfigTableRow("Workbench Image", site.Spec.Workbench.Image)), - If(!internal.IsEmptyStruct(site.Spec.Connect), + // Check Enabled field explicitly for Connect - when Enabled=false, Connect should not appear + If((site.Spec.Connect.Enabled == nil || *site.Spec.Connect.Enabled == true) && !internal.IsEmptyStruct(site.Spec.Connect), SiteConfigTableRow("Connect Image", site.Spec.Connect.Image)), If(!internal.IsEmptyStruct(site.Spec.PackageManager), SiteConfigTableRow("Package Manager Image", site.Spec.PackageManager.Image)), @@ -45,7 +46,8 @@ func SiteConfigBlock(site positcov1beta1.Site) Node { if !internal.IsEmptyStruct(site.Spec.Workbench) { productConfigs["Workbench"] = site.Spec.Workbench } - if !internal.IsEmptyStruct(site.Spec.Connect) { + // Check Enabled field explicitly for Connect - when Enabled=false, Connect config should not appear + if (site.Spec.Connect.Enabled == nil || *site.Spec.Connect.Enabled == true) && !internal.IsEmptyStruct(site.Spec.Connect) { productConfigs["Connect"] = site.Spec.Connect } if !internal.IsEmptyStruct(site.Spec.PackageManager) { diff --git a/internal/controller/core/connect.go b/internal/controller/core/connect.go index 366a472..dddf190 100644 --- a/internal/controller/core/connect.go +++ b/internal/controller/core/connect.go @@ -831,6 +831,22 @@ func (r *ConnectReconciler) ensureDeployedService(ctx context.Context, req ctrl. return ctrl.Result{}, nil } +// CleanupConnect is the finalizer that runs when a Connect CRD is deleted. +// +// WARNING: This function performs DESTRUCTIVE cleanup operations that permanently destroy: +// - The Connect database via db.CleanupDatabase (drops the database if configured to do so) +// - All secrets: provisioning keys, database password secrets, etc. +// - All Kubernetes resources: deployments, services, ingress, PVCs, configmaps, etc. +// +// This finalizer is automatically triggered when: +// 1. The Site CR is deleted (complete teardown) +// 2. Connect is disabled via Site.Spec.Connect.Enabled=false (user disables the product) +// +// When a user sets Enabled=false, the site controller calls cleanupConnect() which deletes +// the Connect CRD, triggering this finalizer. This results in complete data loss. +// +// Re-enabling Connect after disabling it will start fresh with a new database, new secrets, +// and no previous content. This is intentional behavior to ensure clean resource teardown. func (r *ConnectReconciler) CleanupConnect(ctx context.Context, req ctrl.Request, c *positcov1beta1.Connect) (ctrl.Result, error) { if err := r.cleanupDeployedService(ctx, req, c); err != nil { return ctrl.Result{}, err diff --git a/internal/controller/core/site_controller.go b/internal/controller/core/site_controller.go index 4694045..c75cb50 100644 --- a/internal/controller/core/site_controller.go +++ b/internal/controller/core/site_controller.go @@ -177,8 +177,11 @@ func (r *SiteReconciler) reconcileResources(ctx context.Context, req ctrl.Reques return ctrl.Result{}, err } - if err := r.provisionFsxVolume(ctx, site, connectVolumeName, "connect", connectVolumeSize); err != nil { - return ctrl.Result{}, err + // Only provision Connect volume if Connect is enabled + if site.Spec.Connect.Enabled == nil || *site.Spec.Connect.Enabled == true { + if err := r.provisionFsxVolume(ctx, site, connectVolumeName, "connect", connectVolumeSize); err != nil { + return ctrl.Result{}, err + } } if err := r.provisionFsxVolume(ctx, site, devVolumeName, "workbench", connectVolumeSize); err != nil { @@ -209,10 +212,13 @@ func (r *SiteReconciler) reconcileResources(ctx context.Context, req ctrl.Reques return ctrl.Result{}, err } - connectStorageClassName = fmt.Sprintf("%s-nfs", connectVolumeName) + // Only provision Connect volume if Connect is enabled + if site.Spec.Connect.Enabled == nil || *site.Spec.Connect.Enabled == true { + connectStorageClassName = fmt.Sprintf("%s-nfs", connectVolumeName) - if err := r.provisionNfsVolume(ctx, site, connectVolumeName, "connect", connectStorageClassName, connectVolumeSize); err != nil { - return ctrl.Result{}, err + if err := r.provisionNfsVolume(ctx, site, connectVolumeName, "connect", connectStorageClassName, connectVolumeSize); err != nil { + return ctrl.Result{}, err + } } devStorageClassName = fmt.Sprintf("%s-nfs", devVolumeName) @@ -296,6 +302,8 @@ func (r *SiteReconciler) reconcileResources(ctx context.Context, req ctrl.Reques workbenchAdditionalVolumes = append(workbenchAdditionalVolumes, site.Spec.Workbench.AdditionalVolumes...) // CONNECT + // When Enabled is nil or true, reconcile Connect (create/update resources) + // When Enabled is false, cleanup Connect (DESTRUCTIVE: deletes database, secrets, and all resources) if site.Spec.Connect.Enabled == nil || *site.Spec.Connect.Enabled == true { if err := r.reconcileConnect( ctx, @@ -314,6 +322,9 @@ func (r *SiteReconciler) reconcileResources(ctx context.Context, req ctrl.Reques } } else { // Connect is disabled - clean up any existing Connect resources + // WARNING: This triggers permanent deletion of the Connect CRD, which causes + // the Connect finalizer to destroy the database, secrets, and all resources. + // See cleanupConnect() and CleanupConnect() for details. if err := r.cleanupConnect(ctx, req, l); err != nil { l.Error(err, "error cleaning up connect resources") return ctrl.Result{}, err diff --git a/internal/controller/core/site_controller_connect.go b/internal/controller/core/site_controller_connect.go index dd96225..31e543f 100644 --- a/internal/controller/core/site_controller_connect.go +++ b/internal/controller/core/site_controller_connect.go @@ -252,6 +252,21 @@ func (r *SiteReconciler) reconcileConnect( return nil } +// cleanupConnect deletes the Connect CRD when Connect is disabled (Enabled=false). +// +// WARNING: This is a DESTRUCTIVE operation. Deleting the Connect CRD triggers the Connect +// finalizer (CleanupConnect in connect_controller.go) which permanently destroys: +// - The Connect database and all its data +// - All secrets (database credentials, provisioning keys, etc.) +// - Persistent volumes and claims +// - All deployed Kubernetes resources +// +// This means that disabling Connect via Site.Spec.Connect.Enabled=false is a one-way +// operation that results in complete data loss. Re-enabling Connect will start fresh +// with a new database and no previous content or configuration. +// +// This behavior is intentional to ensure clean teardown of Connect resources when +// a user explicitly disables the product. func (r *SiteReconciler) cleanupConnect(ctx context.Context, req controllerruntime.Request, l logr.Logger) error { l = l.WithValues("event", "cleanup-connect") diff --git a/internal/controller/core/site_controller_workbench.go b/internal/controller/core/site_controller_workbench.go index e5ad5b3..009e238 100644 --- a/internal/controller/core/site_controller_workbench.go +++ b/internal/controller/core/site_controller_workbench.go @@ -160,12 +160,19 @@ 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, - getEffectiveBaseDomain(site.Spec.Connect.BaseDomain, site.Spec.Domain), - v1beta1.SiteSubDomain, - ), + // Only set DefaultRSConnectServer if Connect is enabled + // When Connect is disabled (Enabled=false), leave this empty so Workbench + // doesn't have a config entry pointing to a non-existent service + DefaultRSConnectServer: func() string { + if site.Spec.Connect.Enabled != nil && *site.Spec.Connect.Enabled == false { + return "" + } + return "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...? From 3eef1425513b2c071c48a8642d57ba598d698be2 Mon Sep 17 00:00:00 2001 From: Steve Nolen Date: Wed, 11 Feb 2026 10:16:53 -0500 Subject: [PATCH 5/6] forgot helm-generate! --- config/crd/bases/core.posit.team_sites.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/config/crd/bases/core.posit.team_sites.yaml b/config/crd/bases/core.posit.team_sites.yaml index e820e7a..1775b6d 100644 --- a/config/crd/bases/core.posit.team_sites.yaml +++ b/config/crd/bases/core.posit.team_sites.yaml @@ -194,6 +194,21 @@ spec: description: |- Enabled controls whether Connect is deployed. Defaults to true if not specified. Set to false to explicitly disable Connect deployment. + + WARNING: Disabling Connect (setting Enabled=false) is a DESTRUCTIVE operation that + permanently deletes all Connect resources including: + - The Connect database and all its data + - All secrets (database credentials, provisioning keys, etc.) + - Persistent volumes and claims + - All deployed Kubernetes resources (deployments, services, ingress, etc.) + + Re-enabling Connect after disabling it will start completely fresh with: + - A new, empty database + - New secrets and credentials + - No content or configuration from the previous deployment + + This is intentional behavior, not a bug. Only disable Connect if you intend to + permanently destroy the Connect instance and all its data. type: boolean experimentalFeatures: properties: From 69de6e1fb44815958948af86241c04a34278a3ae Mon Sep 17 00:00:00 2001 From: Steve Nolen Date: Wed, 11 Feb 2026 10:26:04 -0500 Subject: [PATCH 6/6] forgot helm-generate...and apparently committed before it was finished. --- .../templates/crd/core.posit.team_sites.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dist/chart/templates/crd/core.posit.team_sites.yaml b/dist/chart/templates/crd/core.posit.team_sites.yaml index 5d869f6..9d4bfe7 100755 --- a/dist/chart/templates/crd/core.posit.team_sites.yaml +++ b/dist/chart/templates/crd/core.posit.team_sites.yaml @@ -215,6 +215,21 @@ spec: description: |- Enabled controls whether Connect is deployed. Defaults to true if not specified. Set to false to explicitly disable Connect deployment. + + WARNING: Disabling Connect (setting Enabled=false) is a DESTRUCTIVE operation that + permanently deletes all Connect resources including: + - The Connect database and all its data + - All secrets (database credentials, provisioning keys, etc.) + - Persistent volumes and claims + - All deployed Kubernetes resources (deployments, services, ingress, etc.) + + Re-enabling Connect after disabling it will start completely fresh with: + - A new, empty database + - New secrets and credentials + - No content or configuration from the previous deployment + + This is intentional behavior, not a bug. Only disable Connect if you intend to + permanently destroy the Connect instance and all its data. type: boolean experimentalFeatures: properties: