Upgrading from v0.54.0 to v0.55.0
This document provides instructions for upgrading Perses specially when dealing with breaking changes. As Perses is a rapidly evolving project, it's important to keep your installation up to date to benefit from the latest features and improvements.
Then, upgrading Perses is also depending on your usage as Perses can be used in different ways (e.g. as a standalone application, as a library, etc.). Therefore, the upgrade process may vary based on your specific use case.
Perses application#
Important dashboards are now grouped#
frontend.important_dashboards is now a list of groups, each with an optional title and description, and a list
of dashboard selectors. A selector without dashboard marks every dashboard of the project as important.
The previous flat list of selectors is deprecated and will be removed in v0.57.0. It is still accepted (and converted into a single untitled group), but a deprecation warning is logged at startup. Mixing both formats is rejected.
# Before
frontend:
important_dashboards:
- project: "perses"
dashboard: "Demo"
# After
frontend:
important_dashboards:
- title: "Quick links" # Optional
dashboards:
- project: "perses"
dashboard: "Demo"If you set important dashboards through environment variables, the legacy shape is not supported there: Perses will fail to start until the variables are updated accordingly:
PERSES_FRONTEND_IMPORTANT_DASHBOARDS_0_PROJECT -> PERSES_FRONTEND_IMPORTANT_DASHBOARDS_0_DASHBOARDS_0_PROJECT
PERSES_FRONTEND_IMPORTANT_DASHBOARDS_0_DASHBOARD -> PERSES_FRONTEND_IMPORTANT_DASHBOARDS_0_DASHBOARDS_0_DASHBOARDThe datasource proxy restricts the destinations it can reach#
To prevent the datasource proxy from being used to reach services that are not datasources (Server-Side Request Forgery), the destinations of the proxy are now verified. By default, the proxy refuses to reach:
- the loopback interface (
127.0.0.0/8,::1,localhost) - the link-local addresses (
169.254.0.0/16,fe80::/10) and the known cloud metadata / credentials endpoints - the multicast, reserved and unspecified addresses
- the Kubernetes API service when Perses is running in a Kubernetes cluster
- any URL scheme other than
httpandhttps, the URLs containing credentials (http://user:password@host), and the Unix sockets for the SQL datasources - the IPv4 addresses not written in the dotted-decimal notation (e.g.
2130706433,0x7f000001or127.1)
The private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, ...) remain allowed by default.
If one of your datasources is running on the same host as Perses (e.g. http://localhost:9090), the requests to
this datasource are now refused with a 403 Forbidden error, and saving such a datasource is refused with a
400 Bad Request error. You have to explicitly allow the loopback interface:
datasource:
proxy:
allowed_networks:
- "127.0.0.0/8"
- "::1/128"The same applies to the OAuth token URL of the secrets used by the datasources. Saving a secret whose OAuth token URL
is not allowed is refused with a 400 Bad Request error, and the requests to a datasource using such a secret are
refused with a 403 Forbidden error. If you restrict the destinations with allowed_hosts, this list must also contain
the host of the OAuth token URL.
When an IP address is part of both an allowed and a denied network, the most specific network wins (on equal prefix lengths, the allowed network wins). Allowing a large network therefore doesn't allow a more specific denied network, which has to be allowed explicitly. For example:
- allowing
127.0.0.0/8allows the loopback interface; - allowing
10.0.0.0/8(e.g. withdeny_private_networks: true) doesn't allow the Kubernetes API service IP; - allowing
169.254.0.0/16doesn't allow the cloud metadata endpoint169.254.169.254.
If Perses is using an HTTP proxy configured through the environment (HTTP_PROXY, HTTPS_PROXY), a datasource can no
longer target the address of this proxy, neither directly (for example when the proxy is bypassed with NO_PROXY) nor
through the proxy itself.
You can also further restrict the destinations, for example to deny the private networks or to only allow a list of hosts. See the DatasourceProxy config for more details.
Note for the users of v0.55.0-beta.4: the configuration of the HTTP proxy introduced in this beta (datasource.http_proxy)
has moved to datasource.proxy.http, so the whole configuration of the datasource proxy lives in a single section.
# Before (v0.55.0-beta.4)
datasource:
http_proxy:
max_timeout: 30s
# After
datasource:
proxy:
http:
max_timeout: 30sThe datasource proxy no longer forwards the credentials used to authenticate against Perses#
Before forwarding a request to a datasource, the proxy now removes the credentials that the client used to authenticate against Perses:
- The
Cookieheader sent by the client is always removed, because it contains the Perses session (and possibly the tokens of the OIDC/OAuth provider). You can't forward it withallowHeaders. ACookieheader explicitly defined in the datasource configuration is still sent. - When the Perses native authorization is enabled, the
Authorizationheader sent by the client (which contains the Perses token) is always removed. You can't forward it withallowHeaders. Use a Secret or theoauthPassthroughoption to authenticate against the datasource. - When the authorization is delegated (i.e. Kubernetes), the
Authorizationheader sent by the client is still forwarded to the datasource. To prevent this, addAuthorizationtodropHeaders. - An
Authorizationheader defined in theheadersof the datasource configuration is now always ignored, whatever its case (e.g.authorization). If you were using it to authenticate against the datasource, move the credentials to a Secret (authorization,basicAuthoroauth) and reference it in the datasource configuration:
# Before
kind: Datasource
spec:
plugin:
kind: PrometheusDatasource
spec:
proxy:
kind: HTTPProxy
spec:
url: https://prometheus.example.com
headers:
Authorization: "Bearer <token>"
# After
kind: Secret
metadata:
name: prometheus-token
project: my-project
spec:
authorization:
type: Bearer
credentials: "<token>"
---
kind: Datasource
spec:
plugin:
kind: PrometheusDatasource
spec:
proxy:
kind: HTTPProxy
spec:
url: https://prometheus.example.com
secret: prometheus-tokenThe responses returned by the datasources through the proxy are also hardened, as they are served under the Perses origin:
- they get the headers
Content-Security-Policy: sandbox; default-src 'none'; frame-ancestors 'none'andX-Content-Type-Options: nosniff; - the headers that would apply to the Perses origin are removed:
Set-Cookie,Set-Cookie2,Clear-Site-Data,Refresh,Strict-Transport-Security,Alt-Svc,Service-Worker-Allowedand the CORS headers (the CORS policy is defined bysecurity.cors); - only the relative
Location/Content-Locationheaders are kept.
Finally, the SQL proxy now rejects the queries using INTO OUTFILE or INTO DUMPFILE (MySQL / MariaDB).
See the proxy documentation for more details.
Files referenced in secrets must be in an allowed directory#
The Secrets and GlobalSecrets can reference files from the Perses server (basicAuth.passwordFile,
authorization.credentialsFile, oauth.clientSecretFile, tlsConfig.caFile, tlsConfig.certFile,
tlsConfig.keyFile). This could be used by anyone able to create a secret to read arbitrary files through the
datasource proxy.
These file references are now rejected by default. If you rely on them, you must list the directories where the
files are located with the new setting security.secret_file_allowed_directories:
security:
secret_file_allowed_directories:
- /etc/perses/secretsEach directory must be an absolute path, and the root directory (/) is not accepted. The file paths used in the
secrets must also be absolute. Symlinks are resolved, and the resolved file must remain inside one of the allowed
directories. The verification is done when saving a secret and before the proxy uses it. Saving a secret referencing a
file that is not allowed is refused, and the requests to a datasource using such a secret fail.
encryption_key is mandatory when the authentication is enabled#
When the authentication is enabled (security.enable_auth: true), the encryption key is also used to encrypt the access
and refresh tokens. Perses was previously falling back on a default (and publicly known) key when none was provided.
Now, if the authentication is enabled and neither security.encryption_key nor security.encryption_key_file is set,
Perses refuses to start. The key must be exactly 32 bytes long. You can generate one with one of these commands:
LC_ALL=C tr -dc '[:graph:]' < /dev/urandom | head -c 32
openssl rand -base64 24 | head -c 32security:
enable_auth: true
encryption_key: "<32 bytes long secret>"
# or
encryption_key_file: /etc/perses/encryption_keyNote that if you were already running Perses with the auth enabled and without any encryption key, setting a new key means the data previously encrypted with the default key (e.g. the secrets stored in the database) can no longer be decrypted. You will have to re-create these secrets after the upgrade, and users will need to log in again.
Plugin developer#
Go SDK: datasource.Selector is now a union type#
datasource.Selector has been restructured as a union type to make the distinction between a
concrete datasource reference and a variable reference explicit. The flat struct (with public Kind
and Name fields) no longer exists.
Before:
// Concrete datasource — struct literal
sel := &datasource.Selector{Kind: "PrometheusDatasource", Name: "myPrometheus"}
// Reading fields
fmt.Println(sel.Kind, sel.Name)After:
// Concrete datasource — use the constructor
sel := datasource.NewStaticSelector("PrometheusDatasource", "myPrometheus")
// Variable reference — new in this release; name may be "foo", "$foo", or "${foo}"
sel, err := datasource.NewVariableSelector("myDatasource")
// Reading fields — check which variant is set first
if sel.Static != nil {
fmt.Println(sel.Static.Kind, sel.Static.Name)
}
if sel.Variable != nil {
fmt.Println(sel.Variable.Name) // bare name without "$"
}If you maintain a datasource plugin with a Selector helper, update it to call the constructors:
// Before
func Selector(datasourceName string) *datasource.Selector {
return &datasource.Selector{
Kind: PluginKind,
Name: datasourceName,
}
}
// After
func Selector(datasourceName string) *datasource.Selector {
return datasource.NewStaticSelector(PluginKind, datasourceName)
}
func VariableSelector(datasourceName string) (*datasource.Selector, error) {
return datasource.NewVariableSelector(datasourceName)
}A static selector continues to serialize as {"kind":"...","name":"..."} (unchanged from previous
versions). A variable selector now serializes as the plain JSON/YAML string "$myVar" — this is a
new addition in this release; previously there was no way to express a variable reference in the
datasource selector wire format. Note that marshaling a zero-value Selector (neither Static nor
Variable set) or one with both fields set returns an error.
If you don't know in advance whether the name is a variable reference or not, use datasource.NewSelector(kind, name):
it returns a variable selector when name starts with $, and a static selector otherwise.
Go: the old dashboard and datasource specification is removed#
Since v0.54.0, the dashboard and datasource specification lives in the repository perses/spec. The deprecated copies
kept in perses/perses are now removed. If you still import them, replace the import paths as follows:
Removed package (github.com/perses/perses/...) | Replacement (github.com/perses/spec/...) |
|---|---|
pkg/model/api/v1/common | go/common (Plugin is now in go/plugin) |
pkg/model/api/v1/dashboard | go/dashboard |
pkg/model/api/v1/variable | go/dashboard/variable |
pkg/model/api/v1/plugin | go/plugin |
pkg/model/api/v1/datasource/http | go/datasource/proxy/http |
pkg/model/api/v1/datasource/sql | go/datasource/proxy/sql |
The deprecated types in github.com/perses/perses/pkg/model/api/v1 (DashboardSpec, Panel, PanelSpec,
PanelDisplay, Query, QuerySpec, Link, DatasourceSpec, ...) are also removed. Use their equivalent from
github.com/perses/spec/go/dashboard and github.com/perses/spec/go/datasource (e.g. dashboard.Spec,
dashboard.Panel, datasource.Spec).
CUE: the old schemas are removed#
For the same reason, the following CUE packages are removed:
| Removed package | Replacement |
|---|---|
github.com/perses/perses/cue/model/api/v1/common | github.com/perses/spec/cue/common (#Plugin is in github.com/perses/spec/cue/plugin) |
github.com/perses/perses/cue/model/api/v1/dashboard | github.com/perses/spec/cue/dashboard |
github.com/perses/perses/cue/model/api/v1/variable | github.com/perses/spec/cue/dashboard/variable |
github.com/perses/perses/cue/model/api/v1/plugin | github.com/perses/spec/cue/plugin |
github.com/perses/perses/cue/model/api/v1/datasource/* | github.com/perses/spec/cue/datasource/proxy/http and .../proxy/sql |
github.com/perses/shared/cue/common/proxy | github.com/perses/spec/cue/datasource/proxy/http and .../proxy/sql |
v1.#DashboardSpec and v1.#Panel are replaced by dashboard.#Spec and dashboard.#Panel
(from github.com/perses/spec/cue/dashboard).
If your plugin schemas were using the deprecated github.com/perses/shared/cue/common/proxy package, replace its
definitions as follows:
| Removed definition | Replacement | Package |
|---|---|---|
proxy.#HTTPProxy | http.#Proxy | github.com/perses/spec/cue/datasource/proxy/http |
proxy.#HTTPAllowedEndpoint | http.#AllowedEndpoint | github.com/perses/spec/cue/datasource/proxy/http |
proxy.#baseHTTPDatasourceSpec | datasource.#HTTPDatasourceSpec | github.com/perses/spec/cue/datasource |
proxy.#SQLProxy | sql.#Proxy | github.com/perses/spec/cue/datasource/proxy/sql |
proxy.#MySQL | sql.#MySQLConfig | github.com/perses/spec/cue/datasource/proxy/sql |
proxy.#Postgres | sql.#PostgresConfig | github.com/perses/spec/cue/datasource/proxy/sql |
proxy.#baseSQLDatasourceSpec | datasource.#SQLDatasourceSpec | github.com/perses/spec/cue/datasource |
// Before
import "github.com/perses/shared/cue/common/proxy"
spec: proxy.#baseHTTPDatasourceSpec
// After
import "github.com/perses/spec/cue/datasource"
spec: datasource.#HTTPDatasourceSpecDon't forget to bump the dependencies in your cue.mod/module.cue (github.com/perses/shared/cue@v0 to v0.55.x and
github.com/perses/spec/cue@v0 to v0.3.x).
@perses-dev/core is removed#
As announced in v0.54.0, the package @perses-dev/core is no longer published, and the other packages don't depend on
it anymore. If you are still importing it, follow the migration described in
Core package deprecated: its content is now available in @perses-dev/spec,
@perses-dev/client, @perses-dev/components, @perses-dev/plugin-system and @perses-dev/dashboards.
Toolchain and dependencies upgrade#
The Perses npm packages (@perses-dev/client, @perses-dev/components, @perses-dev/plugin-system,
@perses-dev/dashboards, @perses-dev/explore) changed their requirements:
- Node.js 24 (LTS) and npm 11 are now required (
"engines": { "node": ">=24", "npm": ">=11" }). - ESM only: the CommonJS build (
dist/cjs) is no longer published.mainandmoduleboth point to the ES module output. Projects consuming these packages must support ES modules (e.g. norequire('@perses-dev/...')). - React 17 is no longer supported: the peer dependencies are now
reactandreact-dom^18.3.0. - Zod 4:
zodis upgraded from^3.25to^4. The Zod schemas exported by the packages are typed with both their input and output types:z.ZodType<T>becomesz.ZodType<T, T>(e.g.datasourcesSchema,roleSpecSchema,userSchema...). Follow the Zod 4 migration guide for your own schemas. react-hook-formis upgraded to^7.87.0and@hookform/resolversto^5.9.1.@perses-dev/specmust be upgraded to^0.3.0.- The code is compiled with TypeScript 7 and targets ES2023.
If you are maintaining a plugin, the easiest way to align your project is to compare it with a project freshly generated
with percli plugin generate. The main changes are:
- in
package.json:- add
"type": "module"and theenginesfield; - remove the
build:cjsscript and the file.cjs.swcrc; - set
"main": "lib/index.js"; - upgrade the peer dependencies (
@perses-dev/*to^0.55.0,@perses-dev/specto^0.3.0,react/react-domto^18.3.0,react-hook-formto^7.87.0,@hookform/resolversto^5.9.1);
- add
- in
.swcrc, targetes2023; - in
tsconfig.json, targetes2023and use"moduleResolution": "bundler"; - in
rsbuild.config.ts, add@perses-dev/clientto the shared singletons of the Module Federation configuration:
pluginModuleFederation({
// ...
shared: {
// ...
'@perses-dev/client': { singleton: true },
'@perses-dev/components': { singleton: true },
'@perses-dev/plugin-system': { singleton: true },
'@perses-dev/explore': { singleton: true },
'@perses-dev/dashboards': { singleton: true },
},
});Embedding Perses: the host must register its shared modules#
The plugin runtime (Module Federation) is now ESM-native: the Perses packages are no longer loaded with require when a
remote plugin asks for them. Instead, the application hosting the plugins must provide its own instance of the Perses
packages before any plugin is loaded, using registerHostSharedModules. Otherwise, loading a remote plugin fails
with the error Shared module "..." was not registered before a plugin tried to consume it.
// Before
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);// After
import * as PersesClient from '@perses-dev/client';
import * as PersesComponents from '@perses-dev/components';
import * as PersesDashboards from '@perses-dev/dashboards';
import * as PersesExplore from '@perses-dev/explore';
import * as PersesPluginSystem from '@perses-dev/plugin-system';
import { registerHostSharedModules } from '@perses-dev/plugin-system';
import * as PersesSpec from '@perses-dev/spec';
// Provide the host's already-loaded Perses packages to the plugin runtime as Module Federation singletons.
registerHostSharedModules({
'@perses-dev/spec': PersesSpec,
'@perses-dev/client': PersesClient,
'@perses-dev/components': PersesComponents,
'@perses-dev/plugin-system': PersesPluginSystem,
'@perses-dev/explore': PersesExplore,
'@perses-dev/dashboards': PersesDashboards,
});
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);@perses-dev/components: removed hooks and stricter types#
To comply with the React Compiler rules, the following helpers are removed from @perses-dev/components:
useMemoizedanduseDeepMemo: use React'suseMemoinstead.useId(prefix): use React'suseIdinstead.
// Before
import { useId } from '@perses-dev/components';
const id = useId('MyComponent');
// After
import { useId } from 'react';
const id = `MyComponent-${useId()}`;Some types are also stricter (any is replaced by unknown):
@perses-dev/client: thespecof aResourceis nowunknown. Narrow it (or cast it to the expected type) before using it.@perses-dev/components: the cell values of aTableColumnConfigare nowunknown, andcellDescriptionnow takes aCellContext<T, unknown>. In the cell callbacks, usegetValue<ValueType>()when the value type is known, or narrow the value before using it.
@perses-dev/dashboards: annotation store refactor#
The annotation data is now fetched on demand through the TanStack Query cache (like the panel queries) instead of being loaded when the dashboard is opened and copied into the annotation store. As a consequence:
AnnotationHydrationWrapperis removed.- The
annotationStatefield and thesetAnnotationStateaction of the annotation store are removed. useAnnotationsWithData,useAnnotationStatesanduseAnnotationSpecAndStatenow fetch the data themselves. They must be used under theQueryClientProvider, the plugin registry, the time range, the variable and the datasource store providers.usePanelAnnotationsWithDatano longer returns the hidden annotations.

