mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Cluster: Change "photoprism_" database / user prefix to "cluster_" #98
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -74,6 +74,7 @@ Thumbs.db
|
||||
.heartbeat
|
||||
.idea
|
||||
.codex
|
||||
.config
|
||||
.local
|
||||
*~
|
||||
.goutputstream*
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -77,6 +77,7 @@ Thumbs.db
|
||||
.glide
|
||||
.idea
|
||||
.codex
|
||||
.config
|
||||
.local
|
||||
.project
|
||||
.vscode
|
||||
|
||||
@@ -426,6 +426,6 @@ Note: Across our public documentation, official images, and in production, the c
|
||||
- Theme endpoint: `GET /api/v1/cluster/theme` streams a zip from `conf.ThemePath()`; only reinstall when `app.js` is missing and always use the header helpers in `pkg/http/header`.
|
||||
- Registration flow: send `rotate=true` only for MySQL/MariaDB nodes without credentials, treat 401/403/404 as terminal, include `ClientID` + `ClientSecret` when renaming an existing node, and persist only newly generated secrets or DB settings.
|
||||
- Registry & DTOs: use the client-backed registry (`NewClientRegistryWithConfig`)—the file-backed version is legacy—and treat migration as complete only after swapping callsites, building, and running focused API/CLI tests. Nodes are keyed by UUID v7 (`/api/v1/cluster/nodes/{uuid}`), the registry interface stays UUID-first (`Get`, `FindByNodeUUID`, `FindByClientID`, `RotateSecret`, `DeleteAllByUUID`), CLI lookups resolve `uuid → ClientID → name`, and DTOs normalize `Database.{Name,User,Driver,RotatedAt}` while exposing `ClientSecret` only during creation/rotation. `nodes rm --all-ids` cleans duplicate client rows, admin responses may include `AdvertiseUrl`/`Database`, client/user sessions stay redacted, registry files live under `conf.PortalConfigPath()/nodes/` (mode 0600), and `ClientData` no longer stores `NodeUUID`.
|
||||
- Provisioner & DSN: database/user names use UUID-based HMACs (`photoprism_d<hmac11>`, `photoprism_u<hmac11>`); `BuildDSN` accepts a `driver` but falls back to MySQL format with a warning when unsupported.
|
||||
- Provisioner & DSN: database/user names use UUID-based HMACs (`cluster_d<hmac11>`, `cluster_u<hmac11>`); `BuildDSN` accepts a `driver` but falls back to MySQL format with a warning when unsupported.
|
||||
- If we add Postgres provisioning support, extend `BuildDSN` and `provisioner.DatabaseDriver` handling, add validations, and return `driver=postgres` consistently in API/CLI.
|
||||
- Testing: exercise Portal endpoints with `httptest`, guard extraction paths with `pkg/fs.Unzip` size caps, and expect admin-only fields to disappear when authenticated as a client/user session.
|
||||
|
||||
@@ -103,7 +103,7 @@ Background Workers
|
||||
- Auto indexer: `internal/workers/auto/*`.
|
||||
|
||||
Cluster / Portal
|
||||
- Node types: `internal/service/cluster/const.go` (`cluster.RoleInstance`, `cluster.RolePortal`, `cluster.RoleService`).
|
||||
- Node types: `internal/service/cluster/const.go` (`cluster.RoleApp`, `cluster.RolePortal`, `cluster.RoleService`).
|
||||
- Node bootstrap & registration: `internal/service/cluster/node/*` (HTTP to Portal; do not import Portal internals).
|
||||
- Registration now retries once on 401/403 by rotating the node client secret with the join token and persists the new credentials (falling back to in-memory storage if the secrets directory is read-only).
|
||||
- Theme sync logs explicitly when refresh/rotation occurs so operators can trace credential churn in standard log levels.
|
||||
@@ -201,8 +201,8 @@ Cluster Registry & Provisioner Cheatsheet
|
||||
- UUID‑first everywhere: API paths `{uuid}`, Registry `Get/Delete/RotateSecret` by UUID; explicit `FindByClientID` exists for OAuth.
|
||||
- Node/DTO fields: `uuid` required; `clientId` optional; database metadata includes `driver`.
|
||||
- Provisioner naming (no slugs):
|
||||
- database: `photoprism_d<hmac11>`
|
||||
- username: `photoprism_u<hmac11>`
|
||||
- database: `cluster_d<hmac11>`
|
||||
- username: `cluster_u<hmac11>`
|
||||
HMAC is base32 of ClusterUUID+NodeUUID; drivers currently `mysql|mariadb`.
|
||||
- DSN builder: `BuildDSN(driver, host, port, user, pass, name)`; warns and falls back to MySQL format for unsupported drivers.
|
||||
- Go tests live beside sources: for `path/to/pkg/<file>.go`, add tests in `path/to/pkg/<file>_test.go` (create if missing). For the same function, group related cases as `t.Run(...)` sub-tests (table-driven where helpful) and name each subtest string in PascalCase.
|
||||
|
||||
@@ -105,7 +105,7 @@ func TestGetMetrics(t *testing.T) {
|
||||
name string
|
||||
role string
|
||||
}{
|
||||
{"metrics-instance-1", string(cluster.RoleApp)},
|
||||
{"metrics-app-1", string(cluster.RoleApp)},
|
||||
{"metrics-service-1", string(cluster.RoleService)},
|
||||
}
|
||||
|
||||
|
||||
@@ -304,7 +304,7 @@ func (c *Config) NodeName() string {
|
||||
return "node-" + s
|
||||
}
|
||||
|
||||
// NodeRole returns the cluster node ROLE (portal, instance, or service).
|
||||
// NodeRole returns the cluster node role (portal, app, or service).
|
||||
func (c *Config) NodeRole() string {
|
||||
if c.Edition() == Portal {
|
||||
c.options.NodeRole = cluster.RolePortal
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestInitConfig_NoPortal_NoOp(t *testing.T) {
|
||||
c := config.NewMinimalTestConfigWithDb("bootstrap", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
|
||||
// Default NodeRole() resolves to instance; no Portal configured.
|
||||
// Default NodeRole() resolves to app; no Portal configured.
|
||||
assert.Equal(t, cluster.RoleApp, c.NodeRole())
|
||||
assert.NoError(t, InitConfig(c))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Package node bootstraps a PhotoPrism node (instance or service) that joins a cluster Portal.
|
||||
Package node bootstraps a PhotoPrism node (app or service) that joins a cluster Portal.
|
||||
|
||||
Responsibilities include:
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ func TestOptionsUpdate_Apply(t *testing.T) {
|
||||
update := cluster.OptionsUpdate{}
|
||||
update.SetClusterUUID("4a47c940-d5de-41b3-88a2-eb816cc659ca")
|
||||
update.SetClusterCIDR("192.0.2.0/24")
|
||||
update.SetDatabaseName("photoprism_cluster")
|
||||
update.SetDatabaseUser("photoprism_user")
|
||||
update.SetDatabaseName("cluster_database")
|
||||
update.SetDatabaseUser("cluster_user")
|
||||
|
||||
written, err := clusternode.ApplyOptionsUpdate(conf, update)
|
||||
require.NoError(t, err)
|
||||
@@ -59,8 +59,8 @@ func TestOptionsUpdate_Apply(t *testing.T) {
|
||||
assert.Equal(t, "value", merged["Existing"])
|
||||
assert.Equal(t, "4a47c940-d5de-41b3-88a2-eb816cc659ca", merged["ClusterUUID"])
|
||||
assert.Equal(t, "192.0.2.0/24", merged["ClusterCIDR"])
|
||||
assert.Equal(t, "photoprism_cluster", merged["DatabaseName"])
|
||||
assert.Equal(t, "photoprism_user", merged["DatabaseUser"])
|
||||
assert.Equal(t, "cluster_database", merged["DatabaseName"])
|
||||
assert.Equal(t, "cluster_user", merged["DatabaseUser"])
|
||||
|
||||
// Applying an empty update should be a no-op.
|
||||
empty := cluster.OptionsUpdate{}
|
||||
|
||||
@@ -31,18 +31,18 @@ The provisioner package manages per-node MariaDB schemas and users for cluster d
|
||||
- Connect from the dev container using `mariadb` (already configured to reach `mariadb:4001`). Common snippets:
|
||||
```bash
|
||||
cat <<'SQL' | mariadb
|
||||
SHOW DATABASES LIKE 'photoprism_d%';
|
||||
SHOW DATABASES LIKE 'cluster_d%';
|
||||
SQL
|
||||
```
|
||||
```bash
|
||||
cat <<'SQL' | mariadb
|
||||
SELECT User, Host FROM mysql.user WHERE User LIKE 'photoprism_u%';
|
||||
SELECT User, Host FROM mysql.user WHERE User LIKE 'cluster_u%';
|
||||
SQL
|
||||
```
|
||||
- Manually drop leftover resources when iterating outside tests:
|
||||
```bash
|
||||
for db in $(cat <<'SQL' | mariadb --batch --skip-column-names
|
||||
SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE 'photoprism_d%';
|
||||
SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE 'cluster_d%';
|
||||
SQL
|
||||
); do
|
||||
printf 'DROP DATABASE IF EXISTS `%s`;\\n' "$db" | mariadb
|
||||
@@ -50,7 +50,7 @@ The provisioner package manages per-node MariaDB schemas and users for cluster d
|
||||
```
|
||||
```bash
|
||||
for user in $(cat <<'SQL' | mariadb --batch --skip-column-names
|
||||
SELECT User FROM mysql.user WHERE User LIKE 'photoprism_u%';
|
||||
SELECT User FROM mysql.user WHERE User LIKE 'cluster_u%';
|
||||
SQL
|
||||
); do
|
||||
cat <<SQL | mariadb
|
||||
@@ -64,7 +64,7 @@ The provisioner package manages per-node MariaDB schemas and users for cluster d
|
||||
|
||||
- Always pair credential creation with `DropCredentials` inside `t.Cleanup` for tests and defer blocks for ad-hoc scripts.
|
||||
- When troubleshooting API or CLI flows, capture the node UUID and name from the response and call `GenerateCredentials` to identify which schema/user to drop once finished.
|
||||
- Before committing, run `SHOW DATABASES LIKE 'photoprism_d%';` and `SELECT User FROM mysql.user WHERE User LIKE 'photoprism_u%';` to verify the MariaDB instance is clean.
|
||||
- Before committing, run `SHOW DATABASES LIKE 'cluster_d%';` and `SELECT User FROM mysql.user WHERE User LIKE 'cluster_u%';` to verify the MariaDB instance is clean.
|
||||
|
||||
## Focused Commands
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
func TestQuoteIdent_Valid(t *testing.T) {
|
||||
cases := []string{
|
||||
"photoprism_db",
|
||||
"cluster_db",
|
||||
"my.db-1_2",
|
||||
"a",
|
||||
"z9-._",
|
||||
@@ -54,9 +54,9 @@ func TestQuoteString_RejectsNUL(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestQuoteAccount(t *testing.T) {
|
||||
got, err := quoteAccount("%", "photoprism_user")
|
||||
got, err := quoteAccount("%", "cluster_user")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "'photoprism_user'@'%'", got)
|
||||
assert.Equal(t, "'cluster_user'@'%'", got)
|
||||
}
|
||||
|
||||
func TestQuoteAccount_Errors(t *testing.T) {
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
const (
|
||||
// Name prefix for generated DB objects.
|
||||
// Final pattern without slugs (UUID-based):
|
||||
// database: photoprism_d<hmac11>
|
||||
// username: photoprism_u<hmac11>
|
||||
// database: cluster_d<hmac11>
|
||||
// username: cluster_u<hmac11>
|
||||
prefix = "photoprism_"
|
||||
dbSuffix = 11
|
||||
userSuffix = 11
|
||||
|
||||
@@ -115,7 +115,7 @@ func (r *ClientRegistry) Put(n *Node) error {
|
||||
m.SetRole(n.Role)
|
||||
}
|
||||
|
||||
// Ensure a default scope for node clients (instance/service) if none is set.
|
||||
// Ensure a default scope for node clients (app/service) if none is set.
|
||||
// Always include "vision"; this only permits access to Vision endpoints WHEN the Portal enables them.
|
||||
if m.Scope() == "" {
|
||||
role := m.AclRole().String()
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestClientRegistry_RoleChange(t *testing.T) {
|
||||
if assert.NotNil(t, got) {
|
||||
assert.Equal(t, "service", got.Role)
|
||||
}
|
||||
// Change to instance
|
||||
// Change to app
|
||||
upd := &Node{Node: cluster.Node{ClientID: got.ClientID, Name: got.Name, Role: cluster.RoleApp}}
|
||||
assert.NoError(t, r.Put(upd))
|
||||
got2, err := r.FindByName("pp-role")
|
||||
|
||||
@@ -120,8 +120,8 @@ func TestClientRegistry_DBDriverAndFields(t *testing.T) {
|
||||
r, _ := NewClientRegistryWithConfig(c)
|
||||
n := &Node{Node: cluster.Node{UUID: rnd.UUIDv7(), Name: "pp-db", Role: cluster.RoleApp}}
|
||||
db := n.ensureDatabase()
|
||||
db.Name = "photoprism_d123"
|
||||
db.User = "photoprism_u123"
|
||||
db.Name = "cluster_d123"
|
||||
db.User = "cluster_u123"
|
||||
db.Driver = "mysql"
|
||||
db.RotatedAt = time.Now().UTC().Format(time.RFC3339)
|
||||
assert.NoError(t, r.Put(n))
|
||||
@@ -129,8 +129,8 @@ func TestClientRegistry_DBDriverAndFields(t *testing.T) {
|
||||
got, err := r.FindByNodeUUID(n.UUID)
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, got) {
|
||||
assert.Equal(t, "photoprism_d123", got.Database.Name)
|
||||
assert.Equal(t, "photoprism_u123", got.Database.User)
|
||||
assert.Equal(t, "cluster_d123", got.Database.Name)
|
||||
assert.Equal(t, "cluster_u123", got.Database.User)
|
||||
assert.Equal(t, "mysql", got.Database.Driver)
|
||||
}
|
||||
|
||||
@@ -138,6 +138,6 @@ func TestClientRegistry_DBDriverAndFields(t *testing.T) {
|
||||
dto := BuildClusterNode(*got, NodeOpts{IncludeAdvertiseUrl: true, IncludeDatabase: true})
|
||||
if assert.NotNil(t, dto.Database) {
|
||||
assert.Equal(t, "mysql", dto.Database.Driver)
|
||||
assert.Equal(t, "photoprism_d123", dto.Database.Name)
|
||||
assert.Equal(t, "cluster_d123", dto.Database.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
type NodeRole = string
|
||||
|
||||
const (
|
||||
RoleApp = NodeRole(acl.RoleApp) // A regular PhotoPrism instance that can join a cluster
|
||||
RoleApp = NodeRole(acl.RoleApp) // A regular PhotoPrism app node that can join a cluster
|
||||
RolePortal = NodeRole(acl.RolePortal) // A management portal for orchestrating a cluster
|
||||
RoleService = NodeRole(acl.RoleService) // Other service used within a cluster, e.g. Ollama or Vision API
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user