mirror of
https://github.com/photoprism/photoprism.git
synced 2025-10-26 02:10:33 +08:00
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
60
internal/api/cluster_metrics.go
Normal file
60
internal/api/cluster_metrics.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/auth/acl"
|
||||||
|
"github.com/photoprism/photoprism/internal/photoprism/get"
|
||||||
|
"github.com/photoprism/photoprism/internal/service/cluster"
|
||||||
|
reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClusterMetrics returns lightweight metrics about the cluster.
|
||||||
|
//
|
||||||
|
// @Summary temporary cluster metrics (counts only)
|
||||||
|
// @Id ClusterMetrics
|
||||||
|
// @Tags Cluster
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} cluster.MetricsResponse
|
||||||
|
// @Failure 401,403,429 {object} i18n.Response
|
||||||
|
// @Router /api/v1/cluster/metrics [get]
|
||||||
|
func ClusterMetrics(router *gin.RouterGroup) {
|
||||||
|
router.GET("/cluster/metrics", func(c *gin.Context) {
|
||||||
|
s := Auth(c, acl.ResourceCluster, acl.ActionView)
|
||||||
|
if s.Abort(c) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := get.Config()
|
||||||
|
if !conf.IsPortal() {
|
||||||
|
AbortFeatureDisabled(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
regy, err := reg.NewClientRegistryWithConfig(conf)
|
||||||
|
if err != nil {
|
||||||
|
AbortUnexpectedError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes, _ := regy.List()
|
||||||
|
counts := map[string]int{"total": len(nodes)}
|
||||||
|
for _, node := range nodes {
|
||||||
|
role := node.Role
|
||||||
|
if role == "" {
|
||||||
|
role = "unknown"
|
||||||
|
}
|
||||||
|
counts[role]++
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, cluster.MetricsResponse{
|
||||||
|
UUID: conf.ClusterUUID(),
|
||||||
|
ClusterCIDR: conf.ClusterCIDR(),
|
||||||
|
Nodes: counts,
|
||||||
|
Time: time.Now().UTC().Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -26,15 +26,20 @@ func TestClusterPermissions(t *testing.T) {
|
|||||||
defer conf.SetAuthMode(config.AuthModePublic)
|
defer conf.SetAuthMode(config.AuthModePublic)
|
||||||
|
|
||||||
ClusterSummary(router)
|
ClusterSummary(router)
|
||||||
|
ClusterMetrics(router)
|
||||||
|
|
||||||
r := PerformRequest(app, http.MethodGet, "/api/v1/cluster")
|
r := PerformRequest(app, http.MethodGet, "/api/v1/cluster")
|
||||||
assert.Equal(t, http.StatusUnauthorized, r.Code)
|
assert.Equal(t, http.StatusUnauthorized, r.Code)
|
||||||
|
|
||||||
|
r = PerformRequest(app, http.MethodGet, "/api/v1/cluster/metrics")
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, r.Code)
|
||||||
})
|
})
|
||||||
t.Run("ForbiddenFromCDN", func(t *testing.T) {
|
t.Run("ForbiddenFromCDN", func(t *testing.T) {
|
||||||
app, router, conf := NewApiTest()
|
app, router, conf := NewApiTest()
|
||||||
conf.Options().NodeRole = cluster.RolePortal
|
conf.Options().NodeRole = cluster.RolePortal
|
||||||
|
|
||||||
ClusterListNodes(router)
|
ClusterListNodes(router)
|
||||||
|
ClusterMetrics(router)
|
||||||
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/cluster/nodes", nil)
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/cluster/nodes", nil)
|
||||||
// Mark as CDN request, which Auth() forbids.
|
// Mark as CDN request, which Auth() forbids.
|
||||||
@@ -47,9 +52,13 @@ func TestClusterPermissions(t *testing.T) {
|
|||||||
app, router, conf := NewApiTest()
|
app, router, conf := NewApiTest()
|
||||||
conf.Options().NodeRole = cluster.RolePortal
|
conf.Options().NodeRole = cluster.RolePortal
|
||||||
ClusterSummary(router)
|
ClusterSummary(router)
|
||||||
|
ClusterMetrics(router)
|
||||||
token := AuthenticateAdmin(app, router)
|
token := AuthenticateAdmin(app, router)
|
||||||
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/cluster", token)
|
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/cluster", token)
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
|
|
||||||
|
r = AuthenticatedRequest(app, http.MethodGet, "/api/v1/cluster/metrics", token)
|
||||||
|
assert.Equal(t, http.StatusOK, r.Code)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Note: most fixture users have admin role; client-scope test below covers non-admin denial.
|
// Note: most fixture users have admin role; client-scope test below covers non-admin denial.
|
||||||
@@ -77,7 +86,11 @@ func TestClusterPermissions(t *testing.T) {
|
|||||||
token := gjson.Get(w.Body.String(), "access_token").String()
|
token := gjson.Get(w.Body.String(), "access_token").String()
|
||||||
|
|
||||||
ClusterSummary(router)
|
ClusterSummary(router)
|
||||||
|
ClusterMetrics(router)
|
||||||
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/cluster", token)
|
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/cluster", token)
|
||||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||||
|
|
||||||
|
r = AuthenticatedRequest(app, http.MethodGet, "/api/v1/cluster/metrics", token)
|
||||||
|
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,6 +324,26 @@
|
|||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"cluster.MetricsResponse": {
|
||||||
|
"properties": {
|
||||||
|
"clusterCidr": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"nodes": {
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"cluster.Node": {
|
"cluster.Node": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"advertiseUrl": {
|
"advertiseUrl": {
|
||||||
@@ -6409,6 +6429,44 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/cluster/metrics": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "ClusterMetrics",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/cluster.MetricsResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/i18n.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/i18n.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"429": {
|
||||||
|
"description": "Too Many Requests",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/i18n.Response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"summary": "temporary cluster metrics (counts only)",
|
||||||
|
"tags": [
|
||||||
|
"Cluster"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/cluster/nodes": {
|
"/api/v1/cluster/nodes": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "ClusterListNodes",
|
"operationId": "ClusterListNodes",
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||||||
api.ClusterUpdateNode(APIv1)
|
api.ClusterUpdateNode(APIv1)
|
||||||
api.ClusterDeleteNode(APIv1)
|
api.ClusterDeleteNode(APIv1)
|
||||||
api.ClusterSummary(APIv1)
|
api.ClusterSummary(APIv1)
|
||||||
|
api.ClusterMetrics(APIv1)
|
||||||
api.ClusterHealth(APIv1)
|
api.ClusterHealth(APIv1)
|
||||||
|
|
||||||
// Technical Endpoints.
|
// Technical Endpoints.
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ type SummaryResponse struct {
|
|||||||
Time string `json:"time"`
|
Time string `json:"time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MetricsResponse is the response type for GET /api/v1/cluster/metrics.
|
||||||
|
// swagger:model MetricsResponse
|
||||||
|
type MetricsResponse struct {
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
ClusterCIDR string `json:"clusterCidr,omitempty"`
|
||||||
|
Nodes map[string]int `json:"nodes"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterSecrets contains newly issued or rotated node secrets.
|
// RegisterSecrets contains newly issued or rotated node secrets.
|
||||||
// swagger:model RegisterSecrets
|
// swagger:model RegisterSecrets
|
||||||
type RegisterSecrets struct {
|
type RegisterSecrets struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user