2022-02-09 14:01:19 +00:00
package setup
import (
2022-03-23 08:02:39 +00:00
"context"
2023-03-01 00:11:23 +00:00
"embed"
2022-02-09 14:01:19 +00:00
_ "embed"
2024-01-25 16:28:20 +00:00
"net/http"
2022-02-09 14:01:19 +00:00
"github.com/spf13/cobra"
2022-03-23 08:02:39 +00:00
"github.com/spf13/viper"
2022-04-26 23:01:45 +00:00
"github.com/zitadel/logging"
2022-03-23 08:02:39 +00:00
2022-11-04 09:21:58 +00:00
"github.com/zitadel/zitadel/cmd/build"
2024-01-25 16:28:20 +00:00
"github.com/zitadel/zitadel/cmd/encryption"
2022-06-27 10:32:34 +00:00
"github.com/zitadel/zitadel/cmd/key"
"github.com/zitadel/zitadel/cmd/tls"
2024-01-25 16:28:20 +00:00
admin_handler "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/handler"
admin_view "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/view"
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
auth_handler "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/handler"
auth_view "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view"
"github.com/zitadel/zitadel/internal/authz"
authz_es "github.com/zitadel/zitadel/internal/authz/repository/eventsourcing/eventstore"
"github.com/zitadel/zitadel/internal/command"
cryptoDB "github.com/zitadel/zitadel/internal/crypto/database"
2022-04-26 23:01:45 +00:00
"github.com/zitadel/zitadel/internal/database"
2023-12-20 16:13:04 +00:00
"github.com/zitadel/zitadel/internal/database/dialect"
2024-01-25 16:28:20 +00:00
"github.com/zitadel/zitadel/internal/domain"
2022-04-26 23:01:45 +00:00
"github.com/zitadel/zitadel/internal/eventstore"
2023-10-19 10:19:10 +00:00
old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql"
new_es "github.com/zitadel/zitadel/internal/eventstore/v3"
2023-12-05 11:12:01 +00:00
"github.com/zitadel/zitadel/internal/i18n"
2022-04-26 23:01:45 +00:00
"github.com/zitadel/zitadel/internal/migration"
2024-01-25 16:28:20 +00:00
notify_handler "github.com/zitadel/zitadel/internal/notification"
"github.com/zitadel/zitadel/internal/query"
2022-11-04 09:21:58 +00:00
"github.com/zitadel/zitadel/internal/query/projection"
2024-05-30 09:35:30 +00:00
es_v4 "github.com/zitadel/zitadel/internal/v2/eventstore"
es_v4_pg "github.com/zitadel/zitadel/internal/v2/eventstore/postgres"
2024-01-25 16:28:20 +00:00
"github.com/zitadel/zitadel/internal/webauthn"
2022-03-23 08:02:39 +00:00
)
var (
//go:embed steps.yaml
defaultSteps [ ] byte
2022-04-25 15:05:20 +00:00
stepFiles [ ] string
2022-02-09 14:01:19 +00:00
)
func New ( ) * cobra . Command {
2022-04-25 15:05:20 +00:00
cmd := & cobra . Command {
2022-02-09 14:01:19 +00:00
Use : "setup" ,
Short : "setup ZITADEL instance" ,
Long : ` sets up data to start ZITADEL .
Requirements :
- cockroachdb ` ,
2022-03-23 08:02:39 +00:00
Run : func ( cmd * cobra . Command , args [ ] string ) {
2022-06-24 12:38:22 +00:00
err := tls . ModeFromFlag ( cmd )
logging . OnError ( err ) . Fatal ( "invalid tlsMode" )
2024-01-25 16:28:20 +00:00
err = BindInitProjections ( cmd )
logging . OnError ( err ) . Fatal ( "unable to bind \"init-projections\" flag" )
2024-05-30 09:35:30 +00:00
err = bindForMirror ( cmd )
logging . OnError ( err ) . Fatal ( "unable to bind \"for-mirror\" flag" )
2022-03-28 08:05:09 +00:00
config := MustNewConfig ( viper . GetViper ( ) )
steps := MustNewSteps ( viper . New ( ) )
2022-03-23 08:02:39 +00:00
2022-04-12 14:20:17 +00:00
masterKey , err := key . MasterKey ( cmd )
logging . OnError ( err ) . Panic ( "No master key provided" )
2024-05-30 09:35:30 +00:00
Setup ( cmd . Context ( ) , config , steps , masterKey )
2022-02-09 14:01:19 +00:00
} ,
}
2022-04-25 15:05:20 +00:00
2023-04-28 11:55:35 +00:00
cmd . AddCommand ( NewCleanup ( ) )
2022-04-25 15:05:20 +00:00
Flags ( cmd )
return cmd
}
func Flags ( cmd * cobra . Command ) {
cmd . PersistentFlags ( ) . StringArrayVar ( & stepFiles , "steps" , nil , "paths to step files to overwrite default steps" )
2024-01-25 16:28:20 +00:00
cmd . Flags ( ) . Bool ( "init-projections" , viper . GetBool ( "InitProjections" ) , "beta feature: initializes projections after they are created, allows smooth start as projections are up to date" )
2024-05-30 09:35:30 +00:00
cmd . Flags ( ) . Bool ( "for-mirror" , viper . GetBool ( "ForMirror" ) , "use this flag if you want to mirror your existing data" )
2022-04-25 15:05:20 +00:00
key . AddMasterKeyFlag ( cmd )
2022-06-24 12:38:22 +00:00
tls . AddTLSModeFlag ( cmd )
2022-02-09 14:01:19 +00:00
}
2022-03-23 08:02:39 +00:00
2024-01-25 16:28:20 +00:00
func BindInitProjections ( cmd * cobra . Command ) error {
return viper . BindPFlag ( "InitProjections.Enabled" , cmd . Flags ( ) . Lookup ( "init-projections" ) )
}
2024-05-30 09:35:30 +00:00
func bindForMirror ( cmd * cobra . Command ) error {
return viper . BindPFlag ( "ForMirror" , cmd . Flags ( ) . Lookup ( "for-mirror" ) )
}
func Setup ( ctx context . Context , config * Config , steps * Steps , masterKey string ) {
2022-07-20 09:20:49 +00:00
logging . Info ( "setup started" )
2023-12-05 11:12:01 +00:00
i18n . MustLoadSupportedLanguagesFromDir ( )
2023-12-20 16:13:04 +00:00
queryDBClient , err := database . Connect ( config . Database , false , dialect . DBPurposeQuery )
2023-10-19 10:19:10 +00:00
logging . OnError ( err ) . Fatal ( "unable to connect to database" )
2023-12-20 16:13:04 +00:00
esPusherDBClient , err := database . Connect ( config . Database , false , dialect . DBPurposeEventPusher )
logging . OnError ( err ) . Fatal ( "unable to connect to database" )
projectionDBClient , err := database . Connect ( config . Database , false , dialect . DBPurposeProjectionSpooler )
2022-03-23 08:02:39 +00:00
logging . OnError ( err ) . Fatal ( "unable to connect to database" )
2023-12-20 16:13:04 +00:00
config . Eventstore . Querier = old_es . NewCRDB ( queryDBClient )
2024-07-03 15:00:56 +00:00
esV3 := new_es . NewEventstore ( esPusherDBClient )
config . Eventstore . Pusher = esV3
config . Eventstore . Searcher = esV3
2023-10-19 10:19:10 +00:00
eventstoreClient := eventstore . NewEventstore ( config . Eventstore )
2024-07-03 15:00:56 +00:00
2022-03-23 08:02:39 +00:00
logging . OnError ( err ) . Fatal ( "unable to start eventstore" )
2024-05-30 09:35:30 +00:00
eventstoreV4 := es_v4 . NewEventstoreFromOne ( es_v4_pg . New ( queryDBClient , & es_v4_pg . Config {
MaxRetries : config . Eventstore . MaxRetries ,
} ) )
2022-03-28 08:05:09 +00:00
2023-12-20 16:13:04 +00:00
steps . s1ProjectionTable = & ProjectionTable { dbClient : queryDBClient . DB }
steps . s2AssetsTable = & AssetTable { dbClient : queryDBClient . DB }
2022-04-13 05:42:48 +00:00
2024-05-30 09:35:30 +00:00
steps . FirstInstance . Skip = config . ForMirror || steps . FirstInstance . Skip
2022-07-27 08:22:20 +00:00
steps . FirstInstance . instanceSetup = config . DefaultInstance
steps . FirstInstance . userEncryptionKey = config . EncryptionKeys . User
steps . FirstInstance . smtpEncryptionKey = config . EncryptionKeys . SMTP
2023-06-15 06:16:39 +00:00
steps . FirstInstance . oidcEncryptionKey = config . EncryptionKeys . OIDC
2022-07-27 08:22:20 +00:00
steps . FirstInstance . masterKey = masterKey
2023-12-20 16:13:04 +00:00
steps . FirstInstance . db = queryDBClient
2022-07-27 08:22:20 +00:00
steps . FirstInstance . es = eventstoreClient
steps . FirstInstance . defaults = config . SystemDefaults
steps . FirstInstance . zitadelRoles = config . InternalAuthZ . RolePermissionMappings
steps . FirstInstance . externalDomain = config . ExternalDomain
steps . FirstInstance . externalSecure = config . ExternalSecure
steps . FirstInstance . externalPort = config . ExternalPort
2022-03-23 08:02:39 +00:00
2023-12-20 16:13:04 +00:00
steps . s5LastFailed = & LastFailed { dbClient : queryDBClient . DB }
steps . s6OwnerRemoveColumns = & OwnerRemoveColumns { dbClient : queryDBClient . DB }
steps . s7LogstoreTables = & LogstoreTables { dbClient : queryDBClient . DB , username : config . Database . Username ( ) , dbType : config . Database . Type ( ) }
steps . s8AuthTokens = & AuthTokenIndexes { dbClient : queryDBClient }
2023-10-19 10:19:10 +00:00
steps . CorrectCreationDate . dbClient = esPusherDBClient
2023-12-20 16:13:04 +00:00
steps . s12AddOTPColumns = & AddOTPColumns { dbClient : queryDBClient }
steps . s13FixQuotaProjection = & FixQuotaConstraints { dbClient : queryDBClient }
2023-10-19 10:19:10 +00:00
steps . s14NewEventsTable = & NewEventsTable { dbClient : esPusherDBClient }
2023-12-20 16:13:04 +00:00
steps . s15CurrentStates = & CurrentProjectionState { dbClient : queryDBClient }
steps . s16UniqueConstraintsLower = & UniqueConstraintToLower { dbClient : queryDBClient }
steps . s17AddOffsetToUniqueConstraints = & AddOffsetToCurrentStates { dbClient : queryDBClient }
steps . s18AddLowerFieldsToLoginNames = & AddLowerFieldsToLoginNames { dbClient : queryDBClient }
2023-12-31 14:30:25 +00:00
steps . s19AddCurrentStatesIndex = & AddCurrentSequencesIndex { dbClient : queryDBClient }
2024-01-09 18:36:46 +00:00
steps . s20AddByUserSessionIndex = & AddByUserIndexToSession { dbClient : queryDBClient }
2024-01-17 10:16:48 +00:00
steps . s21AddBlockFieldToLimits = & AddBlockFieldToLimits { dbClient : queryDBClient }
2024-01-25 16:28:20 +00:00
steps . s22ActiveInstancesIndex = & ActiveInstanceEvents { dbClient : queryDBClient }
2024-03-08 13:33:53 +00:00
steps . s23CorrectGlobalUniqueConstraints = & CorrectGlobalUniqueConstraints { dbClient : esPusherDBClient }
2024-03-20 10:18:46 +00:00
steps . s24AddActorToAuthTokens = & AddActorToAuthTokens { dbClient : queryDBClient }
2024-03-28 06:21:21 +00:00
steps . s25User11AddLowerFieldsToVerifiedEmail = & User11AddLowerFieldsToVerifiedEmail { dbClient : esPusherDBClient }
2024-05-22 15:26:02 +00:00
steps . s26AuthUsers3 = & AuthUsers3 { dbClient : esPusherDBClient }
2024-05-23 05:04:07 +00:00
steps . s27IDPTemplate6SAMLNameIDFormat = & IDPTemplate6SAMLNameIDFormat { dbClient : esPusherDBClient }
2024-07-03 15:00:56 +00:00
steps . s28AddFieldTable = & AddFieldTable { dbClient : esPusherDBClient }
steps . s29FillFieldsForProjectGrant = & FillFieldsForProjectGrant { eventstore : eventstoreClient }
2024-07-05 07:36:00 +00:00
steps . s30FillFieldsForOrgDomainVerified = & FillFieldsForOrgDomainVerified { eventstore : eventstoreClient }
2024-07-08 15:54:19 +00:00
steps . s31AddAggregateIndexToFields = & AddAggregateIndexToFields { dbClient : esPusherDBClient }
2024-09-03 13:19:00 +00:00
steps . s32AddAuthSessionID = & AddAuthSessionID { dbClient : esPusherDBClient }
2024-09-26 07:14:33 +00:00
steps . s33SMSConfigs3TwilioAddVerifyServiceSid = & SMSConfigs3TwilioAddVerifyServiceSid { dbClient : esPusherDBClient }
2024-10-04 13:15:41 +00:00
steps . s34AddCacheSchema = & AddCacheSchema { dbClient : queryDBClient }
2023-10-19 10:19:10 +00:00
2023-12-20 16:13:04 +00:00
err = projection . Create ( ctx , projectionDBClient , eventstoreClient , config . Projections , nil , nil , nil )
2022-11-04 09:21:58 +00:00
logging . OnError ( err ) . Fatal ( "unable to start projections" )
2022-07-20 09:20:49 +00:00
repeatableSteps := [ ] migration . RepeatableMigration {
& externalConfigChange {
es : eventstoreClient ,
ExternalDomain : config . ExternalDomain ,
ExternalPort : config . ExternalPort ,
ExternalSecure : config . ExternalSecure ,
2023-07-14 06:49:57 +00:00
defaults : config . SystemDefaults ,
2022-07-20 09:20:49 +00:00
} ,
2022-11-04 09:21:58 +00:00
& projectionTables {
es : eventstoreClient ,
Version : build . Version ( ) ,
} ,
2022-07-20 09:20:49 +00:00
}
2024-03-08 13:33:53 +00:00
for _ , step := range [ ] migration . Migration {
steps . s14NewEventsTable ,
steps . s1ProjectionTable ,
steps . s2AssetsTable ,
2024-07-03 15:00:56 +00:00
steps . s28AddFieldTable ,
2024-07-08 15:54:19 +00:00
steps . s31AddAggregateIndexToFields ,
2024-03-08 13:33:53 +00:00
steps . FirstInstance ,
steps . s5LastFailed ,
steps . s6OwnerRemoveColumns ,
steps . s7LogstoreTables ,
steps . s8AuthTokens ,
steps . s12AddOTPColumns ,
steps . s13FixQuotaProjection ,
steps . s15CurrentStates ,
steps . s16UniqueConstraintsLower ,
steps . s17AddOffsetToUniqueConstraints ,
steps . s19AddCurrentStatesIndex ,
steps . s20AddByUserSessionIndex ,
steps . s22ActiveInstancesIndex ,
steps . s23CorrectGlobalUniqueConstraints ,
2024-03-20 10:18:46 +00:00
steps . s24AddActorToAuthTokens ,
2024-05-22 15:26:02 +00:00
steps . s26AuthUsers3 ,
2024-07-03 15:00:56 +00:00
steps . s29FillFieldsForProjectGrant ,
2024-07-05 07:36:00 +00:00
steps . s30FillFieldsForOrgDomainVerified ,
2024-10-04 13:15:41 +00:00
steps . s34AddCacheSchema ,
2024-03-08 13:33:53 +00:00
} {
mustExecuteMigration ( ctx , eventstoreClient , step , "migration failed" )
}
2022-04-25 15:05:20 +00:00
2022-07-20 09:20:49 +00:00
for _ , repeatableStep := range repeatableSteps {
2024-03-08 13:33:53 +00:00
mustExecuteMigration ( ctx , eventstoreClient , repeatableStep , "unable to migrate repeatable step" )
2022-04-25 15:05:20 +00:00
}
2023-12-08 12:14:22 +00:00
2024-01-17 10:16:48 +00:00
// These steps are executed after the repeatable steps because they add fields projections
2024-03-08 13:33:53 +00:00
for _ , step := range [ ] migration . Migration {
steps . s18AddLowerFieldsToLoginNames ,
steps . s21AddBlockFieldToLimits ,
2024-03-28 06:21:21 +00:00
steps . s25User11AddLowerFieldsToVerifiedEmail ,
2024-05-23 05:04:07 +00:00
steps . s27IDPTemplate6SAMLNameIDFormat ,
2024-09-03 13:19:00 +00:00
steps . s32AddAuthSessionID ,
2024-09-26 07:14:33 +00:00
steps . s33SMSConfigs3TwilioAddVerifyServiceSid ,
2024-03-08 13:33:53 +00:00
} {
mustExecuteMigration ( ctx , eventstoreClient , step , "migration failed" )
}
2024-01-25 16:28:20 +00:00
// projection initialization must be done last, since the steps above might add required columns to the projections
2024-05-30 09:35:30 +00:00
if ! config . ForMirror && config . InitProjections . Enabled {
2024-01-25 16:28:20 +00:00
initProjections (
ctx ,
eventstoreClient ,
2024-05-30 09:35:30 +00:00
eventstoreV4 ,
2024-01-25 16:28:20 +00:00
queryDBClient ,
projectionDBClient ,
masterKey ,
config ,
)
}
2022-04-25 15:05:20 +00:00
}
2023-03-01 00:11:23 +00:00
2024-03-08 13:33:53 +00:00
func mustExecuteMigration ( ctx context . Context , eventstoreClient * eventstore . Eventstore , step migration . Migration , errorMsg string ) {
err := migration . Migrate ( ctx , eventstoreClient , step )
logging . WithFields ( "name" , step . String ( ) ) . OnError ( err ) . Fatal ( errorMsg )
}
2023-03-01 00:11:23 +00:00
func readStmt ( fs embed . FS , folder , typ , filename string ) ( string , error ) {
stmt , err := fs . ReadFile ( folder + "/" + typ + "/" + filename )
return string ( stmt ) , err
}
2024-01-25 16:28:20 +00:00
func initProjections (
ctx context . Context ,
eventstoreClient * eventstore . Eventstore ,
2024-05-30 09:35:30 +00:00
eventstoreV4 * es_v4 . EventStore ,
2024-01-25 16:28:20 +00:00
queryDBClient ,
projectionDBClient * database . DB ,
masterKey string ,
config * Config ,
) {
logging . Info ( "init-projections is currently in beta" )
keyStorage , err := cryptoDB . NewKeyStorage ( queryDBClient , masterKey )
logging . OnError ( err ) . Fatal ( "unable to start key storage" )
keys , err := encryption . EnsureEncryptionKeys ( ctx , config . EncryptionKeys , keyStorage )
logging . OnError ( err ) . Fatal ( "unable to ensure encryption keys" )
err = projection . Create (
ctx ,
queryDBClient ,
eventstoreClient ,
projection . Config {
RetryFailedAfter : config . InitProjections . RetryFailedAfter ,
MaxFailureCount : config . InitProjections . MaxFailureCount ,
BulkLimit : config . InitProjections . BulkLimit ,
} ,
keys . OIDC ,
keys . SAML ,
config . SystemAPIUsers ,
)
logging . OnError ( err ) . Fatal ( "unable to start projections" )
for _ , p := range projection . Projections ( ) {
err := migration . Migrate ( ctx , eventstoreClient , p )
logging . WithFields ( "name" , p . String ( ) ) . OnError ( err ) . Fatal ( "migration failed" )
}
staticStorage , err := config . AssetStorage . NewStorage ( queryDBClient . DB )
logging . OnError ( err ) . Fatal ( "unable to start asset storage" )
adminView , err := admin_view . StartView ( queryDBClient )
logging . OnError ( err ) . Fatal ( "unable to start admin view" )
admin_handler . Register ( ctx ,
admin_handler . Config {
Client : queryDBClient ,
Eventstore : eventstoreClient ,
BulkLimit : config . InitProjections . BulkLimit ,
FailureCountUntilSkip : uint64 ( config . InitProjections . MaxFailureCount ) ,
} ,
adminView ,
staticStorage ,
)
for _ , p := range admin_handler . Projections ( ) {
err := migration . Migrate ( ctx , eventstoreClient , p )
logging . WithFields ( "name" , p . String ( ) ) . OnError ( err ) . Fatal ( "migration failed" )
}
sessionTokenVerifier := internal_authz . SessionTokenVerifier ( keys . OIDC )
queries , err := query . StartQueries (
ctx ,
eventstoreClient ,
2024-05-30 09:35:30 +00:00
eventstoreV4 . Querier ,
2024-01-25 16:28:20 +00:00
queryDBClient ,
projectionDBClient ,
2024-09-25 19:40:21 +00:00
config . Caches ,
2024-01-25 16:28:20 +00:00
config . Projections ,
config . SystemDefaults ,
keys . IDPConfig ,
keys . OTP ,
keys . OIDC ,
keys . SAML ,
config . InternalAuthZ . RolePermissionMappings ,
sessionTokenVerifier ,
func ( q * query . Queries ) domain . PermissionCheck {
return func ( ctx context . Context , permission , orgID , resourceID string ) ( err error ) {
return internal_authz . CheckPermission ( ctx , & authz_es . UserMembershipRepo { Queries : q } , config . InternalAuthZ . RolePermissionMappings , permission , orgID , resourceID )
}
} ,
0 , // not needed for projections
nil , // not needed for projections
false ,
)
logging . OnError ( err ) . Fatal ( "unable to start queries" )
authView , err := auth_view . StartView ( queryDBClient , keys . OIDC , queries , eventstoreClient )
logging . OnError ( err ) . Fatal ( "unable to start admin view" )
auth_handler . Register ( ctx ,
auth_handler . Config {
Client : queryDBClient ,
Eventstore : eventstoreClient ,
BulkLimit : config . InitProjections . BulkLimit ,
FailureCountUntilSkip : uint64 ( config . InitProjections . MaxFailureCount ) ,
} ,
authView ,
queries ,
)
for _ , p := range auth_handler . Projections ( ) {
err := migration . Migrate ( ctx , eventstoreClient , p )
logging . WithFields ( "name" , p . String ( ) ) . OnError ( err ) . Fatal ( "migration failed" )
}
authZRepo , err := authz . Start ( queries , eventstoreClient , queryDBClient , keys . OIDC , config . ExternalSecure )
logging . OnError ( err ) . Fatal ( "unable to start authz repo" )
permissionCheck := func ( ctx context . Context , permission , orgID , resourceID string ) ( err error ) {
return internal_authz . CheckPermission ( ctx , authZRepo , config . InternalAuthZ . RolePermissionMappings , permission , orgID , resourceID )
}
commands , err := command . StartCommands (
eventstoreClient ,
config . SystemDefaults ,
config . InternalAuthZ . RolePermissionMappings ,
staticStorage ,
& webauthn . Config {
DisplayName : config . WebAuthNName ,
ExternalSecure : config . ExternalSecure ,
} ,
config . ExternalDomain ,
config . ExternalSecure ,
config . ExternalPort ,
keys . IDPConfig ,
keys . OTP ,
keys . SMTP ,
keys . SMS ,
keys . User ,
keys . DomainVerification ,
keys . OIDC ,
keys . SAML ,
& http . Client { } ,
permissionCheck ,
sessionTokenVerifier ,
config . OIDC . DefaultAccessTokenLifetime ,
config . OIDC . DefaultRefreshTokenExpiration ,
config . OIDC . DefaultRefreshTokenIdleExpiration ,
config . DefaultInstance . SecretGenerators ,
)
logging . OnError ( err ) . Fatal ( "unable to start commands" )
notify_handler . Register (
ctx ,
config . Projections . Customizations [ "notifications" ] ,
config . Projections . Customizations [ "notificationsquotas" ] ,
config . Projections . Customizations [ "telemetry" ] ,
* config . Telemetry ,
config . ExternalDomain ,
config . ExternalPort ,
config . ExternalSecure ,
commands ,
queries ,
eventstoreClient ,
config . Login . DefaultOTPEmailURLV2 ,
config . SystemDefaults . Notifications . FileSystemPath ,
keys . User ,
keys . SMTP ,
keys . SMS ,
)
for _ , p := range notify_handler . Projections ( ) {
err := migration . Migrate ( ctx , eventstoreClient , p )
logging . WithFields ( "name" , p . String ( ) ) . OnError ( err ) . Fatal ( "migration failed" )
}
}