fix: init sub commands (#3218)

* fix(init): add sub commands

* fix(init): admin user in config,
test(init): verify functions

* refactor: config, remove second commands

* refactor: init steps

* chore: fix link in readme

* chore: numerate sql files

* Update cmd/admin/initialise/sql/README.md

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update cmd/admin/initialise/sql/README.md

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* fix(init): remove unused index

* user

* fix database username in defaults.yaml

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
Silvan 2022-02-16 13:30:49 +01:00 committed by GitHub
parent 389eb4a27a
commit 4272ea6fe1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 639 additions and 185 deletions

View File

@ -4,4 +4,23 @@ import "github.com/caos/zitadel/internal/database"
type Config struct { type Config struct {
Database database.Config Database database.Config
AdminUser database.User
}
func adminConfig(config Config) database.Config {
adminConfig := config.Database
adminConfig.Username = config.AdminUser.Username
adminConfig.Password = config.AdminUser.Password
adminConfig.SSL.Cert = config.AdminUser.SSL.Cert
adminConfig.SSL.Key = config.AdminUser.SSL.Key
if config.AdminUser.SSL.RootCert != "" {
adminConfig.SSL.RootCert = config.AdminUser.SSL.RootCert
}
if config.AdminUser.SSL.Mode != "" {
adminConfig.SSL.Mode = config.AdminUser.SSL.Mode
}
//use default database because the zitadel database might not exist
adminConfig.Database = ""
return adminConfig
} }

View File

@ -0,0 +1,28 @@
package initialise
import (
"database/sql"
)
func exists(query string, args ...interface{}) func(*sql.DB) (exists bool, err error) {
return func(db *sql.DB) (exists bool, err error) {
row := db.QueryRow("SELECT EXISTS("+query+")", args...)
err = row.Scan(&exists)
return exists, err
}
}
func exec(stmt string, args ...interface{}) func(*sql.DB) error {
return func(db *sql.DB) error {
_, err := db.Exec(stmt, args...)
return err
}
}
func verify(db *sql.DB, checkExists func(*sql.DB) (bool, error), create func(*sql.DB) error) error {
exists, err := checkExists(db)
if exists || err != nil {
return err
}
return create(db)
}

View File

@ -13,20 +13,6 @@ import (
_ "github.com/lib/pq" _ "github.com/lib/pq"
) )
var (
user string
password string
sslCert string
sslKey string
)
const (
userFlag = "user"
passwordFlag = "password"
sslCertFlag = "ssl-cert"
sslKeyFlag = "ssl-key"
)
func New() *cobra.Command { func New() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "init", Use: "init",
@ -42,52 +28,39 @@ The user provided by flags needs priviledge to
- grant all rights of the ZITADEL database to the user created if not yet set - grant all rights of the ZITADEL database to the user created if not yet set
`, `,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
config := new(Config) config := Config{}
if err := viper.Unmarshal(config); err != nil { if err := viper.Unmarshal(&config); err != nil {
return err return err
} }
return initialise(config, verifyUser, verifyDB, verifyGrant) if err := initialise(config,
verifyUser(config.Database),
verifyDatabase(config.Database),
verifyGrant(config.Database),
); err != nil {
return err
}
return verifyZitadel(config.Database)
}, },
} }
cmd.PersistentFlags().StringVar(&password, passwordFlag, "", "password of the the provided user")
cmd.PersistentFlags().StringVar(&sslCert, sslCertFlag, "", "ssl cert from the provided user")
cmd.PersistentFlags().StringVar(&sslKey, sslKeyFlag, "", "ssl key from the provided user")
cmd.PersistentFlags().StringVar(&user, userFlag, "", "(required) the user to check if the database, user and grants exists and create if not")
cmd.MarkPersistentFlagRequired(userFlag)
cmd.AddCommand(newZitadel(), newDatabase(), newUser(), newGrant()) cmd.AddCommand(newZitadel(), newDatabase(), newUser(), newGrant())
return cmd return cmd
} }
func adminConfig(config database.Config) database.Config { func initialise(config Config, steps ...func(*sql.DB) error) error {
adminConfig := config
adminConfig.User = user
adminConfig.Password = password
adminConfig.SSL.Cert = sslCert
adminConfig.SSL.Key = sslKey
return adminConfig
}
func initialise(config *Config, steps ...func(*sql.DB, database.Config) error) error {
logging.Info("initialization started") logging.Info("initialization started")
db, err := database.Connect(adminConfig(config.Database)) db, err := database.Connect(adminConfig(config))
if err != nil { if err != nil {
return err return err
} }
for _, step := range steps { for _, step := range steps {
if err = step(db, config.Database); err != nil { if err = step(db); err != nil {
return err return err
} }
} }
if err = db.Close(); err != nil { return db.Close()
return err
}
return verifyZitadel(config.Database)
} }

View File

@ -0,0 +1,82 @@
package initialise
import (
"database/sql"
"database/sql/driver"
"regexp"
"testing"
"github.com/DATA-DOG/go-sqlmock"
)
type db struct {
mock sqlmock.Sqlmock
db *sql.DB
}
func prepareDB(t *testing.T, expectations ...expectation) db {
t.Helper()
client, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("unable to create sql mock: %v", err)
}
for _, expectation := range expectations {
expectation(mock)
}
return db{
mock: mock,
db: client,
}
}
type expectation func(m sqlmock.Sqlmock)
func expectExists(query string, value bool, args ...driver.Value) expectation {
return func(m sqlmock.Sqlmock) {
m.ExpectQuery(regexp.QuoteMeta(query)).WithArgs(args...).WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(value))
}
}
func expectQueryErr(query string, err error, args ...driver.Value) expectation {
return func(m sqlmock.Sqlmock) {
m.ExpectQuery(regexp.QuoteMeta(query)).WithArgs(args...).WillReturnError(err)
}
}
func expectExec(stmt string, err error, args ...driver.Value) expectation {
return func(m sqlmock.Sqlmock) {
query := m.ExpectExec(regexp.QuoteMeta(stmt)).WithArgs(args...)
if err != nil {
query.WillReturnError(err)
return
}
query.WillReturnResult(sqlmock.NewResult(1, 1))
}
}
func expectBegin(err error) expectation {
return func(m sqlmock.Sqlmock) {
query := m.ExpectBegin()
if err != nil {
query.WillReturnError(err)
}
}
}
func expectCommit(err error) expectation {
return func(m sqlmock.Sqlmock) {
query := m.ExpectCommit()
if err != nil {
query.WillReturnError(err)
}
}
}
func expectRollback(err error) expectation {
return func(m sqlmock.Sqlmock) {
query := m.ExpectRollback()
if err != nil {
query.WillReturnError(err)
}
}
}

View File

@ -0,0 +1 @@
CREATE USER $1 WITH PASSWORD $2

View File

@ -0,0 +1,2 @@
-- replace %[1]s with the name of the database
CREATE DATABASE %[1]s

View File

@ -0,0 +1,3 @@
-- replace the first %[1]s with the database
-- replace the second \%[2]s with the user
GRANT ALL ON DATABASE %[1]s TO %[2]s

View File

@ -0,0 +1 @@
CREATE SCHEMA eventstore

View File

@ -0,0 +1 @@
CREATE SCHEMA projections

View File

@ -0,0 +1 @@
SET experimental_enable_hash_sharded_indexes = on

View File

@ -0,0 +1,24 @@
CREATE TABLE eventstore.events (
id UUID DEFAULT gen_random_uuid()
, event_type TEXT NOT NULL
, aggregate_type TEXT NOT NULL
, aggregate_id TEXT NOT NULL
, aggregate_version TEXT NOT NULL
, event_sequence BIGINT NOT NULL
, previous_aggregate_sequence BIGINT
, previous_aggregate_type_sequence INT8
, creation_date TIMESTAMPTZ NOT NULL DEFAULT now()
, event_data JSONB
, editor_user TEXT NOT NULL
, editor_service TEXT NOT NULL
, resource_owner TEXT NOT NULL
, PRIMARY KEY (event_sequence DESC) USING HASH WITH BUCKET_COUNT = 10
, INDEX agg_type_agg_id (aggregate_type, aggregate_id)
, INDEX agg_type (aggregate_type)
, INDEX agg_type_seq (aggregate_type, event_sequence DESC)
STORING (id, event_type, aggregate_id, aggregate_version, previous_aggregate_sequence, creation_date, event_data, editor_user, editor_service, resource_owner, previous_aggregate_type_sequence)
, INDEX max_sequence (aggregate_type, aggregate_id, event_sequence DESC)
, CONSTRAINT previous_sequence_unique UNIQUE (previous_aggregate_sequence DESC)
, CONSTRAINT prev_agg_type_seq_unique UNIQUE(previous_aggregate_type_sequence)
)

View File

@ -0,0 +1,14 @@
# SQL initialisation
The sql-files in this folder initialize the ZITADEL database and user. These objects need to exist before ZITADEL is able to set and start up.
## files
- 01_user.sql: create the user zitadel uses to connect to the database
- 02_database.sql: create the database for zitadel
- 03_grant_user.sql: grants the user created before to have full access to its database. The user needs full access to the database because zitadel makes ddl/dml on runtime
- 04_eventstore.sql: creates the schema needed for eventsourcing
- 05_projections.sql: creates the schema needed to read the data
- files 06_enable_hash_sharded_indexes.sql and 07_events_table.sql must run in the same session
- 06_enable_hash_sharded_indexes.sql enables the [hash sharded index](https://www.cockroachlabs.com/docs/stable/hash-sharded-indexes.html) feature for this session
- 07_events_table.sql creates the table for eventsourcing

View File

@ -2,13 +2,21 @@ package initialise
import ( import (
"database/sql" "database/sql"
_ "embed"
"fmt"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/database" "github.com/caos/zitadel/internal/database"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
var (
searchDatabase = "SELECT database_name FROM [show databases] WHERE database_name = $1"
//go:embed sql/02_database.sql
databaseStmt string
)
func newDatabase() *cobra.Command { func newDatabase() *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "database", Use: "database",
@ -24,31 +32,20 @@ The user provided by flags needs priviledge to
- grant all rights of the ZITADEL database to the user created if not yet set - grant all rights of the ZITADEL database to the user created if not yet set
`, `,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
config := new(Config) config := Config{}
if err := viper.Unmarshal(config); err != nil { if err := viper.Unmarshal(&config); err != nil {
return err return err
} }
return initialise(config, verifyDB) return initialise(config, verifyDatabase(config.Database))
}, },
} }
} }
func verifyDB(db *sql.DB, config database.Config) error { func verifyDatabase(config database.Config) func(*sql.DB) error {
logging.Info("verify database") return func(db *sql.DB) error {
exists, err := existsDatabase(db, config) return verify(db,
if exists || err != nil { exists(searchDatabase, config.Database),
return err exec(fmt.Sprintf(databaseStmt, config.Database)),
)
} }
return createDatabase(db, config)
}
func existsDatabase(db *sql.DB, config database.Config) (exists bool, err error) {
row := db.QueryRow("SELECT EXISTS(SELECT database_name FROM [show databases] WHERE database_name = $1)", config.Database)
err = row.Scan(&exists)
return exists, err
}
func createDatabase(db *sql.DB, config database.Config) error {
_, err := db.Exec("CREATE DATABASE " + config.Database)
return err
} }

View File

@ -0,0 +1,80 @@
package initialise
import (
"database/sql"
"errors"
"testing"
"github.com/caos/zitadel/internal/database"
)
func Test_verifyDB(t *testing.T) {
type args struct {
db db
config database.Config
}
tests := []struct {
name string
args args
targetErr error
}{
{
name: "exists fails",
args: args{
db: prepareDB(t, expectQueryErr("SELECT EXISTS(SELECT database_name FROM [show databases] WHERE database_name = $1)", sql.ErrConnDone, "zitadel")),
config: database.Config{
Database: "zitadel",
},
},
targetErr: sql.ErrConnDone,
},
{
name: "doesn't exists, create fails",
args: args{
db: prepareDB(t,
expectExists("SELECT EXISTS(SELECT database_name FROM [show databases] WHERE database_name = $1)", false, "zitadel"),
expectExec("CREATE DATABASE zitadel", sql.ErrTxDone),
),
config: database.Config{
Database: "zitadel",
},
},
targetErr: sql.ErrTxDone,
},
{
name: "doesn't exists, create successful",
args: args{
db: prepareDB(t,
expectExists("SELECT EXISTS(SELECT database_name FROM [show databases] WHERE database_name = $1)", false, "zitadel"),
expectExec("CREATE DATABASE zitadel", nil),
),
config: database.Config{
Database: "zitadel",
},
},
targetErr: nil,
},
{
name: "already exists",
args: args{
db: prepareDB(t,
expectExists("SELECT EXISTS(SELECT database_name FROM [show databases] WHERE database_name = $1)", true, "zitadel"),
),
config: database.Config{
Database: "zitadel",
},
},
targetErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := verifyDatabase(tt.args.config)(tt.args.db.db); !errors.Is(err, tt.targetErr) {
t.Errorf("verifyDB() error = %v, want: %v", err, tt.targetErr)
}
if err := tt.args.db.mock.ExpectationsWereMet(); err != nil {
t.Error(err)
}
})
}
}

View File

@ -2,6 +2,8 @@ package initialise
import ( import (
"database/sql" "database/sql"
_ "embed"
"fmt"
"github.com/caos/logging" "github.com/caos/logging"
"github.com/caos/zitadel/internal/database" "github.com/caos/zitadel/internal/database"
@ -9,6 +11,12 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
var (
searchGrant = "SELECT * FROM [SHOW GRANTS ON DATABASE %s] where grantee = $1 AND privilege_type = 'ALL'"
//go:embed sql/03_grant_user.sql
grantStmt string
)
func newGrant() *cobra.Command { func newGrant() *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "grant", Use: "grant",
@ -19,31 +27,21 @@ Prereqesits:
- cockroachdb - cockroachdb
`, `,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
config := new(Config) config := Config{}
if err := viper.Unmarshal(config); err != nil { if err := viper.Unmarshal(&config); err != nil {
return err return err
} }
return initialise(config, verifyGrant) return initialise(config, verifyGrant(config.Database))
}, },
} }
} }
func verifyGrant(db *sql.DB, config database.Config) error { func verifyGrant(config database.Config) func(*sql.DB) error {
logging.Info("verify grant") return func(db *sql.DB) error {
exists, err := hasGrant(db, config) logging.WithFields("user", config.Username).Info("verify grant")
if exists || err != nil { return verify(db,
return err exists(fmt.Sprintf(searchGrant, config.Database), config.Username),
exec(fmt.Sprintf(grantStmt, config.Database, config.Username)),
)
} }
return grant(db, config)
}
func hasGrant(db *sql.DB, config database.Config) (has bool, err error) {
row := db.QueryRow("SELECT EXISTS(SELECT * FROM [SHOW GRANTS ON DATABASE "+config.Database+"] where grantee = $1 AND privilege_type = 'ALL')", config.User)
err = row.Scan(&has)
return has, err
}
func grant(db *sql.DB, config database.Config) error {
_, err := db.Exec("GRANT ALL ON DATABASE " + config.Database + " TO " + config.User)
return err
} }

View File

@ -0,0 +1,92 @@
package initialise
import (
"database/sql"
"errors"
"testing"
"github.com/caos/zitadel/internal/database"
)
func Test_verifyGrant(t *testing.T) {
type args struct {
db db
config database.Config
}
tests := []struct {
name string
args args
targetErr error
}{
{
name: "exists fails",
args: args{
db: prepareDB(t, expectQueryErr("SELECT EXISTS(SELECT * FROM [SHOW GRANTS ON DATABASE zitadel] where grantee = $1 AND privilege_type = 'ALL'", sql.ErrConnDone, "zitadel-user")),
config: database.Config{
Database: "zitadel",
User: database.User{
Username: "zitadel-user",
},
},
},
targetErr: sql.ErrConnDone,
},
{
name: "doesn't exists, create fails",
args: args{
db: prepareDB(t,
expectExists("SELECT EXISTS(SELECT * FROM [SHOW GRANTS ON DATABASE zitadel] where grantee = $1 AND privilege_type = 'ALL'", false, "zitadel-user"),
expectExec("GRANT ALL ON DATABASE zitadel TO zitadel-user", sql.ErrTxDone),
),
config: database.Config{
Database: "zitadel",
User: database.User{
Username: "zitadel-user",
},
},
},
targetErr: sql.ErrTxDone,
},
{
name: "correct",
args: args{
db: prepareDB(t,
expectExists("SELECT EXISTS(SELECT * FROM [SHOW GRANTS ON DATABASE zitadel] where grantee = $1 AND privilege_type = 'ALL'", false, "zitadel-user"),
expectExec("GRANT ALL ON DATABASE zitadel TO zitadel-user", nil),
),
config: database.Config{
Database: "zitadel",
User: database.User{
Username: "zitadel-user",
},
},
},
targetErr: nil,
},
{
name: "already exists",
args: args{
db: prepareDB(t,
expectExists("SELECT EXISTS(SELECT * FROM [SHOW GRANTS ON DATABASE zitadel] where grantee = $1 AND privilege_type = 'ALL'", true, "zitadel-user"),
),
config: database.Config{
Database: "zitadel",
User: database.User{
Username: "zitadel-user",
},
},
},
targetErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := verifyGrant(tt.args.config)(tt.args.db.db); !errors.Is(err, tt.targetErr) {
t.Errorf("verifyGrant() error = %v, want: %v", err, tt.targetErr)
}
if err := tt.args.db.mock.ExpectationsWereMet(); err != nil {
t.Error(err)
}
})
}
}

View File

@ -2,6 +2,7 @@ package initialise
import ( import (
"database/sql" "database/sql"
_ "embed"
"github.com/caos/logging" "github.com/caos/logging"
"github.com/caos/zitadel/internal/database" "github.com/caos/zitadel/internal/database"
@ -9,6 +10,12 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
var (
searchUser = "SELECT username FROM [show roles] WHERE username = $1"
//go:embed sql/01_user.sql
createUserStmt string
)
func newUser() *cobra.Command { func newUser() *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "user", Use: "user",
@ -24,31 +31,21 @@ The user provided by flags needs priviledge to
- grant all rights of the ZITADEL database to the user created if not yet set - grant all rights of the ZITADEL database to the user created if not yet set
`, `,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
config := new(Config) config := Config{}
if err := viper.Unmarshal(config); err != nil { if err := viper.Unmarshal(&config); err != nil {
return err return err
} }
return initialise(config, verifyUser) return initialise(config, verifyUser(config.Database))
}, },
} }
} }
func verifyUser(db *sql.DB, config database.Config) error { func verifyUser(config database.Config) func(*sql.DB) error {
logging.Info("verify user") return func(db *sql.DB) error {
exists, err := existsUser(db, config) logging.WithFields("username", config.Username).Info("verify user")
if exists || err != nil { return verify(db,
return err exists(searchUser, config.Username),
exec(createUserStmt, config.Username, &sql.NullString{String: config.Password, Valid: config.Password != ""}),
)
} }
return createUser(db, config)
}
func existsUser(db *sql.DB, config database.Config) (exists bool, err error) {
row := db.QueryRow("SELECT EXISTS(SELECT username FROM [show roles] WHERE username = $1)", config.User)
err = row.Scan(&exists)
return exists, err
}
func createUser(db *sql.DB, config database.Config) error {
_, err := db.Exec("CREATE USER $1 WITH PASSWORD $2", config.User, &sql.NullString{String: config.Password, Valid: config.Password != ""})
return err
} }

View File

@ -0,0 +1,109 @@
package initialise
import (
"database/sql"
"errors"
"testing"
"github.com/caos/zitadel/internal/database"
)
func Test_verifyUser(t *testing.T) {
type args struct {
db db
config database.Config
}
tests := []struct {
name string
args args
targetErr error
}{
{
name: "exists fails",
args: args{
db: prepareDB(t, expectQueryErr("SELECT EXISTS(SELECT username FROM [show roles] WHERE username = $1)", sql.ErrConnDone, "zitadel-user")),
config: database.Config{
Database: "zitadel",
User: database.User{
Username: "zitadel-user",
},
},
},
targetErr: sql.ErrConnDone,
},
{
name: "doesn't exists, create fails",
args: args{
db: prepareDB(t,
expectExists("SELECT EXISTS(SELECT username FROM [show roles] WHERE username = $1)", false, "zitadel-user"),
expectExec("CREATE USER $1 WITH PASSWORD $2", sql.ErrTxDone, "zitadel-user", nil),
),
config: database.Config{
Database: "zitadel",
User: database.User{
Username: "zitadel-user",
},
},
},
targetErr: sql.ErrTxDone,
},
{
name: "correct without password",
args: args{
db: prepareDB(t,
expectExists("SELECT EXISTS(SELECT username FROM [show roles] WHERE username = $1)", false, "zitadel-user"),
expectExec("CREATE USER $1 WITH PASSWORD $2", nil, "zitadel-user", nil),
),
config: database.Config{
Database: "zitadel",
User: database.User{
Username: "zitadel-user",
},
},
},
targetErr: nil,
},
{
name: "correct with password",
args: args{
db: prepareDB(t,
expectExists("SELECT EXISTS(SELECT username FROM [show roles] WHERE username = $1)", false, "zitadel-user"),
expectExec("CREATE USER $1 WITH PASSWORD $2", nil, "zitadel-user", "password"),
),
config: database.Config{
Database: "zitadel",
User: database.User{
Username: "zitadel-user",
Password: "password",
},
},
},
targetErr: nil,
},
{
name: "already exists",
args: args{
db: prepareDB(t,
expectExists("SELECT EXISTS(SELECT username FROM [show roles] WHERE username = $1)", true, "zitadel-user"),
),
config: database.Config{
Database: "zitadel",
User: database.User{
Username: "zitadel-user",
},
},
},
targetErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := verifyUser(tt.args.config)(tt.args.db.db); !errors.Is(err, tt.targetErr) {
t.Errorf("verifyGrant() error = %v, want: %v", err, tt.targetErr)
}
if err := tt.args.db.mock.ExpectationsWereMet(); err != nil {
t.Error(err)
}
})
}
}

View File

@ -2,8 +2,8 @@ package initialise
import ( import (
"database/sql" "database/sql"
_ "embed"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/database" "github.com/caos/zitadel/internal/database"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -12,7 +12,19 @@ import (
const ( const (
eventstoreSchema = "eventstore" eventstoreSchema = "eventstore"
projectionsSchema = "projections" projectionsSchema = "projections"
eventsTable = "events" )
var (
searchEventsTable = "SELECT table_name FROM [SHOW TABLES] WHERE table_name = 'events'"
searchSchema = "SELECT schema_name FROM [SHOW SCHEMAS] WHERE schema_name = $1"
//go:embed sql/06_enable_hash_sharded_indexes.sql
enableHashShardedIdx string
//go:embed sql/07_events_table.sql
createEventsStmt string
//go:embed sql/05_projections.sql
createProjectionsStmt string
//go:embed sql/04_eventstore.sql
createEventstoreStmt string
) )
func newZitadel() *cobra.Command { func newZitadel() *cobra.Command {
@ -29,7 +41,7 @@ Prereqesits:
if err := viper.Unmarshal(config); err != nil { if err := viper.Unmarshal(config); err != nil {
return err return err
} }
return initialise(config, verifyUser) return verifyZitadel(config.Database)
}, },
} }
} }
@ -40,93 +52,32 @@ func verifyZitadel(config database.Config) error {
return err return err
} }
if err := verifySchema(db, config, projectionsSchema); err != nil { if err := verify(db, exists(searchSchema, projectionsSchema), exec(createProjectionsStmt)); err != nil {
return err return err
} }
if err := verifySchema(db, config, eventstoreSchema); err != nil { if err := verify(db, exists(searchSchema, eventstoreSchema), exec(createEventstoreStmt)); err != nil {
return err return err
} }
if err := verifyEvents(db, config); err != nil { if err := verify(db, exists(searchSchema, projectionsSchema), createEvents); err != nil {
return err return err
} }
return db.Close() return db.Close()
} }
func verifySchema(db *sql.DB, config database.Config, schema string) error { func createEvents(db *sql.DB) error {
logging.WithFields("schema", schema).Info("verify schema")
exists, err := existsSchema(db, config, schema)
if exists || err != nil {
return err
}
return createSchema(db, config, schema)
}
func existsSchema(db *sql.DB, config database.Config, schema string) (exists bool, err error) {
row := db.QueryRow("SELECT EXISTS(SELECT schema_name FROM [SHOW SCHEMAS] WHERE schema_name = $1)", schema)
err = row.Scan(&exists)
return exists, err
}
func createSchema(db *sql.DB, config database.Config, schema string) error {
_, err := db.Exec("CREATE SCHEMA " + schema)
return err
}
func verifyEvents(db *sql.DB, config database.Config) error {
logging.Info("verify events table")
exists, err := existsEvents(db, config)
if exists || err != nil {
return err
}
return createEvents(db, config)
}
func existsEvents(db *sql.DB, config database.Config) (exists bool, err error) {
row := db.QueryRow("SELECT EXISTS(SELECT table_name FROM [SHOW TABLES] WHERE table_name = $1)", eventsTable)
err = row.Scan(&exists)
return exists, err
}
func createEvents(db *sql.DB, config database.Config) error {
tx, err := db.Begin() tx, err := db.Begin()
if err != nil { if err != nil {
return err return err
} }
if _, err = tx.Exec("SET experimental_enable_hash_sharded_indexes = on"); err != nil { if _, err = tx.Exec(enableHashShardedIdx); err != nil {
tx.Rollback() tx.Rollback()
return err return err
} }
stmt := `CREATE TABLE eventstore.events ( if _, err = tx.Exec(createEventsStmt); err != nil {
id UUID DEFAULT gen_random_uuid()
, event_type TEXT NOT NULL
, aggregate_type TEXT NOT NULL
, aggregate_id TEXT NOT NULL
, aggregate_version TEXT NOT NULL
, event_sequence BIGINT NOT NULL
, previous_aggregate_sequence BIGINT
, previous_aggregate_type_sequence INT8
, creation_date TIMESTAMPTZ NOT NULL DEFAULT now()
, event_data JSONB
, editor_user TEXT NOT NULL
, editor_service TEXT NOT NULL
, resource_owner TEXT NOT NULL
, PRIMARY KEY (event_sequence DESC) USING HASH WITH BUCKET_COUNT = 10
, INDEX agg_type_agg_id (aggregate_type, aggregate_id)
, INDEX agg_type (aggregate_type)
, INDEX agg_type_seq (aggregate_type, event_sequence DESC)
STORING (id, event_type, aggregate_id, aggregate_version, previous_aggregate_sequence, creation_date, event_data, editor_user, editor_service, resource_owner, previous_aggregate_type_sequence)
, INDEX changes_idx (aggregate_type, aggregate_id, creation_date) USING HASH WITH BUCKET_COUNT = 10
, INDEX max_sequence (aggregate_type, aggregate_id, event_sequence DESC)
, CONSTRAINT previous_sequence_unique UNIQUE (previous_aggregate_sequence DESC)
, CONSTRAINT prev_agg_type_seq_unique UNIQUE(previous_aggregate_type_sequence)
)`
if _, err = tx.Exec(stmt); err != nil {
tx.Rollback() tx.Rollback()
return err return err
} }

View File

@ -0,0 +1,73 @@
package initialise
import (
"database/sql"
"errors"
"testing"
)
func Test_verifyEvents(t *testing.T) {
type args struct {
db db
}
tests := []struct {
name string
args args
targetErr error
}{
{
name: "unable to begin",
args: args{
db: prepareDB(t,
expectBegin(sql.ErrConnDone),
),
},
targetErr: sql.ErrConnDone,
},
{
name: "hash sharded indexes fails",
args: args{
db: prepareDB(t,
expectBegin(nil),
expectExec("SET experimental_enable_hash_sharded_indexes = on", sql.ErrNoRows),
expectRollback(nil),
),
},
targetErr: sql.ErrNoRows,
},
{
name: "create table fails",
args: args{
db: prepareDB(t,
expectBegin(nil),
expectExec("SET experimental_enable_hash_sharded_indexes = on", nil),
expectExec(createEventsStmt, sql.ErrNoRows),
expectRollback(nil),
),
},
targetErr: sql.ErrNoRows,
},
{
name: "correct",
args: args{
db: prepareDB(t,
expectBegin(nil),
expectExec("SET experimental_enable_hash_sharded_indexes = on", nil),
expectExec(createEventsStmt, nil),
expectCommit(nil),
),
},
targetErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := createEvents(tt.args.db.db); !errors.Is(err, tt.targetErr) {
t.Errorf("createEvents() error = %v, want: %v", err, tt.targetErr)
}
if err := tt.args.db.mock.ExpectationsWereMet(); err != nil {
t.Error(err)
}
})
}
}

View File

@ -11,19 +11,23 @@ ExternalSecure: true
Database: Database:
Host: localhost Host: localhost
Port: 26257 Port: 26257
User: zitadel
Database: zitadel Database: zitadel
Password: ""
MaxOpenConns: 20 MaxOpenConns: 20
MaxConnLifetime: 30m MaxConnLifetime: 30m
MaxConnIdleTime: 30m MaxConnIdleTime: 30m
Options: "" Options: ""
User:
Username: zitadel
Password: ""
SSL: SSL:
Mode: diabled Mode: diabled
RootCert: "" RootCert: ""
Cert: "" Cert: ""
Key: "" Key: ""
AdminUser:
Username: root
Projections: Projections:
Config: Config:
RequeueEvery: 10s RequeueEvery: 10s

View File

@ -14,19 +14,23 @@ const (
type Config struct { type Config struct {
Host string Host string
Port string Port string
User string
Password string
Database string Database string
SSL SSL
MaxOpenConns uint32 MaxOpenConns uint32
MaxConnLifetime time.Duration MaxConnLifetime time.Duration
MaxConnIdleTime time.Duration MaxConnIdleTime time.Duration
User
//Additional options to be appended as options=<Options> //Additional options to be appended as options=<Options>
//The value will be taken as is. Multiple options are space separated. //The value will be taken as is. Multiple options are space separated.
Options string Options string
} }
type User struct {
Username string
Password string
SSL SSL
}
type SSL struct { type SSL struct {
// type of connection security // type of connection security
Mode string Mode string
@ -57,7 +61,7 @@ func (c Config) String() string {
fields := []string{ fields := []string{
"host=" + c.Host, "host=" + c.Host,
"port=" + c.Port, "port=" + c.Port,
"user=" + c.User, "user=" + c.Username,
"dbname=" + c.Database, "dbname=" + c.Database,
"application_name=zitadel", "application_name=zitadel",
"sslmode=" + c.SSL.Mode, "sslmode=" + c.SSL.Mode,