diff --git a/backend/cmd/config/config.go b/backend/cmd/config/config.go index cbd0478cbb..a3b079bb92 100644 --- a/backend/cmd/config/config.go +++ b/backend/cmd/config/config.go @@ -1,45 +1,74 @@ -/* -Copyright © 2025 NAME HERE -*/ package config import ( "fmt" + "os" + "reflect" "github.com/spf13/cobra" "github.com/spf13/viper" ) -var ( - // ConfigureCmd represents the config command - ConfigureCmd = &cobra.Command{ - Use: "configure", - Short: "Guides you through configuring Zitadel", - // Long: `A longer description that spans multiple lines and likely contains examples - // and usage of using your command. For example: +type Config struct { + Version Version +} - // Cobra is a CLI library for Go that empowers applications. - // This application is a tool to generate the needed files - // to quickly create a Cobra application.`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("config called") - fmt.Println(viper.AllSettings()) - fmt.Println(viper.Sub("database").AllSettings()) - viper.en - }, +func (c Config) Hooks() []viper.DecoderConfigOption { + return []viper.DecoderConfigOption{ + viper.DecodeHook(decodeVersion), + } +} + +func (c Config) CurrentVersion() Version { + return c.Version +} + +var Path string + +// InitConfig reads in config file and ENV variables if set. +func InitConfig() { + if Path != "" { + // Use config file from the flag. + viper.SetConfigFile(Path) + } else { + // Find home directory. + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + // Search config in home directory with name ".zitadel" (without extension). + viper.AddConfigPath(home) + viper.SetConfigType("yaml") + viper.SetConfigName(".zitadel") } - upgrade bool + viper.AutomaticEnv() // read in environment variables that match + viper.SetEnvPrefix("ZITADEL") + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + } +} + +type Version uint8 + +const ( + VersionUnknown Version = iota + V3 ) -func init() { - // Here you will define your flags and configuration settings. - ConfigureCmd.Flags().BoolVarP(&upgrade, "upgrade", "u", false, "Only changed configuration values since the previously used version will be asked for") +func decodeVersion(from, to reflect.Value) (_ interface{}, err error) { + if to.Type() != reflect.TypeOf(Version(0)) { + return from.Interface(), nil + } - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // configureCmd.PersistentFlags().String("foo", "", "A help for foo") + switch from.Interface().(string) { + case "": + return VersionUnknown, nil + case "v3": + return V3, nil - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: + } + + return nil, fmt.Errorf("unsupported version: %v", from.Interface()) } diff --git a/backend/cmd/configure/configure.go b/backend/cmd/configure/configure.go index 3c9a76af51..2d9554dd5b 100644 --- a/backend/cmd/configure/configure.go +++ b/backend/cmd/configure/configure.go @@ -1,41 +1,48 @@ package configure import ( - "fmt" - "github.com/spf13/cobra" "github.com/spf13/viper" - - "github.com/zitadel/zitadel/backend/storage/database/dialect" + "github.com/zitadel/zitadel/backend/cmd/config" ) var ( // ConfigureCmd represents the config command ConfigureCmd = &cobra.Command{ Use: "configure", - Short: "Guides you through configuring Zitadel", + Short: "Guides you through configuring Zitadel for the specified command", // Long: `A longer description that spans multiple lines and likely contains examples // and usage of using your command. For example: // Cobra is a CLI library for Go that empowers applications. // This application is a tool to generate the needed files // to quickly create a Cobra application.`, + // Run: func(cmd *cobra.Command, args []string) { + // fmt.Println("config called") + // // fmt.Println(viper.AllSettings()) + // // fmt.Println(viper.Sub("database").AllSettings()) + // // pool, err := config.Database.Connect(cmd.Context()) + // // _, _ = pool, err + // }, + PersistentPreRun: configurePreRun, Run: func(cmd *cobra.Command, args []string) { - fmt.Println("config called") - fmt.Println(viper.AllSettings()) - fmt.Println(viper.Sub("database").AllSettings()) - pool, err := config.Database.Connect(cmd.Context()) - _, _ = pool, err + t := new(test) + // Update2(*t) + Update("test", "test", t.Fields())(cmd, args) }, - PreRun: ReadConfigPreRun[Config](viper.GetViper(), &config), } - config Config + configuration Config ) +func configurePreRun(cmd *cobra.Command, args []string) { + // cmd.InheritedFlags().Lookup("config").Hidden = true + ReadConfigPreRun(viper.GetViper(), &configuration)(cmd, args) +} + func init() { // Here you will define your flags and configuration settings. - ConfigureCmd.Flags().BoolVarP(&config.upgrade, "upgrade", "u", false, "Only changed configuration values since the previously used version will be asked for") + ConfigureCmd.PersistentFlags().BoolVarP(&configuration.upgrade, "upgrade", "u", false, "Only changed configuration values since the previously used version will be asked for") // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: @@ -46,11 +53,63 @@ func init() { } type Config struct { - Database dialect.Config - - upgrade bool + upgrade bool `mapstructure:"-"` } -func (c Config) Hooks() []viper.DecoderConfigOption { - return c.Database.Hooks() +func (c *Config) Hooks() []viper.DecoderConfigOption { + return nil +} + +type sub struct { + F1 string + F2 int + F3 *bool +} + +func (s sub) Fields() []Updater { + return []Updater{ + Field[string]{ + FieldName: "f1", + Value: &s.F1, + Default: "", + Description: "field 1", + Version: config.V3, + }, + Field[int]{ + FieldName: "f2", + Value: &s.F2, + Default: 0, + Description: "field 2", + Version: config.V3, + }, + Field[*bool]{ + FieldName: "f3", + Value: &s.F3, + Default: nil, + Description: "field 3", + Version: config.V3, + }, + } +} + +type test struct { + F1 string + Sub sub +} + +func (t test) Fields() []Updater { + return []Updater{ + Field[string]{ + FieldName: "f1", + Value: &t.F1, + Default: "", + Description: "field 1", + Version: config.V3, + }, + Struct{ + FieldName: "sub", + Description: "sub field", + SubFields: t.Sub.Fields(), + }, + } } diff --git a/backend/cmd/configure/read.go b/backend/cmd/configure/read.go index 15f1f9baf0..8656b5a4cc 100644 --- a/backend/cmd/configure/read.go +++ b/backend/cmd/configure/read.go @@ -9,18 +9,17 @@ type Unmarshaller interface { Hooks() []viper.DecoderConfigOption } -func ReadConfigPreRun[C Unmarshaller](v *viper.Viper, config *C) func(cmd *cobra.Command, args []string) { +func ReadConfigPreRun[C Unmarshaller](v *viper.Viper, config C) func(cmd *cobra.Command, args []string) { return func(cmd *cobra.Command, args []string) { - if err := v.Unmarshal(config, (*config).Hooks()...); err != nil { + if err := v.Unmarshal(config, config.Hooks()...); err != nil { panic(err) } } } -func ReadConfig[C Unmarshaller](v *viper.Viper) (*C, error) { - var config C +func ReadConfig[C Unmarshaller](v *viper.Viper) (config C, err error) { if err := v.Unmarshal(&config, config.Hooks()...); err != nil { - return nil, err + return config, err } - return &config, nil + return config, nil } diff --git a/backend/cmd/configure/update_config.go b/backend/cmd/configure/update_config.go new file mode 100644 index 0000000000..206ee77692 --- /dev/null +++ b/backend/cmd/configure/update_config.go @@ -0,0 +1,243 @@ +package configure + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/zitadel/zitadel/backend/cmd/config" +) + +func Update(name, description string, fields []Updater) func(cmd *cobra.Command, args []string) { + return func(cmd *cobra.Command, args []string) { + u := Struct{ + FieldName: name, + Description: description, + SubFields: fields, + } + fields := viper.AllSettings() + updateStruct(u, fields, 1) + + fmt.Println("Using config file:", viper.ConfigFileUsed(), "fields:", fields) + // viper.MergeConfigMap(fields) + // if err := viper.WriteConfig(); err != nil { + // panic(err) + // } + } +} + +func updateStruct(updater StructUpdater, fields map[string]any, depth int) { + for _, field := range updater.Fields() { + // fmt.Printf("field: %s.%s\n %s\n", parent, field.Name(), field.Describe()) + setField(field, fields, depth) + } +} + +func setField(field Updater, fields map[string]any, depth int) { + if !field.ShouldUpdate(config.V3) { + prompt := promptui.Prompt{ + Label: field.Name() + " did not change since last configure, skip?", + IsConfirm: true, + } + if _, err := prompt.Run(); err == nil { + fmt.Println("skip") + return + } + return + } + fmt.Println(field.Name(), field.Describe()) + switch f := field.(type) { + case StructUpdater: + if fields[f.Name()] == nil { + fields[f.Name()] = map[string]any{} + } + fmt.Printf("%.*s %s: %s\n", depth*2, "-", f.Name(), f.Describe()) + updateStruct(f, fields[f.Name()].(map[string]any), depth+1) + case FieldUpdater: + prompt := promptui.Prompt{ + Label: fmt.Sprintf("%s (%s) (%T)", f.Name(), f.Describe(), f.DefaultValue()), + Default: fmt.Sprintf("%v", f.DefaultValue()), + Validate: func(s string) error { + if isConfirm(reflect.TypeOf(f.DefaultValue())) { + return nil + } + return f.Set(s) + }, + IsConfirm: isConfirm(reflect.TypeOf(f.DefaultValue())), + } + val, err := prompt.Run() + if err != nil { + panic(err) + } + fields[f.Name()] = f.Set(val) + case OneOfUpdater: + var possibilities []string + for _, subField := range f.Fields() { + possibilities = append(possibilities, subField.Name()) + fields[subField.Name()] = subField + } + + prompt := promptui.Select{ + Label: fmt.Sprintf("Select one of %s: (%s)", f.Name(), f.Describe()), + Items: possibilities, + } + i, _, err := prompt.Run() + if err != nil { + panic(err) + } + setField(f.Fields()[i], fields, depth+1) + } +} + +func isConfirm(t reflect.Type) bool { + if t.Kind() == reflect.Ptr { + return isConfirm(t.Elem()) + } + return t.Kind() == reflect.Bool +} + +type Updater interface { + Name() string + Describe() string + ShouldUpdate(version config.Version) bool +} + +type FieldUpdater interface { + Updater + DefaultValue() any + Set(value string) error + _field() +} + +type StructUpdater interface { + Updater + Fields() []Updater + _struct() +} + +type OneOfUpdater interface { + Updater + Fields() []Updater + _oneOf() +} + +var _ FieldUpdater = (*Field[string])(nil) + +type Field[T any] struct { + FieldName string + Default T + Value *T + Description string + Version config.Version +} + +// DefaultValue implements [FieldUpdater]. +func (uf Field[T]) DefaultValue() any { + return uf.Default +} + +// Describe implements [FieldUpdater]. +func (uf Field[T]) Describe() string { + return uf.Description +} + +// Set implements [FieldUpdater]. +func (uf Field[T]) Set(value string) error { + var v T + if err := json.Unmarshal([]byte(value), &v); err != nil { + return fmt.Errorf("failed to unmarshal value: %v", err) + } + *uf.Value = v + return nil +} + +// Field implements [FieldUpdater]. +func (uf Field[T]) Name() string { + return uf.FieldName +} + +// ShouldUpdate implements [FieldUpdater]. +func (uf Field[T]) ShouldUpdate(version config.Version) bool { + return uf.Version <= version +} + +func (f Field[T]) _field() {} + +var _ StructUpdater = (*Struct)(nil) + +type Struct struct { + FieldName string + Description string + SubFields []Updater +} + +// Describe implements [StructUpdater]. +func (us Struct) Describe() string { + return us.Description +} + +func (us Struct) Name() string { + return us.FieldName +} + +func (us Struct) Fields() []Updater { + return us.SubFields +} + +func (f Struct) _struct() {} + +type OneOf struct { + FieldName string + Description string + SubFields []Updater +} + +// Describe implements [OneOfUpdater]. +func (o OneOf) Describe() string { + return o.Description +} + +// Fields implements [OneOfUpdater]. +func (o OneOf) Fields() []Updater { + return o.SubFields +} + +// Name implements [FieldUpdater]. +func (o OneOf) Name() string { + return o.FieldName +} + +func (f OneOf) _oneOf() {} + +// ShouldUpdate implements [OneOfUpdater]. +func (o OneOf) ShouldUpdate(version config.Version) bool { + for _, field := range o.SubFields { + if !field.ShouldUpdate(version) { + continue + } + return true + } + return false +} + +var _ OneOfUpdater = (*OneOf)(nil) + +func (us Struct) ShouldUpdate(version config.Version) bool { + for _, field := range us.SubFields { + if !field.ShouldUpdate(version) { + continue + } + return true + } + return false +} + +func FieldName(parent, field string) string { + if parent == "" { + return field + } + return parent + "." + field +} diff --git a/backend/cmd/configure/update_config2.go b/backend/cmd/configure/update_config2.go new file mode 100644 index 0000000000..a953f85a7f --- /dev/null +++ b/backend/cmd/configure/update_config2.go @@ -0,0 +1,109 @@ +package configure + +import ( + "fmt" + "os" + "reflect" + "strings" + + "github.com/manifoldco/promptui" +) + +func Update2(config any) { + // Print the intro + printIntro() + + // Start the interactive CLI + interactiveCLI(reflect.ValueOf(config), 0) + fmt.Println(config) +} + +const ( + ExitValue = "" + BackValue = "⬅ Back" + Prefix = "📁 " +) + +var introTemplate = ` + +----------------------------------------+ + | 🛠 Config Interactive CLI 🛠 | + +----------------------------------------+ + | | + | %5s : Dive into nested config | + | %6s : Return to previous menu | + | %6s : Exit application | + | | + | Choose an option to explore! | + | | + +----------------------------------------+ + ` + +func printIntro() { + fmt.Printf(introTemplate, Prefix, BackValue, ExitValue) +} + +// interactiveCLI handles the interactive CLI +func interactiveCLI(v reflect.Value, depth int) { + for { + var items []string + + // If depth is greater than 0, we are in a nested struct and should add a "Back" option + if depth > 0 { + items = append(items, BackValue) + } + + // Add all the field names + items = append(items, getFieldNames(v)...) + + // Add an "Exit" option + items = append(items, ExitValue) + + prompt := promptui.Select{ + Label: "Select Field", + Items: items, + } + + _, result, err := prompt.Run() + + if err != nil { + fmt.Printf("Prompt failed %v\n", err) + return + } + + switch result { + case BackValue: + return + case ExitValue: + // Exit the entire application + os.Exit(0) + default: + fieldName := strings.TrimPrefix(result, Prefix) + selectedField := v.FieldByName(fieldName) + if selectedField.Kind() == reflect.Struct { + interactiveCLI(selectedField, depth+1) + } else { + prompt := promptui.Prompt{ + Label: fmt.Sprintf("Field %s (%s)", result, selectedField.Kind()), + Default: fmt.Sprintf("%v", selectedField.Interface()), + } + res, err := prompt.Run() + fmt.Println(res, err) + // fmt.Printf("%s: %v\n", result, selectedField.Interface()) + } + } + } +} + +// getFieldNames returns all the field names +func getFieldNames(v reflect.Value) []string { + t := v.Type() + var fieldNames []string + for i := 0; i < v.NumField(); i++ { + fieldName := t.Field(i).Name + if v.Field(i).Kind() == reflect.Struct { + fieldName = Prefix + fieldName + } + fieldNames = append(fieldNames, fieldName) + } + return fieldNames +} diff --git a/backend/cmd/prepare/001/sql/001_user.sql b/backend/cmd/prepare/001/sql/001_user.sql new file mode 100644 index 0000000000..95b6c5b98a --- /dev/null +++ b/backend/cmd/prepare/001/sql/001_user.sql @@ -0,0 +1 @@ +CREATE USER IF NOT EXISTS {{ .Username}}; \ No newline at end of file diff --git a/backend/cmd/prepare/001/sql/002_database.sql b/backend/cmd/prepare/001/sql/002_database.sql new file mode 100644 index 0000000000..f80389acad --- /dev/null +++ b/backend/cmd/prepare/001/sql/002_database.sql @@ -0,0 +1 @@ +CREATE DATABASE IF NOT EXISTS {{ .DatabaseName }} WITH OWNER {{ .Username }}; \ No newline at end of file diff --git a/backend/cmd/prepare/001/step_001.go b/backend/cmd/prepare/001/step_001.go new file mode 100644 index 0000000000..32118d824d --- /dev/null +++ b/backend/cmd/prepare/001/step_001.go @@ -0,0 +1,68 @@ +package step001 + +import ( + "context" + "embed" + "fmt" + + "github.com/zitadel/zitadel/backend/cmd/config" + "github.com/zitadel/zitadel/backend/cmd/configure" + "github.com/zitadel/zitadel/backend/storage/database" +) + +var ( + //go:embed sql/*.sql + migrations embed.FS +) + +type Step001 struct { + Database database.Pool `mapstructure:"-"` + + DatabaseName string `configure:"added:"v3",default:"zitadel"` + Username string `configure:"added:"v3",default:"zitadel"` +} + +// Fields implements configure.StructUpdater. +func (v Step001) Fields() []configure.Updater { + return []configure.Updater{ + configure.Field[string]{ + FieldName: "databaseName", + Default: "zitadel", + Value: &v.DatabaseName, + Description: "The name of the database Zitadel will store its data in", + Version: config.V3, + }, + configure.Field[string]{ + FieldName: "username", + Default: "zitadel", + Value: &v.Username, + Description: "The username Zitadel will use to connect to the database", + Version: config.V3, + }, + } +} + +// Name implements configure.StructUpdater. +func (v *Step001) Name() string { + return "step001" +} + +// var _ configure.StructUpdater = (*Step001)(nil) + +func (v *Step001) Migrate(ctx context.Context) error { + files, err := migrations.ReadDir("sql") + if err != nil { + return err + } + for _, file := range files { + fmt.Println(file.Name()) + fmt.Println(migrations.ReadFile(file.Name())) + } + conn, err := v.Database.Acquire(ctx) + if err != nil { + return err + } + defer conn.Release(ctx) + + return nil +} diff --git a/backend/cmd/prepare/config.go b/backend/cmd/prepare/config.go new file mode 100644 index 0000000000..e8739f5e03 --- /dev/null +++ b/backend/cmd/prepare/config.go @@ -0,0 +1,114 @@ +package prepare + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/zitadel/zitadel/backend/cmd/config" + "github.com/zitadel/zitadel/backend/cmd/configure" + step001 "github.com/zitadel/zitadel/backend/cmd/prepare/001" + "github.com/zitadel/zitadel/backend/storage/database" + "github.com/zitadel/zitadel/backend/storage/database/dialect" +) + +var ( + configuration Config + + // configurePrepare represents the prepare command + configurePrepare = &cobra.Command{ + Use: "prepare", + Short: "Writes the configuration for the prepare command", + // Long: `A longer description that spans multiple lines and likely contains examples + // and usage of using your command. For example: + + // Cobra is a CLI library for Go that empowers applications. + // This application is a tool to generate the needed files + // to quickly create a Cobra application.`, + // Run: func(cmd *cobra.Command, args []string) { + // var err error + // config.Client, err = config.Database.Connect(cmd.Context()) + // if err != nil { + // panic(err) + // } + // defer config.Client.Close(cmd.Context()) + // if err := (&step001.Step001{Database: config.Client}).Migrate(cmd.Context()); err != nil { + // panic(err) + // } + // }, + Run: configure.Update( + "prepare", + "Writes the configuration for the prepare command", + configuration.Fields(), + ), + PreRun: configure.ReadConfigPreRun(viper.GetViper(), &configuration), + } +) + +type Config struct { + config.Config `mapstructure:",squash"` + + Database dialect.Config + Step001 step001.Step001 + + // runtime config + Client database.Pool `mapstructure:"-"` +} + +// Describe implements configure.StructUpdater. +func (c *Config) Describe() string { + return "Configuration for the prepare command" +} + +// Name implements configure.StructUpdater. +func (c *Config) Name() string { + return "prepare" +} + +// ShouldUpdate implements configure.StructUpdater. +func (c *Config) ShouldUpdate(version config.Version) bool { + for _, field := range c.Fields() { + if field.ShouldUpdate(version) { + return true + } + } + return false +} + +// Fields implements configure.UpdateConfig. +func (c Config) Fields() []configure.Updater { + return []configure.Updater{ + configure.Struct{ + FieldName: "step001", + Description: "The configuration for the first step of the prepare command", + SubFields: c.Step001.Fields(), + }, + configure.Struct{ + FieldName: "database", + Description: "The configuration for the database connection", + SubFields: c.Database.Fields(), + }, + } +} + +func (c *Config) Hooks() (decoders []viper.DecoderConfigOption) { + for _, hooks := range []configure.Unmarshaller{ + c.Config, + c.Database, + } { + decoders = append(decoders, hooks.Hooks()...) + } + return decoders +} + +func init() { + configure.ConfigureCmd.AddCommand(configurePrepare) + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // prepareCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // prepareCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/backend/cmd/prepare/prepare.go b/backend/cmd/prepare/prepare.go index b4f8ba4bfa..0f84cc3bcf 100644 --- a/backend/cmd/prepare/prepare.go +++ b/backend/cmd/prepare/prepare.go @@ -1,37 +1,34 @@ -/* -Copyright © 2025 NAME HERE -*/ package prepare import ( - "fmt" - "github.com/spf13/cobra" + step001 "github.com/zitadel/zitadel/backend/cmd/prepare/001" ) -// PrepareCmd represents the prepare command -var PrepareCmd = &cobra.Command{ - Use: "prepare", - Short: "Prepares the environment before starting Zitadel", - // Long: `A longer description that spans multiple lines and likely contains examples - // and usage of using your command. For example: +var ( + // PrepareCmd represents the prepare command + PrepareCmd = &cobra.Command{ + Use: "prepare", + Short: "Prepares external services before starting Zitadel", + // Long: `A longer description that spans multiple lines and likely contains examples + // and usage of using your command. For example: - // Cobra is a CLI library for Go that empowers applications. - // This application is a tool to generate the needed files - // to quickly create a Cobra application.`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("prepare called") - }, -} - -func init() { - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // prepareCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // prepareCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + // Cobra is a CLI library for Go that empowers applications. + // This application is a tool to generate the needed files + // to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + var err error + configuration.Client, err = configuration.Database.Connect(cmd.Context()) + if err != nil { + panic(err) + } + defer configuration.Client.Close(cmd.Context()) + if err := (&step001.Step001{Database: configuration.Client}).Migrate(cmd.Context()); err != nil { + panic(err) + } + }, + } +) + +type Migration interface { } diff --git a/backend/cmd/prepare/step_001.go b/backend/cmd/prepare/step_001.go deleted file mode 100644 index 63b820c517..0000000000 --- a/backend/cmd/prepare/step_001.go +++ /dev/null @@ -1,23 +0,0 @@ -package prepare - -import ( - "context" - - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/backend/storage/eventstore" -) - -type Step001 struct { - Database database.Pool -} - -func (v *Step001) Migrate(ctx context.Context) error { - conn, err := v.Database.Acquire(ctx) - if err != nil { - return err - } - defer conn.Release(ctx) - - eventstore.New(conn). - return nil -} diff --git a/backend/cmd/root.go b/backend/cmd/root.go index 2de31735d8..06233f2676 100644 --- a/backend/cmd/root.go +++ b/backend/cmd/root.go @@ -1,29 +1,21 @@ package cmd import ( - "fmt" "os" "github.com/spf13/cobra" - "github.com/spf13/viper" "github.com/zitadel/zitadel/backend/cmd/config" + "github.com/zitadel/zitadel/backend/cmd/configure" "github.com/zitadel/zitadel/backend/cmd/prepare" "github.com/zitadel/zitadel/backend/cmd/start" "github.com/zitadel/zitadel/backend/cmd/upgrade" ) -var cfgFile string - // RootCmd represents the base command when called without any subcommands var RootCmd = &cobra.Command{ Use: "zitadel [subcommand]", Short: "A brief description of your application", - Long: `A longer description that spans multiple lines and likely contains -examples and usage of using your application. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, + Long: `zitadel`, // Uncomment the following line if your bare application // has an action associated with it: // Run: func(cmd *cobra.Command, args []string) { }, @@ -39,46 +31,22 @@ func Execute() { } func init() { - RootCmd.AddCommand(config.ConfigureCmd) - RootCmd.AddCommand(prepare.PrepareCmd) - RootCmd.AddCommand(start.StartCmd) - RootCmd.AddCommand(upgrade.UpgradeCmd) + RootCmd.AddCommand( + configure.ConfigureCmd, + prepare.PrepareCmd, + start.StartCmd, + upgrade.UpgradeCmd, + ) - cobra.OnInitialize(initConfig) + cobra.OnInitialize(config.InitConfig) // Here you will define your flags and configuration settings. // Cobra supports persistent flags, which, if defined here, // will be global for your application. - RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.zitadel.yaml)") + RootCmd.PersistentFlags().StringVar(&config.Path, "config", "", "config file (default is $HOME/.zitadel.yaml)") // Cobra also supports local flags, which will only run // when this action is called directly. RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } - -// initConfig reads in config file and ENV variables if set. -func initConfig() { - if cfgFile != "" { - // Use config file from the flag. - viper.SetConfigFile(cfgFile) - } else { - // Find home directory. - home, err := os.UserHomeDir() - cobra.CheckErr(err) - - // Search config in home directory with name ".zitadel" (without extension). - viper.AddConfigPath(home) - viper.SetConfigType("yaml") - viper.SetConfigName(".zitadel") - } - - viper.AutomaticEnv() // read in environment variables that match - viper.AllowEmptyEnv(true) - viper.SetEnvPrefix("ZITADEL") - - // If a config file is found, read it in. - if err := viper.ReadInConfig(); err == nil { - fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) - } -} diff --git a/backend/cmd/start/config.go b/backend/cmd/start/config.go index a524df8ba3..52e9ee1e60 100644 --- a/backend/cmd/start/config.go +++ b/backend/cmd/start/config.go @@ -7,7 +7,7 @@ import ( ) type Config struct { - Database dialect.Config + Database dialect.Config `version:"v3"` } func (c Config) Hooks() []viper.DecoderConfigOption { diff --git a/backend/cmd/start/start.go b/backend/cmd/start/start.go index 40f5301a84..fb045cd9a3 100644 --- a/backend/cmd/start/start.go +++ b/backend/cmd/start/start.go @@ -1,28 +1,32 @@ -/* -Copyright © 2025 NAME HERE -*/ package start import ( "fmt" "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/zitadel/zitadel/backend/cmd/configure" ) -// StartCmd represents the start command -var StartCmd = &cobra.Command{ - Use: "start", - Short: "Starts the Zitadel server", - // Long: `A longer description that spans multiple lines and likely contains examples - // and usage of using your command. For example: +var ( + // StartCmd represents the start command + StartCmd = &cobra.Command{ + Use: "start", + Short: "Starts the Zitadel server", + // Long: `A longer description that spans multiple lines and likely contains examples + // and usage of using your command. For example: - // Cobra is a CLI library for Go that empowers applications. - // This application is a tool to generate the needed files - // to quickly create a Cobra application.`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("start called") - }, -} + // Cobra is a CLI library for Go that empowers applications. + // This application is a tool to generate the needed files + // to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("start called") + }, + PreRun: configure.ReadConfigPreRun(viper.GetViper(), &config), + } + + config Config +) func init() { // Here you will define your flags and configuration settings. diff --git a/backend/cmd/test.yaml b/backend/cmd/test.yaml index 966a8f74d2..5d138e5c13 100644 --- a/backend/cmd/test.yaml +++ b/backend/cmd/test.yaml @@ -1,5 +1,19 @@ database: - postgres: 'something' - cockroach: - host: localhost - port: 26257 \ No newline at end of file + cockroach: + host: localhost + port: 26257 + gosql: + fieldname: gosql + default: null + value: null + description: Configuration for connection string for gosql + version: 1 + postgres: + fieldname: postgres + default: null + value: null + description: Configuration for connection string for postgres + version: 1 +step001: + databasename: zitadel + username: zitadel diff --git a/backend/cmd/upgrade/upgrade.go b/backend/cmd/upgrade/upgrade.go index b9d17b64a4..9d92aca3a5 100644 --- a/backend/cmd/upgrade/upgrade.go +++ b/backend/cmd/upgrade/upgrade.go @@ -1,6 +1,3 @@ -/* -Copyright © 2025 NAME HERE -*/ package upgrade import ( @@ -25,7 +22,6 @@ var UpgradeCmd = &cobra.Command{ } func init() { - // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command diff --git a/backend/storage/database/config.go b/backend/storage/database/config.go index 02ab4c8fff..56c86b2b2d 100644 --- a/backend/storage/database/config.go +++ b/backend/storage/database/config.go @@ -1,7 +1,7 @@ package database -var Config = make(map[string]any) +import "context" -func AddDatabaseConfig(name string, configure func(map[string]any) error) { - Config[name] = configure +type Connector interface { + Connect(ctx context.Context) (Pool, error) } diff --git a/backend/storage/database/database.go b/backend/storage/database/database.go index b8a146fbe5..9d9cc6f676 100644 --- a/backend/storage/database/database.go +++ b/backend/storage/database/database.go @@ -12,7 +12,7 @@ type Row interface { type Rows interface { Row Next() bool - Close() + Close() error Err() error } diff --git a/backend/storage/database/dialect/config.go b/backend/storage/database/dialect/config.go index ca5be5f806..9bcf7daca3 100644 --- a/backend/storage/database/dialect/config.go +++ b/backend/storage/database/dialect/config.go @@ -3,11 +3,14 @@ package dialect import ( "context" "errors" + "fmt" "reflect" "github.com/mitchellh/mapstructure" "github.com/spf13/viper" + "github.com/zitadel/zitadel/backend/cmd/config" + "github.com/zitadel/zitadel/backend/cmd/configure" "github.com/zitadel/zitadel/backend/storage/database" "github.com/zitadel/zitadel/backend/storage/database/dialect/gosql" "github.com/zitadel/zitadel/backend/storage/database/dialect/postgres" @@ -16,6 +19,7 @@ import ( type Hook struct { Match func(string) bool Decode func(name string, config any) (database.Connector, error) + Name string } var hooks = make([]Hook, 0) @@ -25,10 +29,12 @@ func init() { Hook{ Match: postgres.NameMatcher, Decode: postgres.DecodeConfig, + Name: postgres.Name, }, Hook{ Match: gosql.NameMatcher, Decode: gosql.DecodeConfig, + Name: gosql.Name, }, ) } @@ -39,6 +45,42 @@ type Config struct { connector database.Connector } +// Fields implements [configure.StructUpdater]. +func (c *Config) Fields() []configure.Updater { + dialects := configure.OneOf{ + FieldName: "dialect", + Description: "The database dialect Zitadel connects to", + SubFields: []configure.Updater{}, + } + for _, hook := range hooks { + value := c.Dialects[hook.Name] + dialects.SubFields = append(dialects.SubFields, &configure.Field[any]{ + FieldName: hook.Name, + Default: nil, + Description: fmt.Sprintf("Configuration for connection string for %s", hook.Name), + Version: config.V3, + Value: &value, + }) + } + + return []configure.Updater{ + dialects, + } +} + +// Name implements [configure.StructUpdater]. +func (c *Config) Name() string { + return "database" +} + +func (c Config) Connect(ctx context.Context) (database.Pool, error) { + if len(c.Dialects) != 1 { + return nil, errors.New("Exactly one dialect must be configured") + } + + return c.connector.Connect(ctx) +} + // Hooks implements [configure.Unmarshaller]. func (c Config) Hooks() []viper.DecoderConfigOption { return []viper.DecoderConfigOption{ @@ -46,8 +88,10 @@ func (c Config) Hooks() []viper.DecoderConfigOption { } } -func (c Config) Connect(ctx context.Context) (database.Pool, error) { - return c.connector.Connect(ctx) +// var _ configure.StructUpdater = (*Config)(nil) + +func (c Config) Configure(v *viper.Viper, currentVersion config.Version) Config { + return c } func (c *Config) decodeDialect() error { diff --git a/backend/storage/database/dialect/gosql/config.go b/backend/storage/database/dialect/gosql/config.go index 2557d054e4..001688bbbd 100644 --- a/backend/storage/database/dialect/gosql/config.go +++ b/backend/storage/database/dialect/gosql/config.go @@ -9,7 +9,10 @@ import ( "github.com/zitadel/zitadel/backend/storage/database" ) -var _ database.Connector = (*Config)(nil) +var ( + _ database.Connector = (*Config)(nil) + Name = "gosql" +) type Config struct { db *sql.DB diff --git a/backend/storage/database/dialect/postgres/config.go b/backend/storage/database/dialect/postgres/config.go index c2696a2b7c..ef93d0d19d 100644 --- a/backend/storage/database/dialect/postgres/config.go +++ b/backend/storage/database/dialect/postgres/config.go @@ -11,7 +11,10 @@ import ( "github.com/zitadel/zitadel/backend/storage/database" ) -var _ database.Connector = (*Config)(nil) +var ( + _ database.Connector = (*Config)(nil) + Name = "postgres" +) type Config struct{ *pgxpool.Config } diff --git a/backend/storage/database/postgres/config.go b/backend/storage/database/postgres/config.go deleted file mode 100644 index 36797ca3f7..0000000000 --- a/backend/storage/database/postgres/config.go +++ /dev/null @@ -1,4 +0,0 @@ -package postgres - -type Config struct { -} diff --git a/backend/storage/database/postgres/conn.go b/backend/storage/database/postgres/conn.go deleted file mode 100644 index b36cc76403..0000000000 --- a/backend/storage/database/postgres/conn.go +++ /dev/null @@ -1,46 +0,0 @@ -package postgres - -import ( - "context" - - "github.com/jackc/pgx/v5/pgxpool" - "github.com/zitadel/zitadel/backend/storage/database" -) - -type pgxConn struct{ *pgxpool.Conn } - -var _ database.Client = (*pgxConn)(nil) - -// Release implements [database.Client]. -func (c *pgxConn) Release(_ context.Context) error { - c.Conn.Release() - return nil -} - -// Begin implements [database.Client]. -func (c *pgxConn) Begin(ctx context.Context, opts *database.TransactionOptions) (database.Transaction, error) { - tx, err := c.Conn.BeginTx(ctx, transactionOptionsToPgx(opts)) - if err != nil { - return nil, err - } - return &pgxTx{tx}, nil -} - -// Query implements sql.Client. -// Subtle: this method shadows the method (*Conn).Query of pgxConn.Conn. -func (c *pgxConn) Query(ctx context.Context, sql string, args ...any) (database.Rows, error) { - return c.Conn.Query(ctx, sql, args...) -} - -// QueryRow implements sql.Client. -// Subtle: this method shadows the method (*Conn).QueryRow of pgxConn.Conn. -func (c *pgxConn) QueryRow(ctx context.Context, sql string, args ...any) database.Row { - return c.Conn.QueryRow(ctx, sql, args...) -} - -// Exec implements [database.Pool]. -// Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. -func (c *pgxConn) Exec(ctx context.Context, sql string, args ...any) error { - _, err := c.Conn.Exec(ctx, sql, args...) - return err -} diff --git a/backend/storage/database/postgres/pool.go b/backend/storage/database/postgres/pool.go deleted file mode 100644 index ff8dc5dc16..0000000000 --- a/backend/storage/database/postgres/pool.go +++ /dev/null @@ -1,55 +0,0 @@ -package postgres - -import ( - "context" - - "github.com/jackc/pgx/v5/pgxpool" - "github.com/zitadel/zitadel/backend/storage/database" -) - -type pgxPool struct{ pgxpool.Pool } - -var _ database.Pool = (*pgxPool)(nil) - -// Acquire implements [database.Pool]. -func (c *pgxPool) Acquire(ctx context.Context) (database.Client, error) { - conn, err := c.Pool.Acquire(ctx) - if err != nil { - return nil, err - } - return &pgxConn{conn}, nil -} - -// Query implements [database.Pool]. -// Subtle: this method shadows the method (Pool).Query of pgxPool.Pool. -func (c *pgxPool) Query(ctx context.Context, sql string, args ...any) (database.Rows, error) { - return c.Pool.Query(ctx, sql, args...) -} - -// QueryRow implements [database.Pool]. -// Subtle: this method shadows the method (Pool).QueryRow of pgxPool.Pool. -func (c *pgxPool) QueryRow(ctx context.Context, sql string, args ...any) database.Row { - return c.Pool.QueryRow(ctx, sql, args...) -} - -// Exec implements [database.Pool]. -// Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. -func (c *pgxPool) Exec(ctx context.Context, sql string, args ...any) error { - _, err := c.Pool.Exec(ctx, sql, args...) - return err -} - -// Begin implements [database.Pool]. -func (c *pgxPool) Begin(ctx context.Context, opts *database.TransactionOptions) (database.Transaction, error) { - tx, err := c.Pool.BeginTx(ctx, transactionOptionsToPgx(opts)) - if err != nil { - return nil, err - } - return &pgxTx{tx}, nil -} - -// Close implements [database.Pool]. -func (c *pgxPool) Close(_ context.Context) error { - c.Pool.Close() - return nil -} diff --git a/backend/storage/database/postgres/tx.go b/backend/storage/database/postgres/tx.go deleted file mode 100644 index 779d48f8b9..0000000000 --- a/backend/storage/database/postgres/tx.go +++ /dev/null @@ -1,83 +0,0 @@ -package postgres - -import ( - "context" - - "github.com/jackc/pgx/v5" - "github.com/zitadel/zitadel/backend/storage/database" -) - -type pgxTx struct{ pgx.Tx } - -var _ database.Transaction = (*pgxTx)(nil) - -// Commit implements [database.Transaction]. -func (tx *pgxTx) Commit(ctx context.Context) error { - return tx.Tx.Commit(ctx) -} - -// Rollback implements [database.Transaction]. -func (tx *pgxTx) Rollback(ctx context.Context) error { - return tx.Tx.Rollback(ctx) -} - -// End implements [database.Transaction]. -func (tx *pgxTx) End(ctx context.Context, err error) error { - if err != nil { - tx.Rollback(ctx) - return err - } - return tx.Commit(ctx) -} - -// Query implements [database.Transaction]. -// Subtle: this method shadows the method (Tx).Query of pgxTx.Tx. -func (tx *pgxTx) Query(ctx context.Context, sql string, args ...any) (database.Rows, error) { - return tx.Tx.Query(ctx, sql, args...) -} - -// QueryRow implements [database.Transaction]. -// Subtle: this method shadows the method (Tx).QueryRow of pgxTx.Tx. -func (tx *pgxTx) QueryRow(ctx context.Context, sql string, args ...any) database.Row { - return tx.Tx.QueryRow(ctx, sql, args...) -} - -// Exec implements [database.Pool]. -// Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. -func (tx *pgxTx) Exec(ctx context.Context, sql string, args ...any) error { - _, err := tx.Tx.Exec(ctx, sql, args...) - return err -} - -func transactionOptionsToPgx(opts *database.TransactionOptions) pgx.TxOptions { - if opts == nil { - return pgx.TxOptions{} - } - - return pgx.TxOptions{ - IsoLevel: isolationToPgx(opts.IsolationLevel), - AccessMode: accessModeToPgx(opts.AccessMode), - } -} - -func isolationToPgx(isolation database.IsolationLevel) pgx.TxIsoLevel { - switch isolation { - case database.IsolationLevelSerializable: - return pgx.Serializable - case database.IsolationLevelReadCommitted: - return pgx.ReadCommitted - default: - return pgx.Serializable - } -} - -func accessModeToPgx(accessMode database.AccessMode) pgx.TxAccessMode { - switch accessMode { - case database.AccessModeReadWrite: - return pgx.ReadWrite - case database.AccessModeReadOnly: - return pgx.ReadOnly - default: - return pgx.ReadWrite - } -} diff --git a/backend/zitadel b/backend/zitadel new file mode 100755 index 0000000000..b29b24a943 Binary files /dev/null and b/backend/zitadel differ diff --git a/go.mod b/go.mod index f316c3e866..b6e17886fc 100644 --- a/go.mod +++ b/go.mod @@ -102,6 +102,7 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.0 // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect + github.com/chzyer/readline v1.5.1 // indirect github.com/crewjam/httperr v0.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-ini/ini v1.67.0 // indirect @@ -118,6 +119,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/lib/pq v1.10.9 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index 709d6b7a14..41334ccb18 100644 --- a/go.sum +++ b/go.sum @@ -110,6 +110,13 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -509,6 +516,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -961,6 +970,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=