diff --git a/config/crds/authzed.com_spicedbclusters.yaml b/config/crds/authzed.com_spicedbclusters.yaml index ea2306ec..4f8dcf53 100644 --- a/config/crds/authzed.com_spicedbclusters.yaml +++ b/config/crds/authzed.com_spicedbclusters.yaml @@ -116,6 +116,29 @@ spec: identity, sidecar proxy). When true, SecretName and Key are ignored. type: boolean type: object + migrationDatastoreURI: + description: |- + MigrationDatastoreURI configures a separate connection string for migrations. + When set, migration jobs use this URI instead of DatastoreURI, enabling + least-privilege credentials for the application while using elevated + credentials only for migrations. + properties: + key: + description: |- + Key is the key within the Secret. Defaults to the standard SpiceDB key + name for this credential (datastore_uri or preshared_key) if omitted. + type: string + secretName: + description: SecretName is the name of the Kubernetes Secret + in the same namespace. + type: string + skip: + description: |- + Skip instructs the operator not to validate or inject this credential. + Use when the credential is provided externally (CSI driver, workload + identity, sidecar proxy). When true, SecretName and Key are ignored. + type: boolean + type: object migrationSecrets: description: MigrationSecrets configures the source for the migration secrets. diff --git a/pkg/apis/authzed/v1alpha1/types.go b/pkg/apis/authzed/v1alpha1/types.go index 39618e70..0b02ef3e 100644 --- a/pkg/apis/authzed/v1alpha1/types.go +++ b/pkg/apis/authzed/v1alpha1/types.go @@ -124,6 +124,13 @@ type ClusterCredentials struct { // MigrationSecrets configures the source for the migration secrets. // +optional MigrationSecrets *CredentialRef `json:"migrationSecrets,omitempty"` + + // MigrationDatastoreURI configures a separate connection string for migrations. + // When set, migration jobs use this URI instead of DatastoreURI, enabling + // least-privilege credentials for the application while using elevated + // credentials only for migrations. + // +optional + MigrationDatastoreURI *CredentialRef `json:"migrationDatastoreURI,omitempty"` } // CredentialRef describes where to read a single credential value. diff --git a/pkg/apis/authzed/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/authzed/v1alpha1/zz_generated.deepcopy.go index 7e2804fe..649e72aa 100644 --- a/pkg/apis/authzed/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/authzed/v1alpha1/zz_generated.deepcopy.go @@ -28,6 +28,11 @@ func (in *ClusterCredentials) DeepCopyInto(out *ClusterCredentials) { *out = new(CredentialRef) **out = **in } + if in.MigrationDatastoreURI != nil { + in, out := &in.MigrationDatastoreURI, &out.MigrationDatastoreURI + *out = new(CredentialRef) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterCredentials. diff --git a/pkg/config/config.go b/pkg/config/config.go index dcc80844..40c936c6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -176,6 +176,7 @@ type SpiceConfig struct { DatastoreURIRef ResolvedCredentialRef PresharedKeyRef ResolvedCredentialRef MigrationSecretsRef ResolvedCredentialRef + MigrationDatastoreURIRef ResolvedCredentialRef ExtraPodLabels map[string]string ExtraPodAnnotations map[string]string ExtraServiceAccountAnnotations map[string]string @@ -367,6 +368,24 @@ func NewConfig(cluster *v1alpha1.SpiceDBCluster, globalConfig *OperatorConfig, s Key: key, } } + + // Resolve MigrationDatastoreURI credential. + // If not specified, migrations use the same DatastoreURI. + if credentials.MigrationDatastoreURI == nil { + // Default: use the same datastore URI for migrations + spiceConfig.MigrationDatastoreURIRef = spiceConfig.DatastoreURIRef + } else if credentials.MigrationDatastoreURI.Skip { + spiceConfig.MigrationDatastoreURIRef = ResolvedCredentialRef{Skip: true} + } else { + key := credentials.MigrationDatastoreURI.Key + if key == "" { + key = "migration_datastore_uri" + } + spiceConfig.MigrationDatastoreURIRef = ResolvedCredentialRef{ + SecretName: credentials.MigrationDatastoreURI.SecretName, + Key: key, + } + } } if len(migrationConfig.SpannerCredsSecretRef) > 0 { @@ -752,9 +771,14 @@ func (c *Config) unpatchedMigrationJob(migrationHash string) *applybatchv1.JobAp envVars := []*applycorev1.EnvVarApplyConfiguration{ applycorev1.EnvVar().WithName(envPrefix + "_LOG_LEVEL").WithValue(c.MigrationLogLevel), } - if !c.DatastoreURIRef.Skip { + // Use MigrationDatastoreURIRef for migrations if set, otherwise fall back to DatastoreURIRef. + migrationURIRef := c.MigrationDatastoreURIRef + if migrationURIRef.SecretName == "" && !migrationURIRef.Skip { + migrationURIRef = c.DatastoreURIRef + } + if !migrationURIRef.Skip { envVars = append(envVars, - applycorev1.EnvVar().WithName(envPrefix+"_DATASTORE_CONN_URI").WithValueFrom(applycorev1.EnvVarSource().WithSecretKeyRef(applycorev1.SecretKeySelector().WithName(c.DatastoreURIRef.SecretName).WithKey(c.DatastoreURIRef.Key))), + applycorev1.EnvVar().WithName(envPrefix+"_DATASTORE_CONN_URI").WithValueFrom(applycorev1.EnvVarSource().WithSecretKeyRef(applycorev1.SecretKeySelector().WithName(migrationURIRef.SecretName).WithKey(migrationURIRef.Key))), ) } if !c.MigrationSecretsRef.Skip { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 1bd7cd2c..efa1a67d 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -170,6 +170,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -257,6 +258,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -342,6 +344,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -408,6 +411,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -476,6 +480,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -563,6 +568,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -650,6 +656,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -741,6 +748,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -835,6 +843,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -924,6 +933,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -1013,6 +1023,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -1104,6 +1115,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -1198,6 +1210,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -1289,6 +1302,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -1382,6 +1396,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -1473,6 +1488,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -1563,6 +1579,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -1654,6 +1671,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -1750,6 +1768,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -1850,6 +1869,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ @@ -1940,6 +1960,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ //nolint:gosec // this is a test @@ -2031,6 +2052,7 @@ func TestNewConfig(t *testing.T) { DatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, PresharedKeyRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "preshared_key"}, MigrationSecretsRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "migration_secrets"}, + MigrationDatastoreURIRef: ResolvedCredentialRef{SecretName: "test-secret", Key: "datastore_uri"}, ProjectLabels: true, ProjectAnnotations: true, Passthrough: map[string]string{ diff --git a/pkg/crds/authzed.com_spicedbclusters.yaml b/pkg/crds/authzed.com_spicedbclusters.yaml index ea2306ec..4f8dcf53 100644 --- a/pkg/crds/authzed.com_spicedbclusters.yaml +++ b/pkg/crds/authzed.com_spicedbclusters.yaml @@ -116,6 +116,29 @@ spec: identity, sidecar proxy). When true, SecretName and Key are ignored. type: boolean type: object + migrationDatastoreURI: + description: |- + MigrationDatastoreURI configures a separate connection string for migrations. + When set, migration jobs use this URI instead of DatastoreURI, enabling + least-privilege credentials for the application while using elevated + credentials only for migrations. + properties: + key: + description: |- + Key is the key within the Secret. Defaults to the standard SpiceDB key + name for this credential (datastore_uri or preshared_key) if omitted. + type: string + secretName: + description: SecretName is the name of the Kubernetes Secret + in the same namespace. + type: string + skip: + description: |- + Skip instructs the operator not to validate or inject this credential. + Use when the credential is provided externally (CSI driver, workload + identity, sidecar proxy). When true, SecretName and Key are ignored. + type: boolean + type: object migrationSecrets: description: MigrationSecrets configures the source for the migration secrets.