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
19 changes: 19 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# SpiceDB Operator Examples

This directory contains examples of how to configure and deploy SpiceDB clusters using the SpiceDB operator.

## Examples

- [cockroachdb-tls-ingress](cockroachdb-tls-ingress/) - Production-ready setup with CockroachDB, TLS, and ingress
- [alternative-registry](alternative-registry/) - Using a private or alternative container registry for SpiceDB images
- [separate-migration-datastore-uri](separate-migration-datastore-uri/) - Using separate database credentials for migrations vs runtime

## Getting Started

Each example includes:

- A README with detailed explanations
- YAML manifests that can be applied to your cluster
- Configuration best practices for the specific use case

Choose an example that matches your needs and follow the instructions in its README.
67 changes: 67 additions & 0 deletions examples/separate-migration-datastore-uri/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Separate Migration Datastore URI

This example demonstrates how to use separate database connection strings for migrations versus normal SpiceDB operation.

## Overview

The SpiceDB operator supports using different database credentials for:

- **Migrations**: Often require elevated privileges (CREATE TABLE, DROP TABLE, ALTER TABLE)
- **Application Runtime**: Should use least-privilege credentials (SELECT, INSERT, UPDATE, DELETE)

This separation follows security best practices by ensuring the SpiceDB application pods don't have unnecessary database permissions.

## How it Works

When the operator detects a `migration_datastore_uri` key in the secret, it will:

1. Use `migration_datastore_uri` for migration jobs
2. Continue using `datastore_uri` for the SpiceDB application pods

## Example Configuration

See [spicedb-cluster.yaml](spicedb-cluster.yaml) for the complete example.

The key part is the secret configuration:

```yaml
apiVersion: v1
kind: Secret
metadata:
name: spicedb-config
stringData:
# Used by SpiceDB application pods - limited privileges
datastore_uri: "postgresql://spicedb_user:password@postgres:5432/spicedb?sslmode=require"

# Used by migration jobs - elevated privileges
migration_datastore_uri: "postgresql://spicedb_admin:admin_password@postgres:5432/spicedb?sslmode=require"

preshared_key: "your-secure-preshared-key"
```
## Database User Setup
Here's an example of how to set up the PostgreSQL users:
```sql
-- Create the admin user (for migrations)
CREATE USER spicedb_admin WITH PASSWORD 'admin_password';
GRANT CREATE ON DATABASE spicedb TO spicedb_admin;
GRANT ALL PRIVILEGES ON SCHEMA public TO spicedb_admin;

-- Create the application user (for runtime)
CREATE USER spicedb_user WITH PASSWORD 'password';

-- After migrations are run, grant appropriate permissions
-- The migration will create the tables, then you can run:
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO spicedb_user;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO spicedb_user;
```

## Security Considerations

- Store credentials in a proper secret management system
- Use strong, unique passwords for both users
- Consider using SSL/TLS for database connections
- Regularly rotate credentials
- Monitor database access logs
36 changes: 36 additions & 0 deletions examples/separate-migration-datastore-uri/spicedb-cluster.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
apiVersion: v1
kind: Namespace
metadata:
name: spicedb-separate-migration
---
apiVersion: authzed.com/v1alpha1
kind: SpiceDBCluster
metadata:
name: example-separate-migration
namespace: spicedb-separate-migration
spec:
# Number of SpiceDB replicas
replicas: 3

config:
datastoreEngine: postgres
logLevel: info

secretName: spicedb-config
---
apiVersion: v1
kind: Secret
metadata:
name: spicedb-config
namespace: spicedb-separate-migration
stringData:
# Application datastore URI - used by SpiceDB pods
# This user should have limited privileges (SELECT, INSERT, UPDATE, DELETE)
datastore_uri: "postgresql://CHANGE-ME-APP-USER:CHANGE-ME-APP-PASSWORD@CHANGE-ME-POSTGRES-HOST:5432/CHANGE-ME-DATABASE?sslmode=require"

# Migration datastore URI - used only by migration jobs
# This user needs elevated privileges (CREATE, DROP, ALTER tables)
migration_datastore_uri: "postgresql://CHANGE-ME-ADMIN-USER:CHANGE-ME-ADMIN-PASSWORD@CHANGE-ME-POSTGRES-HOST:5432/CHANGE-ME-DATABASE?sslmode=require"

# Shared preshared key for API authentication
preshared_key: "CHANGE-ME-TO-A-VERY-SECRET-PRESHARED-KEY"
40 changes: 28 additions & 12 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,18 @@ type Config struct {
// MigrationConfig stores data that is relevant for running migrations
// or deciding if migrations need to be run
type MigrationConfig struct {
TargetMigration string
TargetPhase string
MigrationLogLevel string
DatastoreEngine string
DatastoreURI string
SpannerCredsSecretRef string
TargetSpiceDBImage string
EnvPrefix string
SpiceDBCmd string
DatastoreTLSSecretName string
SpiceDBVersion *v1alpha1.SpiceDBVersion
TargetMigration string
TargetPhase string
MigrationLogLevel string
DatastoreEngine string
DatastoreURI string
HasMigrationDatastoreURI bool
SpannerCredsSecretRef string
TargetSpiceDBImage string
EnvPrefix string
SpiceDBCmd string
DatastoreTLSSecretName string
SpiceDBVersion *v1alpha1.SpiceDBVersion
}

// SpiceConfig contains config relevant to running spicedb or determining
Expand Down Expand Up @@ -286,6 +287,14 @@ func NewConfig(cluster *v1alpha1.SpiceDBCluster, globalConfig *OperatorConfig, s
errs = append(errs, fmt.Errorf("secret must contain a datastore_uri field"))
}
migrationConfig.DatastoreURI = string(datastoreURI)

// Check for separate migration datastore URI
if migrationURI, ok := secret.Data["migration_datastore_uri"]; ok && len(migrationURI) > 0 {
migrationConfig.HasMigrationDatastoreURI = true
// Note: We don't store the actual URI in the config for security reasons
// It will be read from the secret by the migration job
}

psk, ok = secret.Data["preshared_key"]
if !ok {
errs = append(errs, fmt.Errorf("secret must contain a preshared_key field"))
Expand Down Expand Up @@ -666,9 +675,16 @@ func (c *Config) jobName(migrationHash string) string {

func (c *Config) unpatchedMigrationJob(migrationHash string) *applybatchv1.JobApplyConfiguration {
envPrefix := c.SpiceConfig.EnvPrefix

// Use migration_datastore_uri if present, otherwise fall back to datastore_uri
datastoreURIKey := "datastore_uri"
if c.HasMigrationDatastoreURI {
datastoreURIKey = "migration_datastore_uri"
}

envVars := []*applycorev1.EnvVarApplyConfiguration{
applycorev1.EnvVar().WithName(envPrefix + "_LOG_LEVEL").WithValue(c.MigrationLogLevel),
applycorev1.EnvVar().WithName(envPrefix + "_DATASTORE_CONN_URI").WithValueFrom(applycorev1.EnvVarSource().WithSecretKeyRef(applycorev1.SecretKeySelector().WithName(c.SecretName).WithKey("datastore_uri"))),
applycorev1.EnvVar().WithName(envPrefix + "_DATASTORE_CONN_URI").WithValueFrom(applycorev1.EnvVarSource().WithSecretKeyRef(applycorev1.SecretKeySelector().WithName(c.SecretName).WithKey(datastoreURIKey))),
applycorev1.EnvVar().WithName(envPrefix + "_SECRETS").WithValueFrom(applycorev1.EnvVarSource().WithSecretKeyRef(applycorev1.SecretKeySelector().WithName(c.SecretName).WithKey("migration_secrets").WithOptional(true))),
}

Expand Down
127 changes: 126 additions & 1 deletion pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1388,6 +1388,91 @@ func TestNewConfig(t *testing.T) {
},
wantPortCount: 4,
},
{
name: "separate migration datastore URI",
args: args{
cluster: v1alpha1.ClusterSpec{Config: json.RawMessage(`
{
"logLevel": "info",
"datastoreEngine": "cockroachdb"
}
`)},
globalConfig: OperatorConfig{
ImageName: "image",
UpdateGraph: updates.UpdateGraph{
Channels: []updates.Channel{
{
Name: "cockroachdb",
Metadata: map[string]string{"datastore": "cockroachdb", "default": "true"},
Nodes: []updates.State{
{ID: "v1", Tag: "v1"},
},
Edges: map[string][]string{"v1": {}},
},
},
},
},
secret: &corev1.Secret{Data: map[string][]byte{
"datastore_uri": []byte("uri"),
"migration_datastore_uri": []byte("migration-uri"),
"preshared_key": []byte("psk"),
}},
},
wantWarnings: []error{fmt.Errorf("no TLS configured, consider setting \"tlsSecretName\"")},
want: &Config{
MigrationConfig: MigrationConfig{
MigrationLogLevel: "debug",
DatastoreEngine: "cockroachdb",
DatastoreURI: "uri",
HasMigrationDatastoreURI: true,
SpannerCredsSecretRef: "",
TargetSpiceDBImage: "image:v1",
EnvPrefix: "SPICEDB",
SpiceDBCmd: "spicedb",
DatastoreTLSSecretName: "",
TargetMigration: "head",
SpiceDBVersion: &v1alpha1.SpiceDBVersion{
Name: "v1",
Channel: "cockroachdb",
Attributes: []v1alpha1.SpiceDBVersionAttributes{
v1alpha1.SpiceDBVersionAttributesMigration,
},
},
},
SpiceConfig: SpiceConfig{
LogLevel: "info",
SkipMigrations: false,
Name: "test",
Namespace: "test",
UID: "1",
Replicas: 2,
PresharedKey: "psk",
EnvPrefix: "SPICEDB",
SpiceDBCmd: "spicedb",
ServiceAccountName: "test",
DispatchEnabled: true,
DispatchUpstreamCASecretPath: "tls.crt",
ProjectLabels: true,
ProjectAnnotations: true,
Passthrough: map[string]string{
"datastoreEngine": "cockroachdb",
"dispatchClusterEnabled": "true",
"terminationLogPath": "/dev/termination-log",
},
},
},
wantEnvs: []string{
"SPICEDB_POD_NAME=FIELD_REF=metadata.name",
"SPICEDB_LOG_LEVEL=info",
"SPICEDB_GRPC_PRESHARED_KEY=preshared_key",
"SPICEDB_DATASTORE_CONN_URI=datastore_uri",
"SPICEDB_DISPATCH_UPSTREAM_ADDR=kubernetes:///test.test:dispatch",
"SPICEDB_DATASTORE_ENGINE=cockroachdb",
"SPICEDB_DISPATCH_CLUSTER_ENABLED=true",
"SPICEDB_TERMINATION_LOG_PATH=/dev/termination-log",
},
wantPortCount: 4,
},
{
name: "disable dispatch",
args: args{
Expand Down Expand Up @@ -2341,13 +2426,32 @@ metadata:
},
wantJob: expectedJob(),
},
{
name: "uses migration datastore URI when present",
cluster: v1alpha1.ClusterSpec{
Config: json.RawMessage(`
{
"logLevel": "debug",
"datastoreEngine": "cockroachdb"
}
`),
},
wantJob: expectedJob(func(_ *applybatchv1.JobApplyConfiguration) {
// The test below will verify the correct env var is set
}),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secret := &corev1.Secret{Data: map[string][]byte{
"datastore_uri": []byte("uri"),
"preshared_key": []byte("psk"),
}}

// Add migration_datastore_uri for specific test
if tt.name == "uses migration datastore URI when present" {
secret.Data["migration_datastore_uri"] = []byte("migration-uri")
}
cluster := &v1alpha1.SpiceDBCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Expand All @@ -2364,7 +2468,28 @@ metadata:
gotJob, err := json.Marshal(got.MigrationJob("1"))
require.NoError(t, err)

require.JSONEq(t, string(wantJob), string(gotJob))
// For migration datastore URI test, we check the env var specifically
if tt.name == "uses migration datastore URI when present" {
// Verify the migration job uses migration_datastore_uri key
job := got.MigrationJob("1")
containers := job.Spec.Template.Spec.Containers
require.Len(t, containers, 1)

var foundDatastoreURI bool
for _, env := range containers[0].Env {
if env.Name != nil && *env.Name == "SPICEDB_DATASTORE_CONN_URI" {
foundDatastoreURI = true
require.NotNil(t, env.ValueFrom)
require.NotNil(t, env.ValueFrom.SecretKeyRef)
require.NotNil(t, env.ValueFrom.SecretKeyRef.Key)
require.Equal(t, "migration_datastore_uri", *env.ValueFrom.SecretKeyRef.Key)
}
}
require.True(t, foundDatastoreURI, "SPICEDB_DATASTORE_CONN_URI env var not found")
} else {
// For other tests, use the original JSONEq assertion
require.JSONEq(t, string(wantJob), string(gotJob))
}
})
}
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/controller/validate_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ func TestValidateConfigHandler(t *testing.T) {
Status: v1alpha1.ClusterStatus{
Image: "image:v1",
Migration: "head",
TargetMigrationHash: "69066f71d9cf4a1c",
CurrentMigrationHash: "69066f71d9cf4a1c",
TargetMigrationHash: "d6d2c3e587329b6e",
CurrentMigrationHash: "d6d2c3e587329b6e",
CurrentVersion: &v1alpha1.SpiceDBVersion{
Name: "v1",
Channel: "cockroachdb",
Expand Down
Loading