mirror of
https://github.com/zitadel/zitadel.git
synced 2026-07-25 18:28:00 +00:00
Users deploying ZITADEL against a managed PostgreSQL service (RDS, Cloud SQL, Azure Database, etc.) often do not have superuser access and cannot provide `Admin.*` credentials. The documented workaround — provisioning the user and database manually and then running `start-from-setup` — silently skips schema bootstrapping, causing `relation "eventstore.events" does not exist` errors with no clear recovery path. The root cause is that `zitadel init` conflates two steps that require different privileges without exposing them separately: - **Provisioning step** (`CREATE ROLE`, `CREATE DATABASE`, `GRANT`) — requires superuser. - **Schema bootstrapping step** (create `eventstore`/`projections`/`system` schemas and base tables) — requires only DB owner. Users who handle the provisioning step externally have no supported way to run schema bootstrapping alone. ## Changes - **`cmd/initialise/verify_schema.go`** (renamed from `verify_zitadel.go`): Rename `newZitadel()` → `newSchema()` (internal); rename the `init zitadel` sub-command to `zitadel init schema` (backwards-compatible alias kept) with a clear description that it bootstraps the ZITADEL database schema without admin/superuser privileges. Fix stale error log message to reference `init schema`. - **`cmd/initialise/verify_schema_test.go`** (renamed from `verify_zitadel_test.go`): Test file renamed to match source file. - **`cmd/initialise/init.go`**: Update call site to `newSchema()`. Add guidance in the `init` command's Long description about using `zitadel init schema` for users without admin credentials. - **`cmd/initialise/verify_database.go`**: Add a `pg_database` catalog pre-check before attempting `CREATE DATABASE`, so `zitadel init` with `ADMIN=service_user` no longer fails with `permission denied to create database` when the database was already provisioned externally. - **`cmd/initialise/verify_database_test.go`**: Add test cases covering the new catalog-check skip path, the existing error-skip path, and the error-propagation path when the `pg_database` query itself fails. - **`apps/docs/content/self-hosting/manage/database/index.mdx`**: Inline the `_postgres.mdx` partial (now only PostgreSQL is supported), add a top-level callout, and add a **Managed PostgreSQL / No Admin Access** section with explicit 3-step instructions and security guidance (strong passwords, SSL, TLS). Additional improvements: add inline `# Use 'require' or 'verify-full' for production` comments on `Mode: disable` lines in the YAML example; add clarifying comment to the redundant `GRANT` in the SQL snippet; replace admonition syntax with proper Fumadocs `<Callout>` components; add full database connection env vars to the `start-from-setup` example. - **`apps/docs/content/self-hosting/manage/updating_scaling.mdx`**: Rewrite the init phase description to clearly distinguish the provisioning and schema bootstrapping steps, and document `zitadel init schema` as the path for manual provisioning. Replace admonition syntax with proper Fumadocs `<Callout>` components. - **`apps/docs/content/self-hosting/manage/database/_postgres.mdx`**: Deleted (content merged into `index.mdx`). ## Problem This relates to https://github.com/zitadel/zitadel/discussions/9363 I think we can improve our UX in cases where a user wants to use an external DB and/or does not want to share too broad permissions with zitadel ## Related problems * https://github.com/zitadel/zitadel/issues/10432 * https://github.com/zitadel/zitadel/discussions/8583 * https://github.com/zitadel/zitadel/issues/7903 * https://github.com/zitadel/zitadel/issues/9718 * https://github.com/zitadel/zitadel/issues/8012 * https://github.com/zitadel/zitadel/issues/8558 ## Related PRs https://github.com/zitadel/zitadel/pull/11021
171 lines
3.9 KiB
Go
171 lines
3.9 KiB
Go
package initialise
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
|
|
"github.com/zitadel/zitadel/backend/v3/instrumentation/logging"
|
|
"github.com/zitadel/zitadel/internal/database"
|
|
)
|
|
|
|
var (
|
|
//go:embed sql/*.sql
|
|
stmts embed.FS
|
|
|
|
createUserStmt string
|
|
grantStmt string
|
|
databaseStmt string
|
|
createEventstoreStmt string
|
|
createProjectionsStmt string
|
|
createSystemStmt string
|
|
createEncryptionKeysStmt string
|
|
createEventsStmt string
|
|
createUniqueConstraints string
|
|
|
|
roleAlreadyExistsCode = "42710"
|
|
dbAlreadyExistsCode = "42P04"
|
|
)
|
|
|
|
func New() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "init",
|
|
Short: "initialize ZITADEL instance",
|
|
Long: `Sets up the minimum requirements to start ZITADEL.
|
|
|
|
Prerequisites:
|
|
- PostgreSql database
|
|
|
|
The user provided by flags needs privileges to
|
|
- create the database if it does not exist
|
|
- see other users and create a new one if the user does not exist
|
|
- grant all rights of the ZITADEL database to the user created if not yet set
|
|
|
|
If you don't have admin/superuser credentials (e.g. on a managed PostgreSQL
|
|
service), you can provision the database user and database manually and then
|
|
run only 'zitadel init schema' to bootstrap the required schemas and tables
|
|
using the service user credentials. No admin privileges are needed for that
|
|
sub-command. See the database documentation for details.
|
|
`,
|
|
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
|
defer func() {
|
|
logging.OnError(cmd.Context(), err).Error("zitadel init command failed")
|
|
}()
|
|
config, shutdown, err := NewConfig(cmd, viper.GetViper())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
err = errors.Join(err, shutdown(cmd.Context()))
|
|
}()
|
|
|
|
return InitAll(cmd.Context(), config)
|
|
},
|
|
}
|
|
|
|
cmd.AddCommand(newSchema(), newDatabase(), newUser(), newGrant())
|
|
return cmd
|
|
}
|
|
|
|
func InitAll(ctx context.Context, config *Config) error {
|
|
err := initialise(ctx, config.Database,
|
|
VerifyUser(config.Database.Username(), config.Database.Password()),
|
|
VerifyDatabase(config.Database.DatabaseName()),
|
|
VerifyGrant(config.Database.DatabaseName(), config.Database.Username()),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("initialize database failed: %w", err)
|
|
}
|
|
|
|
err = verifyZitadel(ctx, config.Database)
|
|
if err != nil {
|
|
return fmt.Errorf("initialize ZITADEL failed: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func initialise(ctx context.Context, config database.Config, steps ...func(context.Context, *database.DB) error) error {
|
|
logging.Info(ctx, "initialization started")
|
|
|
|
err := ReadStmts()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
db, err := database.Connect(config, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
|
|
return Init(ctx, db, steps...)
|
|
}
|
|
|
|
func Init(ctx context.Context, db *database.DB, steps ...func(context.Context, *database.DB) error) error {
|
|
for _, step := range steps {
|
|
if err := step(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ReadStmts() (err error) {
|
|
createUserStmt, err = readStmt("01_user")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
databaseStmt, err = readStmt("02_database")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
grantStmt, err = readStmt("03_grant_user")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
createEventstoreStmt, err = readStmt("04_eventstore")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
createProjectionsStmt, err = readStmt("05_projections")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
createSystemStmt, err = readStmt("06_system")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
createEncryptionKeysStmt, err = readStmt("07_encryption_keys_table")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
createEventsStmt, err = readStmt("08_events_table")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
createUniqueConstraints, err = readStmt("10_unique_constraints_table")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func readStmt(step string) (string, error) {
|
|
stmt, err := stmts.ReadFile("sql/" + step + ".sql")
|
|
return string(stmt), err
|
|
}
|