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)
|
||||
|
||||
ClusterSummary(router)
|
||||
ClusterMetrics(router)
|
||||
|
||||
r := PerformRequest(app, http.MethodGet, "/api/v1/cluster")
|
||||
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) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.Options().NodeRole = cluster.RolePortal
|
||||
|
||||
ClusterListNodes(router)
|
||||
ClusterMetrics(router)
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/cluster/nodes", nil)
|
||||
// Mark as CDN request, which Auth() forbids.
|
||||
@@ -47,9 +52,13 @@ func TestClusterPermissions(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.Options().NodeRole = cluster.RolePortal
|
||||
ClusterSummary(router)
|
||||
ClusterMetrics(router)
|
||||
token := AuthenticateAdmin(app, router)
|
||||
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/cluster", token)
|
||||
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.
|
||||
@@ -77,7 +86,11 @@ func TestClusterPermissions(t *testing.T) {
|
||||
token := gjson.Get(w.Body.String(), "access_token").String()
|
||||
|
||||
ClusterSummary(router)
|
||||
ClusterMetrics(router)
|
||||
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/cluster", token)
|
||||
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"
|
||||
},
|
||||
"cluster.MetricsResponse": {
|
||||
"properties": {
|
||||
"clusterCidr": {
|
||||
"type": "string"
|
||||
},
|
||||
"nodes": {
|
||||
"additionalProperties": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"time": {
|
||||
"type": "string"
|
||||
},
|
||||
"uuid": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"cluster.Node": {
|
||||
"properties": {
|
||||
"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": {
|
||||
"get": {
|
||||
"operationId": "ClusterListNodes",
|
||||
|
||||
@@ -201,6 +201,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
||||
api.ClusterUpdateNode(APIv1)
|
||||
api.ClusterDeleteNode(APIv1)
|
||||
api.ClusterSummary(APIv1)
|
||||
api.ClusterMetrics(APIv1)
|
||||
api.ClusterHealth(APIv1)
|
||||
|
||||
// Technical Endpoints.
|
||||
|
||||
@@ -42,6 +42,15 @@ type SummaryResponse struct {
|
||||
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.
|
||||
// swagger:model RegisterSecrets
|
||||
type RegisterSecrets struct {
|
||||
|
||||
Reference in New Issue
Block a user