mirror of
https://github.com/photoprism/photoprism.git
synced 2025-09-26 21:01:58 +08:00
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -229,6 +229,7 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
|
||||
resp := cluster.RegisterResponse{
|
||||
UUID: conf.ClusterUUID(),
|
||||
ClusterCIDR: conf.ClusterCIDR(),
|
||||
Node: reg.BuildClusterNode(*n, opts),
|
||||
Database: cluster.RegisterDatabase{Host: conf.DatabaseHost(), Port: conf.DatabasePort(), Name: dbInfo.Name, User: dbInfo.User, Driver: provisioner.DatabaseDriver},
|
||||
Secrets: respSecret,
|
||||
@@ -299,6 +300,8 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
resp := cluster.RegisterResponse{
|
||||
UUID: conf.ClusterUUID(),
|
||||
ClusterCIDR: conf.ClusterCIDR(),
|
||||
Node: reg.BuildClusterNode(*n, reg.NodeOptsForSession(nil)),
|
||||
Secrets: &cluster.RegisterSecrets{ClientSecret: n.ClientSecret, RotatedAt: n.RotatedAt},
|
||||
Database: cluster.RegisterDatabase{Host: conf.DatabaseHost(), Port: conf.DatabasePort(), Name: creds.Name, User: creds.User, Driver: provisioner.DatabaseDriver, Password: creds.Password, DSN: creds.DSN, RotatedAt: creds.RotatedAt},
|
||||
|
@@ -46,10 +46,11 @@ func ClusterSummary(router *gin.RouterGroup) {
|
||||
nodes, _ := regy.List()
|
||||
|
||||
c.JSON(http.StatusOK, cluster.SummaryResponse{
|
||||
UUID: conf.ClusterUUID(),
|
||||
Nodes: len(nodes),
|
||||
Database: cluster.DatabaseInfo{Driver: conf.DatabaseDriverName(), Host: conf.DatabaseHost(), Port: conf.DatabasePort()},
|
||||
Time: time.Now().UTC().Format(time.RFC3339),
|
||||
UUID: conf.ClusterUUID(),
|
||||
ClusterCIDR: conf.ClusterCIDR(),
|
||||
Nodes: len(nodes),
|
||||
Database: cluster.DatabaseInfo{Driver: conf.DatabaseDriverName(), Host: conf.DatabaseHost(), Port: conf.DatabasePort()},
|
||||
Time: time.Now().UTC().Format(time.RFC3339),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@@ -419,6 +419,9 @@
|
||||
"alreadyRegistered": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"clusterCidr": {
|
||||
"type": "string"
|
||||
},
|
||||
"database": {
|
||||
"$ref": "#/definitions/cluster.RegisterDatabase"
|
||||
},
|
||||
@@ -459,6 +462,9 @@
|
||||
},
|
||||
"cluster.SummaryResponse": {
|
||||
"properties": {
|
||||
"clusterCidr": {
|
||||
"type": "string"
|
||||
},
|
||||
"database": {
|
||||
"$ref": "#/definitions/cluster.DatabaseInfo"
|
||||
},
|
||||
|
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/service/cluster"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/service/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/txt/report"
|
||||
)
|
||||
@@ -330,6 +331,16 @@ func parseLabelSlice(labels []string) map[string]string {
|
||||
|
||||
// Persistence helpers for --write-config
|
||||
func persistRegisterResponse(conf *config.Config, resp *cluster.RegisterResponse) error {
|
||||
updates := map[string]any{}
|
||||
|
||||
if rnd.IsUUID(resp.UUID) {
|
||||
updates["ClusterUUID"] = resp.UUID
|
||||
}
|
||||
|
||||
if cidr := strings.TrimSpace(resp.ClusterCIDR); cidr != "" {
|
||||
updates["ClusterCIDR"] = cidr
|
||||
}
|
||||
|
||||
// Node client secret file
|
||||
if resp.Secrets != nil && resp.Secrets.ClientSecret != "" {
|
||||
// Prefer PHOTOPRISM_NODE_CLIENT_SECRET_FILE; otherwise config cluster path
|
||||
@@ -348,16 +359,18 @@ func persistRegisterResponse(conf *config.Config, resp *cluster.RegisterResponse
|
||||
|
||||
// DB settings (MySQL/MariaDB only)
|
||||
if resp.Database.Name != "" && resp.Database.User != "" {
|
||||
if err := mergeOptionsYaml(conf, map[string]any{
|
||||
"DatabaseDriver": config.MySQL,
|
||||
"DatabaseName": resp.Database.Name,
|
||||
"DatabaseServer": fmt.Sprintf("%s:%d", resp.Database.Host, resp.Database.Port),
|
||||
"DatabaseUser": resp.Database.User,
|
||||
"DatabasePassword": resp.Database.Password,
|
||||
}); err != nil {
|
||||
updates["DatabaseDriver"] = config.MySQL
|
||||
updates["DatabaseName"] = resp.Database.Name
|
||||
updates["DatabaseServer"] = fmt.Sprintf("%s:%d", resp.Database.Host, resp.Database.Port)
|
||||
updates["DatabaseUser"] = resp.Database.User
|
||||
updates["DatabasePassword"] = resp.Database.Password
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := mergeOptionsYaml(conf, updates); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("updated options.yml with database settings for node %s", clean.LogQuote(resp.Node.Name))
|
||||
log.Infof("updated options.yml with cluster registration settings for node %s", clean.LogQuote(resp.Node.Name))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@@ -36,10 +36,11 @@ func clusterSummaryAction(ctx *cli.Context) error {
|
||||
nodes, _ := r.List()
|
||||
|
||||
resp := cluster.SummaryResponse{
|
||||
UUID: conf.ClusterUUID(),
|
||||
Nodes: len(nodes),
|
||||
Database: cluster.DatabaseInfo{Driver: conf.DatabaseDriverName(), Host: conf.DatabaseHost(), Port: conf.DatabasePort()},
|
||||
Time: time.Now().UTC().Format(time.RFC3339),
|
||||
UUID: conf.ClusterUUID(),
|
||||
ClusterCIDR: conf.ClusterCIDR(),
|
||||
Nodes: len(nodes),
|
||||
Database: cluster.DatabaseInfo{Driver: conf.DatabaseDriverName(), Host: conf.DatabaseHost(), Port: conf.DatabasePort()},
|
||||
Time: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if ctx.Bool("json") {
|
||||
@@ -48,8 +49,8 @@ func clusterSummaryAction(ctx *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
cols := []string{"Portal UUID", "Nodes", "DB Driver", "DB Host", "DB Port", "Time"}
|
||||
rows := [][]string{{resp.UUID, fmt.Sprintf("%d", resp.Nodes), resp.Database.Driver, resp.Database.Host, fmt.Sprintf("%d", resp.Database.Port), resp.Time}}
|
||||
cols := []string{"Portal UUID", "Cluster CIDR", "Nodes", "DB Driver", "DB Host", "DB Port", "Time"}
|
||||
rows := [][]string{{resp.UUID, resp.ClusterCIDR, fmt.Sprintf("%d", resp.Nodes), resp.Database.Driver, resp.Database.Host, fmt.Sprintf("%d", resp.Database.Port), resp.Time}}
|
||||
out, err := report.RenderFormat(rows, cols, report.CliFormat(ctx))
|
||||
fmt.Printf("\n%s\n", out)
|
||||
return err
|
||||
|
@@ -95,9 +95,10 @@ func TestClusterThemePull_JoinTokenToOAuth(t *testing.T) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// Return NodeClientID and a fresh secret
|
||||
_ = json.NewEncoder(w).Encode(cluster.RegisterResponse{
|
||||
UUID: rnd.UUID(),
|
||||
Node: cluster.Node{ClientID: "cs5gfen1bgxz7s9i", Name: "pp-node-01"},
|
||||
Secrets: &cluster.RegisterSecrets{ClientSecret: "s3cr3t"},
|
||||
UUID: rnd.UUID(),
|
||||
ClusterCIDR: "203.0.113.0/24",
|
||||
Node: cluster.Node{ClientID: "cs5gfen1bgxz7s9i", Name: "pp-node-01"},
|
||||
Secrets: &cluster.RegisterSecrets{ClientSecret: "s3cr3t"},
|
||||
})
|
||||
case "/api/v1/oauth/token":
|
||||
// Expect Basic for the returned creds
|
||||
|
@@ -219,6 +219,10 @@ func persistRegistration(c *config.Config, r *cluster.RegisterResponse, wantRota
|
||||
updates["ClusterUUID"] = r.UUID
|
||||
}
|
||||
|
||||
if cidr := strings.TrimSpace(r.ClusterCIDR); cidr != "" {
|
||||
updates["ClusterCIDR"] = cidr
|
||||
}
|
||||
|
||||
// Always persist NodeClientID (client UID) from response for future OAuth token requests.
|
||||
if r.Node.ClientID != "" {
|
||||
updates["NodeClientID"] = r.Node.ClientID
|
||||
|
@@ -37,10 +37,11 @@ func TestRegister_PersistSecretAndDB(t *testing.T) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
resp := cluster.RegisterResponse{
|
||||
Node: cluster.Node{Name: "pp-node-01"},
|
||||
UUID: rnd.UUID(),
|
||||
Secrets: &cluster.RegisterSecrets{ClientSecret: "SECRET"},
|
||||
JWKSUrl: jwksURL,
|
||||
Node: cluster.Node{Name: "pp-node-01"},
|
||||
UUID: rnd.UUID(),
|
||||
ClusterCIDR: "192.0.2.0/24",
|
||||
Secrets: &cluster.RegisterSecrets{ClientSecret: "SECRET"},
|
||||
JWKSUrl: jwksURL,
|
||||
Database: cluster.RegisterDatabase{
|
||||
Driver: config.MySQL,
|
||||
Host: "db.local",
|
||||
@@ -84,6 +85,7 @@ func TestRegister_PersistSecretAndDB(t *testing.T) {
|
||||
assert.Contains(t, c.Options().DatabaseDSN, "@tcp(db.local:3306)/pp_db")
|
||||
assert.Equal(t, config.MySQL, c.Options().DatabaseDriver)
|
||||
assert.Equal(t, srv.URL+"/.well-known/jwks.json", c.JWKSUrl())
|
||||
assert.Equal(t, "192.0.2.0/24", c.ClusterCIDR())
|
||||
}
|
||||
|
||||
func TestThemeInstall_Missing(t *testing.T) {
|
||||
@@ -101,7 +103,7 @@ func TestThemeInstall_Missing(t *testing.T) {
|
||||
case "/api/v1/cluster/nodes/register":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// Return NodeClientID + NodeClientSecret so bootstrap can request OAuth token
|
||||
_ = json.NewEncoder(w).Encode(cluster.RegisterResponse{UUID: rnd.UUID(), Node: cluster.Node{ClientID: "cs5gfen1bgxz7s9i", Name: "pp-node-01"}, Secrets: &cluster.RegisterSecrets{ClientSecret: "s3cr3t"}, JWKSUrl: jwksURL2})
|
||||
_ = json.NewEncoder(w).Encode(cluster.RegisterResponse{UUID: rnd.UUID(), ClusterCIDR: "198.51.100.0/24", Node: cluster.Node{ClientID: "cs5gfen1bgxz7s9i", Name: "pp-node-01"}, Secrets: &cluster.RegisterSecrets{ClientSecret: "s3cr3t"}, JWKSUrl: jwksURL2})
|
||||
case "/api/v1/oauth/token":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"access_token": "tok", "token_type": "Bearer"})
|
||||
@@ -148,10 +150,11 @@ func TestRegister_SQLite_NoDBPersist(t *testing.T) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
resp := cluster.RegisterResponse{
|
||||
Node: cluster.Node{Name: "pp-node-01"},
|
||||
Secrets: &cluster.RegisterSecrets{ClientSecret: "SECRET"},
|
||||
JWKSUrl: jwksURL3,
|
||||
Database: cluster.RegisterDatabase{Host: "db.local", Port: 3306, Name: "pp_db", User: "pp_user", Password: "pp_pw", DSN: "pp_user:pp_pw@tcp(db.local:3306)/pp_db?charset=utf8mb4&parseTime=true"},
|
||||
Node: cluster.Node{Name: "pp-node-01"},
|
||||
Secrets: &cluster.RegisterSecrets{ClientSecret: "SECRET"},
|
||||
ClusterCIDR: "203.0.113.0/24",
|
||||
JWKSUrl: jwksURL3,
|
||||
Database: cluster.RegisterDatabase{Host: "db.local", Port: 3306, Name: "pp_db", User: "pp_user", Password: "pp_pw", DSN: "pp_user:pp_pw@tcp(db.local:3306)/pp_db?charset=utf8mb4&parseTime=true"},
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
default:
|
||||
@@ -179,6 +182,7 @@ func TestRegister_SQLite_NoDBPersist(t *testing.T) {
|
||||
assert.Equal(t, config.SQLite3, c.DatabaseDriver())
|
||||
assert.Equal(t, origDSN, c.Options().DatabaseDSN)
|
||||
assert.Equal(t, srv.URL+"/.well-known/jwks.json", c.JWKSUrl())
|
||||
assert.Equal(t, "203.0.113.0/24", c.ClusterCIDR())
|
||||
}
|
||||
|
||||
func TestRegister_404_NoRetry(t *testing.T) {
|
||||
|
@@ -35,10 +35,11 @@ type DatabaseInfo struct {
|
||||
// SummaryResponse is the response type for GET /api/v1/cluster.
|
||||
// swagger:model SummaryResponse
|
||||
type SummaryResponse struct {
|
||||
UUID string `json:"uuid"` // ClusterUUID
|
||||
Nodes int `json:"nodes"`
|
||||
Database DatabaseInfo `json:"database"`
|
||||
Time string `json:"time"`
|
||||
UUID string `json:"uuid"` // ClusterUUID
|
||||
ClusterCIDR string `json:"clusterCidr,omitempty"`
|
||||
Nodes int `json:"nodes"`
|
||||
Database DatabaseInfo `json:"database"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
// RegisterSecrets contains newly issued or rotated node secrets.
|
||||
@@ -65,6 +66,7 @@ type RegisterDatabase struct {
|
||||
// swagger:model RegisterResponse
|
||||
type RegisterResponse struct {
|
||||
UUID string `json:"uuid"` // ClusterUUID
|
||||
ClusterCIDR string `json:"clusterCidr,omitempty"`
|
||||
Node Node `json:"node"`
|
||||
Database RegisterDatabase `json:"database"`
|
||||
Secrets *RegisterSecrets `json:"secrets,omitempty"`
|
||||
|
Reference in New Issue
Block a user