Skip to content
Open
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
82 changes: 80 additions & 2 deletions api/v1/nebariapp_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ type NebariAppSpec struct {
Gateway string `json:"gateway,omitempty"`

// ServiceAccountName is the name of the Kubernetes ServiceAccount used by the
// app's pods. Used for RBAC scoping of OIDC secrets so only the app's pods
// can read its credentials. Defaults to the NebariApp's name if omitted.
// app's pods. Used for RBAC scoping of OIDC and database credentials secrets
// so only the app's pods can read them. Defaults to the NebariApp's name if omitted.
// +optional
// +kubebuilder:validation:MinLength=1
ServiceAccountName string `json:"serviceAccountName,omitempty"`
Expand All @@ -61,6 +61,21 @@ type NebariAppSpec struct {
// When enabled, the service will be discoverable through the landing page portal.
// +optional
LandingPage *LandingPageConfig `json:"landingPage,omitempty"`

// Database requests a managed PostgreSQL database for this application.
// When enabled, the operator provisions a CloudNativePG Cluster named
// "<name>-db" and writes connection credentials to the Secret
// "<name>-db-credentials" with keys: host, port, username, password,
// database, uri. Read access is scoped to the app's ServiceAccount.
//
// Requires the CloudNativePG operator on the cluster (in Nebari, NIC
// installs it on every GitOps bootstrap as foundational infrastructure).
//
// Disabling later stops management but never deletes the database.
// Deleting the NebariApp deletes the database and its data via owner
// references.
// +optional
Database *DatabaseConfig `json:"database,omitempty"`
}

// ServiceReference identifies the Kubernetes Service that backs this application.
Expand Down Expand Up @@ -478,6 +493,37 @@ type LandingPageConfig struct {
HealthCheck *HealthCheckConfig `json:"healthCheck,omitempty"`
}

// DatabaseConfig specifies a managed database request backed by a database
// operator (CloudNativePG).
type DatabaseConfig struct {
// Enabled determines whether a managed database is provisioned.
// +kubebuilder:default=false
// +optional
Enabled bool `json:"enabled,omitempty"`

// Provider selects the database operator backing this request.
// +kubebuilder:validation:Enum=cloudnativepg
// +kubebuilder:default=cloudnativepg
// +optional
Provider string `json:"provider,omitempty"`

// Instances is the number of PostgreSQL instances (1 primary plus N-1 replicas, maximum 9).
// The cap is a platform guardrail, not a CloudNativePG limit.
// +kubebuilder:default=1
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=9
// +optional
Instances int32 `json:"instances,omitempty"`

// Size is the storage request for each instance, as a Kubernetes quantity.
// Size cannot be decreased once set (CloudNativePG restriction).
// +kubebuilder:default="1Gi"
// +kubebuilder:validation:XValidation:rule="sign(quantity(self)) > 0",message="size must be greater than zero"
// +kubebuilder:validation:Pattern=`^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$`
// +optional
Size string `json:"size,omitempty"`
}

// HealthCheckConfig defines health check parameters for a service.
type HealthCheckConfig struct {
// Enabled determines if health checks are performed.
Expand Down Expand Up @@ -574,6 +620,7 @@ type NebariAppStatus struct {
// - "RoutingReady": HTTPRoute has been created and is functioning
// - "TLSReady": TLS certificate is available and configured
// - "AuthReady": Authentication policy is configured (if auth is enabled)
// - "DatabaseReady": Managed database is provisioned and credentials are available (if database is enabled)
// - "Ready": All components are ready (aggregate condition)
// +listType=map
// +listMapKey=type
Expand All @@ -598,6 +645,11 @@ type NebariAppStatus struct {
// +optional
ClientSecretRef *ResourceReference `json:"clientSecretRef,omitempty"`

// DatabaseSecretRef identifies the Secret containing database connection
// credentials (keys: host, port, username, password, database, uri).
// +optional
DatabaseSecretRef *ResourceReference `json:"databaseSecretRef,omitempty"`

// AuthConfigHash stores a SHA-256 hash of the last successfully provisioned OIDC
// client configuration. When this matches the hash of the current spec, and the
// AuthReady condition is True, ProvisionClient is skipped to avoid unnecessary
Expand Down Expand Up @@ -649,6 +701,10 @@ const (
// This includes the SecurityPolicy being created and the client secret being available.
ConditionTypeAuthReady = "AuthReady"

// ConditionTypeDatabaseReady indicates that the managed database is
// provisioned and its credentials Secret is available.
ConditionTypeDatabaseReady = "DatabaseReady"

// ConditionTypeReady is an aggregate condition indicating all components are ready.
ConditionTypeReady = "Ready"
)
Expand Down Expand Up @@ -685,6 +741,18 @@ const (
// ReasonCertificateNotReady indicates the cert-manager Certificate is not ready
ReasonCertificateNotReady = "CertificateNotReady"

// ReasonDatabaseProvisioning indicates the database cluster is being created
ReasonDatabaseProvisioning = "DatabaseProvisioning"

// ReasonDatabaseDisabled indicates database management was disabled while a
// previously provisioned database still exists (it is never deleted by a
// toggle; see the spec.database field documentation)
ReasonDatabaseDisabled = "DatabaseDisabled"

// ReasonCNPGNotInstalled indicates the CloudNativePG operator (and its CRDs)
// is not installed on this cluster
ReasonCNPGNotInstalled = "CNPGNotInstalled"

// ReasonGatewayListenerConflict indicates the Gateway listener conflicts with another NebariApp
ReasonGatewayListenerConflict = "GatewayListenerConflict"

Expand Down Expand Up @@ -748,6 +816,16 @@ const (
// EventReasonClientProvisionFailed is used when OIDC client provisioning fails
EventReasonClientProvisionFailed = "ClientProvisionFailed"

// EventReasonDatabaseProvisioned is used when the managed database becomes ready
EventReasonDatabaseProvisioned = "DatabaseProvisioned"

// EventReasonDatabaseProvisionFailed is used when database provisioning fails
EventReasonDatabaseProvisionFailed = "DatabaseProvisionFailed"

// EventReasonDatabaseOrphaned is used when database management is disabled
// while the database still exists
EventReasonDatabaseOrphaned = "DatabaseOrphaned"

// EventReasonSecurityPolicyCreated is used when SecurityPolicy is created
EventReasonSecurityPolicyCreated = "SecurityPolicyCreated"

Expand Down
25 changes: 25 additions & 0 deletions api/v1/zz_generated.deepcopy.go

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

26 changes: 19 additions & 7 deletions cmd/operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
_ "k8s.io/client-go/plugin/pkg/client/auth"

certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
cnpgv1 "github.com/cloudnative-pg/api/pkg/api/v1"
egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
Expand All @@ -44,6 +45,7 @@ import (
"github.com/nebari-dev/nebari-operator/internal/controller/reconcilers/auth"
"github.com/nebari-dev/nebari-operator/internal/controller/reconcilers/auth/providers"
"github.com/nebari-dev/nebari-operator/internal/controller/reconcilers/core"
"github.com/nebari-dev/nebari-operator/internal/controller/reconcilers/database"
"github.com/nebari-dev/nebari-operator/internal/controller/reconcilers/routing"
tlsreconciler "github.com/nebari-dev/nebari-operator/internal/controller/reconcilers/tls"
"github.com/nebari-dev/nebari-operator/internal/controller/utils/constants"
Expand All @@ -62,6 +64,10 @@ func init() {
utilruntime.Must(gatewayapiv1.Install(scheme))
utilruntime.Must(egv1alpha1.AddToScheme(scheme))
utilruntime.Must(certmanagerv1.AddToScheme(scheme))
// CNPG types are registered unconditionally: scheme registration is
// client-side and harmless when the CRD is absent; the database
// reconciler degrades to a condition at request time in that case.
utilruntime.Must(cnpgv1.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}

Expand Down Expand Up @@ -279,15 +285,21 @@ func main() {
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("nebariapp-routing"),
}
databaseReconciler := &database.DatabaseReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("nebariapp-database"),
}

if err := (&controller.NebariAppReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("nebariapp-controller"),
CoreReconciler: coreReconciler,
TLSReconciler: tlsReconciler,
RoutingReconciler: routingReconciler,
AuthReconciler: authReconciler,
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("nebariapp-controller"),
CoreReconciler: coreReconciler,
TLSReconciler: tlsReconciler,
RoutingReconciler: routingReconciler,
AuthReconciler: authReconciler,
DatabaseReconciler: databaseReconciler,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "NebariApp")
os.Exit(1)
Expand Down
66 changes: 64 additions & 2 deletions config/crd/bases/reconcilers.nebari.dev_nebariapps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,53 @@ spec:
rule: '!has(self.forwardAccessToken) || self.forwardAccessToken
== false || (has(self.enforceAtGateway) && self.enforceAtGateway
== true)'
database:
description: |-
Database requests a managed PostgreSQL database for this application.
When enabled, the operator provisions a CloudNativePG Cluster named
"<name>-db" and writes connection credentials to the Secret
"<name>-db-credentials" with keys: host, port, username, password,
database, uri. Read access is scoped to the app's ServiceAccount.

Requires the CloudNativePG operator on the cluster (in Nebari, NIC
installs it on every GitOps bootstrap as foundational infrastructure).

Disabling later stops management but never deletes the database.
Deleting the NebariApp deletes the database and its data via owner
references.
properties:
enabled:
default: false
description: Enabled determines whether a managed database is
provisioned.
type: boolean
instances:
default: 1
description: |-
Instances is the number of PostgreSQL instances (1 primary plus N-1 replicas, maximum 9).
The cap is a platform guardrail, not a CloudNativePG limit.
format: int32
maximum: 9
minimum: 1
type: integer
provider:
default: cloudnativepg
description: Provider selects the database operator backing this
request.
enum:
- cloudnativepg
type: string
size:
default: 1Gi
description: |-
Size is the storage request for each instance, as a Kubernetes quantity.
Size cannot be decreased once set (CloudNativePG restriction).
pattern: ^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$
type: string
x-kubernetes-validations:
- message: size must be greater than zero
rule: sign(quantity(self)) > 0
type: object
gateway:
default: public
description: |-
Expand Down Expand Up @@ -572,8 +619,8 @@ spec:
serviceAccountName:
description: |-
ServiceAccountName is the name of the Kubernetes ServiceAccount used by the
app's pods. Used for RBAC scoping of OIDC secrets so only the app's pods
can read its credentials. Defaults to the NebariApp's name if omitted.
app's pods. Used for RBAC scoping of OIDC and database credentials secrets
so only the app's pods can read them. Defaults to the NebariApp's name if omitted.
minLength: 1
type: string
required:
Expand Down Expand Up @@ -613,6 +660,7 @@ spec:
- "RoutingReady": HTTPRoute has been created and is functioning
- "TLSReady": TLS certificate is available and configured
- "AuthReady": Authentication policy is configured (if auth is enabled)
- "DatabaseReady": Managed database is provisioned and credentials are available (if database is enabled)
- "Ready": All components are ready (aggregate condition)
items:
description: Condition contains details for one aspect of the current
Expand Down Expand Up @@ -672,6 +720,20 @@ spec:
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
databaseSecretRef:
description: |-
DatabaseSecretRef identifies the Secret containing database connection
credentials (keys: host, port, username, password, database, uri).
properties:
name:
description: Name of the resource.
type: string
namespace:
description: Namespace of the resource (if namespaced).
type: string
required:
- name
type: object
gatewayRef:
description: GatewayRef identifies the Gateway resource that routes
traffic to this application.
Expand Down
11 changes: 11 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ rules:
- patch
- update
- watch
- apiGroups:
- postgresql.cnpg.io
resources:
- clusters
verbs:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- rbac.authorization.k8s.io
resources:
Expand Down
9 changes: 9 additions & 0 deletions config/samples/reconcilers_v1_nebariapp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ spec:
# id.token.claim: "true"
# access.token.claim: "true"
# userinfo.token.claim: "true"
# Optional: request a managed PostgreSQL database (CloudNativePG).
# Credentials land in Secret "<name>-db-credentials" with keys:
# host, port, username, password, database, uri.
# Disabling later never deletes the database; deleting this NebariApp does.
# database:
# enabled: true
# provider: cloudnativepg # default and only supported value
# instances: 1 # default
# size: 1Gi # default; cannot be decreased
---
# Example: Cross-namespace service reference
# Useful for centralized service architectures where services live in different namespaces
Expand Down
Loading