mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-25 15:30:33 +01:00
Compare commits
1 Commits
main
...
nv/v1-depr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c30ba5414c |
@@ -2,7 +2,6 @@ package impldashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -13,10 +12,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/transition"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
@@ -33,190 +30,21 @@ func NewHandler(module dashboard.Module, providerSettings factory.ProviderSettin
|
||||
}
|
||||
|
||||
func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := dashboardtypes.PostableDashboard{}
|
||||
err = json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboardMigrator := transition.NewDashboardMigrateV5(handler.providerSettings.Logger, nil, nil)
|
||||
if req["version"] != "v5" {
|
||||
dashboardMigrator.Migrate(ctx, req)
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.Create(ctx, orgID, claims.Email, valuer.MustNewUUID(claims.IdentityID()), dashboardtypes.SourceUser, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
gettableDashboard, err := dashboardtypes.NewGettableDashboardFromDashboard(dashboard)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusCreated, gettableDashboard)
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError(
|
||||
"create a dashboard with POST /api/v2/dashboards (schema: "+dashboardtypes.V2CreateDashboardSchemaLink+")"))
|
||||
}
|
||||
|
||||
func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := dashboardtypes.UpdatableDashboard{}
|
||||
err = json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
diff := 0
|
||||
// Allow multiple deletions for API key requests; enforce for others
|
||||
if claims.IdentNProvider == authtypes.IdentNProviderTokenizer {
|
||||
diff = 1
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.Update(ctx, orgID, dashboardID, claims.Email, req, diff)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboard)
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError(
|
||||
"update a dashboard with PUT /api/v2/dashboards/{id} (schema: "+dashboardtypes.V2UpdateDashboardSchemaLink+"), or patch it with PATCH /api/v2/dashboards/{id} (schema: "+dashboardtypes.V2PatchDashboardSchemaLink+")"))
|
||||
}
|
||||
|
||||
func (handler *handler) LockUnlock(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := new(dashboardtypes.LockUnlockDashboard)
|
||||
err = json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
isAdmin := false
|
||||
selectors := []coretypes.Selector{
|
||||
coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName),
|
||||
}
|
||||
err = handler.authz.CheckWithTupleCreation(
|
||||
ctx,
|
||||
claims,
|
||||
valuer.MustNewUUID(claims.OrgID),
|
||||
authtypes.Relation{Verb: coretypes.VerbAssignee},
|
||||
coretypes.NewResourceRole(),
|
||||
selectors,
|
||||
selectors,
|
||||
)
|
||||
if err == nil {
|
||||
isAdmin = true
|
||||
}
|
||||
|
||||
err = handler.module.LockUnlock(ctx, orgID, dashboardID, claims.Email, isAdmin, *req.Locked)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, nil)
|
||||
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError("lock a dashboard with PUT /api/v2/dashboards/{id}/lock, or unlock it with DELETE /api/v2/dashboards/{id}/lock"))
|
||||
}
|
||||
|
||||
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
err = handler.module.Delete(ctx, orgID, dashboardID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError("delete a dashboard with DELETE /api/v2/dashboards/{id}"))
|
||||
}
|
||||
|
||||
func (handler *handler) CreatePublic(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -1078,75 +1078,11 @@ func prepareQuery(r *http.Request) (string, error) {
|
||||
}
|
||||
|
||||
func (aH *APIHandler) Get(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
dashboard, err := aH.Signoz.Modules.Dashboard.Get(ctx, orgID, dashboardID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
gettableDashboard, err := dashboardtypes.NewGettableDashboardFromDashboard(dashboard)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, gettableDashboard)
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError("get a dashboard with GET /api/v2/dashboards/{id}"))
|
||||
}
|
||||
|
||||
func (aH *APIHandler) List(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboards, err := aH.Signoz.Modules.Dashboard.List(ctx, orgID)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
gettableDashboards, err := dashboardtypes.NewGettableDashboardsFromDashboards(dashboards)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
render.Success(rw, http.StatusOK, gettableDashboards)
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError("list dashboards with GET /api/v2/dashboards"))
|
||||
}
|
||||
|
||||
func (aH *APIHandler) queryDashboardVarsV2(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -22,6 +22,7 @@ var (
|
||||
ErrCodeDashboardImmutable = errors.MustNewCode("dashboard_immutable")
|
||||
ErrCodeDashboardInvalidPatch = errors.MustNewCode("dashboard_invalid_patch")
|
||||
ErrCodeDashboardMigrationFailed = errors.MustNewCode("dashboard_migration_failed")
|
||||
ErrCodeDashboardV1Deprecated = errors.MustNewCode("dashboard_deprecated")
|
||||
)
|
||||
|
||||
type StorableDashboard struct {
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// v1 dashboards stored their `image` as one of a fixed set of base64 icon data
|
||||
// URIs. v2 references the same icons by a stable asset path (dashboardIconPathPrefix
|
||||
// + name). This maps each preset v1 base64 blob to its v2 icon path so a migrated
|
||||
@@ -51,37 +45,8 @@ func resolveV1Image(image string) string {
|
||||
if path, ok := v1Base64IconToAssetPath[image]; ok {
|
||||
return path
|
||||
}
|
||||
if reencoded, ok := reencodeInlineSVGImage(image); ok {
|
||||
image = reencoded
|
||||
}
|
||||
if (DashboardV2MetadataBase{Image: image}).validateImage() == nil {
|
||||
return image
|
||||
}
|
||||
return defaultDashboardIconPath
|
||||
}
|
||||
|
||||
// reencodeInlineSVGImage rewrites a percent-encoded inline SVG data URI into the
|
||||
// base64 form the v2 schema accepts, preserving an icon that would otherwise fail
|
||||
// validation. Returns (image, false) unchanged for anything else.
|
||||
func reencodeInlineSVGImage(image string) (string, bool) {
|
||||
const mediaType = "data:image/svg+xml"
|
||||
if !strings.HasPrefix(image, mediaType) {
|
||||
return image, false
|
||||
}
|
||||
rest := image[len(mediaType):]
|
||||
// The media type must be followed by its params/data separator, not more type
|
||||
// text (guards against a longer type that merely shares this prefix).
|
||||
if len(rest) == 0 || (rest[0] != ',' && rest[0] != ';') {
|
||||
return image, false
|
||||
}
|
||||
params, encoded, found := strings.Cut(rest, ",")
|
||||
// A base64 URI carries ";base64" before the comma and is already valid — leave it.
|
||||
if !found || strings.Contains(params, "base64") {
|
||||
return image, false
|
||||
}
|
||||
svg, err := url.PathUnescape(encoded)
|
||||
if err != nil {
|
||||
return image, false // malformed escaping — leave it; conversion falls back to the default icon
|
||||
}
|
||||
return "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg)), true
|
||||
}
|
||||
|
||||
@@ -429,11 +429,6 @@ func TestResolveV1Image(t *testing.T) {
|
||||
assert.Equal(t, passthrough, resolveV1Image(passthrough))
|
||||
}
|
||||
|
||||
// A percent-encoded inline SVG icon is preserved by re-encoding it to base64
|
||||
// (the form v2 accepts) rather than being dropped to the default.
|
||||
svgResolved := resolveV1Image("data:image/svg+xml,%3Csvg%2F%3E")
|
||||
assert.Equal(t, "data:image/svg+xml;base64,PHN2Zy8+", svgResolved)
|
||||
|
||||
// A value that v2 would reject (a URL, a corrupt data URI) falls back to the
|
||||
// default eight-ball icon rather than failing the dashboard migration.
|
||||
for _, invalid := range []string{
|
||||
@@ -445,66 +440,6 @@ func TestResolveV1Image(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReencodeInlineSVGImage(t *testing.T) {
|
||||
testCases := []struct {
|
||||
scenario string
|
||||
image string
|
||||
wantRewritten bool
|
||||
want string
|
||||
}{
|
||||
{
|
||||
scenario: "percent-encoded inline svg",
|
||||
image: "data:image/svg+xml,%3Csvg%2F%3E",
|
||||
wantRewritten: true,
|
||||
want: "data:image/svg+xml;base64,PHN2Zy8+",
|
||||
},
|
||||
{
|
||||
scenario: "percent-encoded inline svg with charset param",
|
||||
image: "data:image/svg+xml;charset=utf-8,%3Csvg%2F%3E",
|
||||
wantRewritten: true,
|
||||
want: "data:image/svg+xml;base64,PHN2Zy8+",
|
||||
},
|
||||
{
|
||||
scenario: "already base64 svg is left unchanged",
|
||||
image: "data:image/svg+xml;base64,PHN2Zy8+",
|
||||
wantRewritten: false,
|
||||
want: "data:image/svg+xml;base64,PHN2Zy8+",
|
||||
},
|
||||
{
|
||||
scenario: "non-svg data uri is left unchanged",
|
||||
image: "data:image/png,%89PNG",
|
||||
wantRewritten: false,
|
||||
want: "data:image/png,%89PNG",
|
||||
},
|
||||
{
|
||||
scenario: "an icon path is left unchanged",
|
||||
image: "/assets/Icons/eight-ball",
|
||||
wantRewritten: false,
|
||||
want: "/assets/Icons/eight-ball",
|
||||
},
|
||||
{
|
||||
scenario: "a longer media type merely sharing the svg prefix is left unchanged",
|
||||
image: "data:image/svg+xmlish,%3Csvg%2F%3E",
|
||||
wantRewritten: false,
|
||||
want: "data:image/svg+xmlish,%3Csvg%2F%3E",
|
||||
},
|
||||
{
|
||||
scenario: "empty is left unchanged",
|
||||
image: "",
|
||||
wantRewritten: false,
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.scenario, func(t *testing.T) {
|
||||
got, rewritten := reencodeInlineSVGImage(testCase.image)
|
||||
assert.Equal(t, testCase.wantRewritten, rewritten)
|
||||
assert.Equal(t, testCase.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertV1ToV2ReplacesInvalidImage(t *testing.T) {
|
||||
// An image v2 would reject must not sink the whole migration — it falls back
|
||||
// to the default icon so the dashboard still converts.
|
||||
|
||||
21
pkg/types/dashboardtypes/v1_deprecation.go
Normal file
21
pkg/types/dashboardtypes/v1_deprecation.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package dashboardtypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/errors"
|
||||
|
||||
// The v1 dashboard API is superseded by the v2 dashboard API; every v1 endpoint
|
||||
// now returns a deprecation error pointing at its v2 replacement. The body-carrying
|
||||
// endpoints (create, update) point to the v2 request-schema docs below; the
|
||||
// body-less endpoints just name the v2 endpoint to call.
|
||||
const (
|
||||
V2CreateDashboardSchemaLink = "https://signoz.io/dashboards/schema/dashboard_create.json"
|
||||
V2UpdateDashboardSchemaLink = "https://signoz.io/dashboards/schema/dashboard_update.json"
|
||||
V2PatchDashboardSchemaLink = "https://signoz.io/dashboards/schema/dashboard_patch.json"
|
||||
)
|
||||
|
||||
// NewV1DeprecatedError builds the error a deprecated v1 dashboard endpoint returns.
|
||||
// useInstead names the v2 replacement — a schema link for the create/update
|
||||
// endpoints, or the v2 endpoint to call for the body-less ones.
|
||||
func NewV1DeprecatedError(useInstead string) error {
|
||||
return errors.Newf(errors.TypeUnsupported, ErrCodeDashboardV1Deprecated,
|
||||
"the v1 dashboard API is deprecated; instead, %s", useInstead)
|
||||
}
|
||||
Reference in New Issue
Block a user