Allow sqlite3 in memory when kind is deployment (#923)

* Allow sqlite3 in memory when kind is deployment

Signed-off-by: Daniel Schlatter <[email protected]>

* Warn on unsafe in-memory datastore combinations

Signed-off-by: Daniel Schlatter <[email protected]>

---------

Signed-off-by: Daniel Schlatter <[email protected]>
This commit is contained in:
Daniel Schlatter
2026-08-18 14:22:50 -07:00
committed by GitHub
parent e46ad1594a
commit 59bb8a774c
6 changed files with 236 additions and 4 deletions
+2 -1
View File
@@ -142,7 +142,8 @@ In order to run Tornjak with simple HTTP Connection only, make sure you don't cr
| `dataStore.sql.port` | If 0 (default), it will auto set to 5432 for postgres and 3306 for mysql. Only used by those databases. | `0` | | `dataStore.sql.port` | If 0 (default), it will auto set to 5432 for postgres and 3306 for mysql. Only used by those databases. | `0` |
| `dataStore.sql.username` | Only used when type != "sqlite3" | `spire` | | `dataStore.sql.username` | Only used when type != "sqlite3" | `spire` |
| `dataStore.sql.password` | Only used when type != "sqlite3" | `""` | | `dataStore.sql.password` | Only used when type != "sqlite3" | `""` |
| `dataStore.sql.file` | Data source file. Only used when type == "sqlite3" | `/run/spire/data/datastore.sqlite3` | | `dataStore.sql.file` | Data source file. Only used when type == "sqlite3" and inMemory is false | `/run/spire/data/datastore.sqlite3` |
| `dataStore.sql.inMemory` | Hold the sqlite3 datastore in memory instead of in a file, in which case `file` is unused. The datastore starts empty on every restart, so this only suits a single replica whose registration entries are recreated at startup, for example by the controller manager writing static entries. Required to run as a deployment on sqlite3, since a deployment has no durable per-pod storage. | `false` |
| `dataStore.sql.options` | takes an array of objects of form {<key>: <value>} to use when building the database connection string | `[]` | | `dataStore.sql.options` | takes an array of objects of form {<key>: <value>} to use when building the database connection string | `[]` |
| `dataStore.sql.rootCAPath` | Path to Root CA bundle (MySQL only) | `""` | | `dataStore.sql.rootCAPath` | Path to Root CA bundle (MySQL only) | `""` |
| `dataStore.sql.clientCertPath` | Path to client certificate (MySQL only) | `""` | | `dataStore.sql.clientCertPath` | Path to client certificate (MySQL only) | `""` |
@@ -307,8 +307,15 @@ current-context: cluster
{{- $ropw := "" }} {{- $ropw := "" }}
{{- if eq .Values.dataStore.sql.databaseType "sqlite3" }} {{- if eq .Values.dataStore.sql.databaseType "sqlite3" }}
{{- $_ := set $config "database_type" "sqlite3" }} {{- $_ := set $config "database_type" "sqlite3" }}
{{- if .Values.dataStore.sql.inMemory }}
{{- /* cache=shared is not optional: without it every pooled connection opens its own
empty database, so the server silently loses every write it did not make itself. */}}
{{- $query := include "spire-server.config-sqlite-query" (concat (list (dict "mode" "memory") (dict "cache" "shared")) .Values.dataStore.sql.options) }}
{{- $_ := set $config "connection_string" (printf "memdb%s" $query) }}
{{- else }}
{{- $query := include "spire-server.config-sqlite-query" .Values.dataStore.sql.options }} {{- $query := include "spire-server.config-sqlite-query" .Values.dataStore.sql.options }}
{{- $_ := set $config "connection_string" (printf "%s%s" .Values.dataStore.sql.file $query) }} {{- $_ := set $config "connection_string" (printf "%s%s" .Values.dataStore.sql.file $query) }}
{{- end }}
{{- else if or (eq .Values.dataStore.sql.databaseType "mysql") (eq .Values.dataStore.sql.databaseType "aws_mysql") (eq .Values.dataStore.sql.databaseType "gcp_mysql_sa_iam") }} {{- else if or (eq .Values.dataStore.sql.databaseType "mysql") (eq .Values.dataStore.sql.databaseType "aws_mysql") (eq .Values.dataStore.sql.databaseType "gcp_mysql_sa_iam") }}
{{- if eq .Values.dataStore.sql.databaseType "mysql" }} {{- if eq .Values.dataStore.sql.databaseType "mysql" }}
{{- $_ := set $config "database_type" "mysql" }} {{- $_ := set $config "database_type" "mysql" }}
@@ -41,8 +41,14 @@
{{- if (has .Values.persistence.type (list "pvc" "hostPath")) }} {{- if (has .Values.persistence.type (list "pvc" "hostPath")) }}
{{- fail "When running as deployment, persistence can't be set. 'persistence.type' must be [\"emptyDir\"]" }} {{- fail "When running as deployment, persistence can't be set. 'persistence.type' must be [\"emptyDir\"]" }}
{{- end }} {{- end }}
{{- if (eq .Values.dataStore.sql.databaseType "sqlite3") }} {{- if and (eq .Values.dataStore.sql.databaseType "sqlite3") (not .Values.dataStore.sql.inMemory) }}
{{- fail "When running as deployment, sqlite3 can't be used." }} {{- fail "When running as deployment, sqlite3 can only be used in memory. Set 'dataStore.sql.inMemory' to true." }}
{{- end }}
{{- if and (eq .Values.dataStore.sql.databaseType "sqlite3") .Values.dataStore.sql.inMemory }}
{{- $surge := dig "rollingUpdate" "maxSurge" "" .Values.updateStrategy | toString }}
{{- if not (or (eq (dig "type" "" .Values.updateStrategy) "Recreate") (eq $surge "0") (eq $surge "0%")) }}
{{- fail "An in-memory datastore on a deployment must not surge, or two servers run with separate datastores. Set 'updateStrategy' to {type: Recreate} or {rollingUpdate: {maxSurge: 0}}." }}
{{- end }}
{{- end }} {{- end }}
{{- if (eq (.Values.keyManager.disk.enabled | toString) "true") }} {{- if (eq (.Values.keyManager.disk.enabled | toString) "true") }}
{{- fail "When running as deployment, disk keymanager can't be used. 'keyManager.disk.enabled' must be false." }} {{- fail "When running as deployment, disk keymanager can't be used. 'keyManager.disk.enabled' must be false." }}
+3 -1
View File
@@ -196,8 +196,10 @@ dataStore:
username: spire username: spire
## @param dataStore.sql.password Only used when type != "sqlite3" ## @param dataStore.sql.password Only used when type != "sqlite3"
password: "" password: ""
## @param dataStore.sql.file Data source file. Only used when type == "sqlite3" ## @param dataStore.sql.file Data source file. Only used when type == "sqlite3" and inMemory is false
file: "/run/spire/data/datastore.sqlite3" file: "/run/spire/data/datastore.sqlite3"
## @param dataStore.sql.inMemory Hold the sqlite3 datastore in memory instead of in a file, in which case `file` is unused. The datastore starts empty on every restart, so this only suits a single replica whose registration entries are recreated at startup, for example by the controller manager writing static entries. Required to run as a deployment on sqlite3, since a deployment has no durable per-pod storage.
inMemory: false
## @param dataStore.sql.options [array] takes an array of objects of form {<key>: <value>} to use when building the database connection string ## @param dataStore.sql.options [array] takes an array of objects of form {<key>: <value>} to use when building the database connection string
options: [] options: []
+18
View File
@@ -22,6 +22,24 @@ Warning: You're using an unsupported plugin. Functionality of this release and f
Warning: You're using an experimental config. Functionality of this release and future upgrades aren't guaranteed to work smoothly. Warning: You're using an experimental config. Functionality of this release and future upgrades aren't guaranteed to work smoothly.
{{- end }} {{- end }}
{{- if (index .Values "spire-server").enabled }} {{- if (index .Values "spire-server").enabled }}
{{- $ss := index .Values "spire-server" }}
{{- if and (eq $ss.dataStore.sql.databaseType "sqlite3") $ss.dataStore.sql.inMemory }}
{{- $upstream := false }}
{{- range $name, $cfg := $ss.upstreamAuthority }}
{{- if kindIs "map" $cfg }}{{ if eq ($cfg.enabled | toString) "true" }}{{ $upstream = true }}{{ end }}{{ end }}
{{- end }}
{{- $reconciled := and $ss.controllerManager.enabled (or $ss.controllerManager.reconcile.clusterSPIFFEIDs $ss.controllerManager.reconcile.clusterStaticEntries) }}
{{- if not $reconciled }}
Warning: dataStore.sql.inMemory is set, but no controller manager reconciler is enabled. Registration entries live only in memory and nothing recreates them, so every entry is lost when the server restarts. Enable controllerManager with reconcile.clusterSPIFFEIDs or reconcile.clusterStaticEntries.
{{- end }}
{{- if and $ss.keyManager.memory.enabled (not $upstream) }}
Warning: dataStore.sql.inMemory is set with keyManager.memory and no upstreamAuthority. The server mints a new CA on every restart, so the whole trust domain has to re-attest and previously issued SVIDs stop verifying. Suitable for testing only; configure a KMS key manager or an upstream authority for anything else.
{{- end }}
{{- end }}
{{- end }}
{{- if (index .Values "spire-server").enabled }}
{{- $className := include "spire-server.controller-manager-class-name" (dict "Values" (index .Values "spire-server") "Release" .Release) }} {{- $className := include "spire-server.controller-manager-class-name" (dict "Values" (index .Values "spire-server") "Release" .Release) }}
{{- if (index .Values "spire-server").controllerManager.enabled }} {{- if (index .Values "spire-server").controllerManager.enabled }}
{{- if (index .Values "spire-server").controllerManager.watchClassless }} {{- if (index .Values "spire-server").controllerManager.watchClassless }}
+198
View File
@@ -428,4 +428,202 @@ spire-server:
Expect(serverResource).ShouldNot(ContainSubstring("\n updateStrategy:")) Expect(serverResource).ShouldNot(ContainSubstring("\n updateStrategy:"))
}) })
}) })
Describe("spire-server.kind.deployment.sqlite3", func() {
deployment := func(sql string) string {
return `
spire-server:
kind: deployment
persistence:
type: emptyDir
keyManager:
disk:
enabled: false
memory:
enabled: true
updateStrategy:
type: Recreate
dataStore:
sql:
` + sql
}
It("renders a Deployment when the sqlite3 datastore is in memory", func() {
objs, err := ValueStringRender(chart, deployment(` inMemory: true
`))
Expect(err).Should(Succeed())
serverResource := objs["spire/charts/spire-server/templates/server-resource.yaml"]
Expect(serverResource).Should(ContainSubstring("kind: Deployment"))
Expect(serverResource).ShouldNot(ContainSubstring("kind: StatefulSet"))
})
It("rejects a file backed sqlite3 datastore", func() {
_, err := ValueStringRender(chart, deployment(` inMemory: false
`))
Expect(err).Should(MatchError(ContainSubstring("sqlite3 can only be used in memory")))
})
})
Describe("spire-server.dataStore.sql.inMemory", func() {
It("builds a shared cache connection string and ignores file", func() {
objs, err := ValueStringRender(chart, `
spire-server:
dataStore:
sql:
inMemory: true
file: /run/spire/data/datastore.sqlite3
`)
Expect(err).Should(Succeed())
Expect(objs["spire/charts/spire-server/templates/configmap.yaml"]).
Should(ContainSubstring(`"connection_string": "memdb?mode=memory\u0026cache=shared"`))
})
It("keeps the file connection string when left off", func() {
objs, err := ValueStringRender(chart, `
spire-server:
dataStore:
sql:
file: /run/spire/data/datastore.sqlite3
`)
Expect(err).Should(Succeed())
Expect(objs["spire/charts/spire-server/templates/configmap.yaml"]).
Should(ContainSubstring(`"connection_string": "/run/spire/data/datastore.sqlite3"`))
})
})
Describe("spire-server.dataStore.sql.inMemory warnings", func() {
notes := func(values string) string {
objs, err := ValueStringRender(chart, values)
ExpectWithOffset(1, err).Should(Succeed())
return objs["spire/templates/NOTES.txt"]
}
safe := `
spire-server:
dataStore:
sql:
inMemory: true
controllerManager:
enabled: true
reconcile:
clusterStaticEntries: true
upstreamAuthority:
vault:
enabled: true
`
It("stays quiet on the default values", func() {
Expect(notes(`spire-server: {}`)).ShouldNot(ContainSubstring("Warning: dataStore.sql.inMemory"))
})
It("stays quiet when entries are reconciled and a CA is upstream", func() {
Expect(notes(safe)).ShouldNot(ContainSubstring("Warning: dataStore.sql.inMemory"))
})
It("warns when nothing recreates the registration entries", func() {
Expect(notes(`
spire-server:
dataStore:
sql:
inMemory: true
controllerManager:
enabled: false
`)).Should(ContainSubstring("nothing recreates them"))
})
It("warns when the CA is also in memory with no upstream authority", func() {
Expect(notes(`
spire-server:
dataStore:
sql:
inMemory: true
controllerManager:
enabled: true
reconcile:
clusterStaticEntries: true
keyManager:
disk:
enabled: false
memory:
enabled: true
`)).Should(ContainSubstring("mints a new CA on every restart"))
})
It("stays quiet on a deployment that cannot surge", func() {
Expect(notes(safe + `
kind: deployment
persistence:
type: emptyDir
keyManager:
disk:
enabled: false
memory:
enabled: true
updateStrategy:
type: Recreate
`)).ShouldNot(ContainSubstring("Warning: dataStore.sql.inMemory"))
})
})
Describe("spire-server.updateStrategy surge guard", func() {
deployment := func(strategy string) string {
return `
spire-server:
kind: deployment
persistence:
type: emptyDir
keyManager:
disk:
enabled: false
memory:
enabled: true
dataStore:
sql:
inMemory: true
` + strategy
}
It("rejects an in-memory deployment that can surge", func() {
_, err := ValueStringRender(chart, deployment(``))
Expect(err).Should(MatchError(ContainSubstring("must not surge")))
})
It("rejects an explicit rolling update that can surge", func() {
_, err := ValueStringRender(chart, deployment(` updateStrategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
`))
Expect(err).Should(MatchError(ContainSubstring("must not surge")))
})
It("accepts Recreate", func() {
_, err := ValueStringRender(chart, deployment(` updateStrategy:
type: Recreate
`))
Expect(err).Should(Succeed())
})
It("accepts a rolling update pinned to maxSurge 0", func() {
_, err := ValueStringRender(chart, deployment(` updateStrategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
`))
Expect(err).Should(Succeed())
})
It("accepts maxSurge expressed as a percentage", func() {
_, err := ValueStringRender(chart, deployment(` updateStrategy:
rollingUpdate:
maxSurge: 0%
`))
Expect(err).Should(Succeed())
})
It("leaves a file backed statefulset alone", func() {
_, err := ValueStringRender(chart, `
spire-server:
updateStrategy:
type: RollingUpdate
`)
Expect(err).Should(Succeed())
})
})
}) })