diff --git a/backend/cmd/configure/configure.go b/backend/cmd/configure/configure.go index 2d9554dd5b..ea2a6070c1 100644 --- a/backend/cmd/configure/configure.go +++ b/backend/cmd/configure/configure.go @@ -1,9 +1,9 @@ package configure import ( + "github.com/Masterminds/semver/v3" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/zitadel/zitadel/backend/cmd/config" ) var ( @@ -70,24 +70,21 @@ func (s sub) Fields() []Updater { return []Updater{ Field[string]{ FieldName: "f1", - Value: &s.F1, - Default: "", + Value: "", Description: "field 1", - Version: config.V3, + Version: semver.MustParse("3"), }, Field[int]{ FieldName: "f2", - Value: &s.F2, - Default: 0, + Value: 0, Description: "field 2", - Version: config.V3, + Version: semver.MustParse("3"), }, Field[*bool]{ FieldName: "f3", - Value: &s.F3, - Default: nil, + Value: nil, Description: "field 3", - Version: config.V3, + Version: semver.MustParse("3"), }, } } @@ -101,10 +98,9 @@ func (t test) Fields() []Updater { return []Updater{ Field[string]{ FieldName: "f1", - Value: &t.F1, - Default: "", + Value: "", Description: "field 1", - Version: config.V3, + Version: semver.MustParse("3"), }, Struct{ FieldName: "sub", diff --git a/backend/cmd/configure/update_config.go b/backend/cmd/configure/update_config.go deleted file mode 100644 index b833c4cf92..0000000000 --- a/backend/cmd/configure/update_config.go +++ /dev/null @@ -1,246 +0,0 @@ -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, value, err := prompt.Run() - if err != nil { - fmt.Println("panic", err) - panic(err) - } - fv := fields[value] - // TODO: fv is result of decoder (postgres.config in this case) - setField(f.Fields()[i], fv.(map[string]any), 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 deleted file mode 100644 index a953f85a7f..0000000000 --- a/backend/cmd/configure/update_config2.go +++ /dev/null @@ -1,109 +0,0 @@ -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/configure/update_config3.go b/backend/cmd/configure/update_config3.go new file mode 100644 index 0000000000..2745e4bb40 --- /dev/null +++ b/backend/cmd/configure/update_config3.go @@ -0,0 +1,449 @@ +package configure + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "strconv" + "strings" + + "github.com/Masterminds/semver/v3" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/zitadel/zitadel/backend/cmd/info" +) + +var lastConfiguredVersion *semver.Version + +func Update(name, description string, fields []Updater) func(cmd *cobra.Command, args []string) { + return func(cmd *cobra.Command, args []string) { + configVersion := viper.GetString("configuredVersion") + if configVersion != "" { + lastConfiguredVersion, _ = semver.NewVersion(configVersion) + } + for _, field := range fields { + if err := setField(field, "", 1); err != nil { + panic(err) + } + } + + fmt.Println("Using config file:", viper.ConfigFileUsed(), "fields:", fields) + fmt.Println(viper.AllSettings()) + viper.Set("configuredVersion", info.Version().String()) + // viper.MergeConfigMap(fields) + // if err := viper.WriteConfig(); err != nil { + // panic(err) + // } + if err := viper.WriteConfig(); err != nil { + panic(err) + } + } +} + +func setField(field Updater, parentPath string, depth int) error { + if !field.ShouldUpdate(lastConfiguredVersion) { + 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 nil + } + } + fieldPath := path(parentPath, field.Name()) + switch f := field.(type) { + case StructUpdater: + fmt.Printf("\n%.*s %s: %s\n\n", depth*2, "-", f.Name(), f.Describe()) + for _, subField := range f.Fields() { + err := setField(subField, fieldPath, depth+1) + if err != nil { + return err + } + } + case FieldUpdater: + value := viper.Get(fieldPath) + if value == nil { + value = f.ValueOrDefault() + } + prompt := promptui.Prompt{ + Label: fmt.Sprintf("%s (%s) (%T)", f.Name(), f.Describe(), f.ValueOrDefault()), + Default: fmt.Sprintf("%v", value), + Validate: func(s string) error { + if isConfirm(reflect.TypeOf(value)) { + return nil + } + return f.Set(s) + }, + HideEntered: f.ShouldHide(), + IsConfirm: isConfirm(reflect.TypeOf(value)), + } + _, err := prompt.Run() + if err != nil { + panic(err) + } + viper.Set(fieldPath, f.ValueOrDefault()) + case OneOfUpdater: + var possibilities []string + for _, subField := range f.Fields() { + possibility := subField.Name() + if possibility == "" { + possibility = subField.Describe() + } + possibilities = append(possibilities, possibility) + } + + prompt := promptui.Select{ + Label: fmt.Sprintf("Select one of %s: (%s)", f.Name(), f.Describe()), + Items: possibilities, + } + i, _, err := prompt.Run() + if err != nil { + fmt.Println("panic", err) + panic(err) + } + setField(f.Fields()[i], fieldPath, depth-1) + // TODO: fv is result of decoder (postgres.config in this case) + case ConstantUpdater: + viper.Set(fieldPath, f.Value()) + } + return nil +} + +func path(parent, field string) string { + if parent == "" { + return field + } + if strings.HasSuffix(parent, field) { + return parent + } + if field == "" { + return parent + } + return parent + "." + field +} + +func isConfirm(t reflect.Type) bool { + if t == nil { + return false + } + if t.Kind() == reflect.Ptr { + return isConfirm(t.Elem()) + } + return t.Kind() == reflect.Bool +} + +type Updater interface { + Name() string + Describe() string + ShouldUpdate(version *semver.Version) bool +} + +type FieldUpdater interface { + Updater + // DefaultValue() any + Set(value any) error + ValueOrDefault() any + _field() + ShouldHide() bool +} + +type StructUpdater interface { + Updater + Fields() []Updater + _struct() +} + +type OneOfUpdater interface { + Updater + Fields() []Updater + _oneOf() +} + +type ConstantUpdater interface { + Updater + Value() any +} + +var _ FieldUpdater = (*Field[string])(nil) + +type Field[T any] struct { + FieldName string + Value T + HideInput bool + Description string + Version *semver.Version + Validate func(T) error +} + +// Describe implements [FieldUpdater]. +func (f Field[T]) Describe() string { + return f.Description +} + +// Set implements [FieldUpdater]. +func (f *Field[T]) Set(value any) (err error) { + switch v := value.(type) { + case string: + f.Value, err = mapString[T](v) + case map[string]any: + f.Value, err = mapMap[T](v) + case T: + f.Value = v + default: + err = fmt.Errorf("unsupported type %T", v) + } + + if err != nil || f.Validate == nil { + return err + } + + return f.Validate(f.Value) +} + +func (f Field[T]) ValueOrDefault() any { + return f.Value +} + +func mapMap[T any](value map[string]any) (t T, err error) { + jsonValue, err := json.Marshal(value) + if err != nil { + return t, err + } + return t, json.Unmarshal(jsonValue, &t) +} + +func mapString[T any](value string) (t T, err error) { + var v any + switch reflect.TypeFor[T]().Kind() { + case reflect.Bool: + v, err = strconv.ParseBool(value) + if err != nil { + return t, err + } + case reflect.Int: + i, err := strconv.ParseInt(value, 10, 0) + if err != nil { + return t, err + } + v = int(i) + case reflect.Int8: + i, err := strconv.ParseInt(value, 10, 8) + if err != nil { + return t, err + } + v = int8(i) + case reflect.Int16: + i, err := strconv.ParseInt(value, 10, 16) + if err != nil { + return t, err + } + v = int16(i) + case reflect.Int32: + i, err := strconv.ParseInt(value, 10, 32) + if err != nil { + return t, err + } + v = int32(i) + case reflect.Int64: + i, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return t, err + } + v = int64(i) + case reflect.Uint: + i, err := strconv.ParseUint(value, 10, 0) + if err != nil { + return t, err + } + v = uint(i) + case reflect.Uint8: + i, err := strconv.ParseUint(value, 10, 8) + if err != nil { + return t, err + } + v = uint8(i) + case reflect.Uint16: + i, err := strconv.ParseUint(value, 10, 16) + if err != nil { + return t, err + } + v = uint16(i) + case reflect.Uint32: + i, err := strconv.ParseUint(value, 10, 32) + if err != nil { + return t, err + } + v = uint32(i) + case reflect.Uint64: + i, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return t, err + } + v = uint64(i) + case reflect.Float32: + i, err := strconv.ParseFloat(value, 32) + if err != nil { + return t, err + } + v = float32(i) + case reflect.Float64: + i, err := strconv.ParseFloat(value, 64) + if err != nil { + return t, err + } + v = float64(i) + case reflect.Complex64: + i, err := strconv.ParseComplex(value, 64) + if err != nil { + return t, err + } + v = complex64(i) + case reflect.Complex128: + v, err = strconv.ParseComplex(value, 128) + case reflect.String: + v = value + default: + k := reflect.TypeFor[T]().Kind() + _ = k + return t, errors.New("not implemented") + } + return v.(T), nil +} + +// Field implements [FieldUpdater]. +func (f Field[T]) Name() string { + return f.FieldName +} + +// ShouldUpdate implements [FieldUpdater]. +func (f Field[T]) ShouldUpdate(version *semver.Version) bool { + if version == nil { + return true + } + return f.Version.GreaterThan(version) +} + +// ShouldHide implements [FieldUpdater]. +func (f Field[T]) ShouldHide() bool { + return f.HideInput +} + +func (f Field[T]) _field() {} + +var _ StructUpdater = (*Struct)(nil) + +type Struct struct { + FieldName string + Description string + SubFields []Updater +} + +// Describe implements [StructUpdater]. +func (s Struct) Describe() string { + return s.Description +} + +func (s Struct) Name() string { + return s.FieldName +} + +func (s Struct) Fields() []Updater { + return s.SubFields +} + +func (s Struct) ShouldUpdate(version *semver.Version) bool { + if version == nil { + return true + } + for _, field := range s.SubFields { + if !field.ShouldUpdate(version) { + continue + } + return true + } + return false +} + +func (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 (OneOf) _oneOf() {} + +// ShouldUpdate implements [OneOfUpdater]. +func (o OneOf) ShouldUpdate(version *semver.Version) bool { + if version == nil { + return true + } + + for _, field := range o.SubFields { + if !field.ShouldUpdate(version) { + continue + } + return true + } + return false +} + +var _ OneOfUpdater = (*OneOf)(nil) + +func FieldName(parent, field string) string { + if parent == "" { + return field + } + return parent + "." + field +} + +type Constant[T any] struct { + Description string + Constant T + Version *semver.Version +} + +var _ ConstantUpdater = (*Constant[any])(nil) + +// Describe implements [ConstantUpdater]. +func (c Constant[T]) Describe() string { + return c.Description +} + +// Name implements [ConstantUpdater]. +func (c Constant[T]) Name() string { + return "" +} + +// ShouldUpdate implements [ConstantUpdater]. +func (c Constant[T]) ShouldUpdate(version *semver.Version) bool { + if version == nil { + return true + } + return c.Version.GreaterThan(version) +} + +// Value implements [ConstantUpdater]. +func (c Constant[T]) Value() any { + return c.Constant +} diff --git a/backend/cmd/info/config.go b/backend/cmd/info/config.go new file mode 100644 index 0000000000..f5ac8cf5a7 --- /dev/null +++ b/backend/cmd/info/config.go @@ -0,0 +1,36 @@ +package info + +import ( + "time" + + "github.com/Masterminds/semver/v3" +) + +var ( + version string + commit string + date string +) + +func Version() *semver.Version { + v, _ := semver.NewVersion(version) + if v != nil { + return v + } + return semver.New(uint64(Date().Year()), uint64(Date().Month()), uint64(Date().Day()), "", "") +} + +func Commit() string { + return commit +} + +func Date() time.Time { + if date == "" { + return time.Now() + } + d, err := time.Parse(time.RFC3339, date) + if err != nil { + return time.Now() + } + return d +} diff --git a/backend/cmd/prepare/001/step_001.go b/backend/cmd/prepare/001/step_001.go index 32118d824d..b164c49741 100644 --- a/backend/cmd/prepare/001/step_001.go +++ b/backend/cmd/prepare/001/step_001.go @@ -5,7 +5,8 @@ import ( "embed" "fmt" - "github.com/zitadel/zitadel/backend/cmd/config" + "github.com/Masterminds/semver/v3" + "github.com/zitadel/zitadel/backend/cmd/configure" "github.com/zitadel/zitadel/backend/storage/database" ) @@ -18,26 +19,24 @@ var ( type Step001 struct { Database database.Pool `mapstructure:"-"` - DatabaseName string `configure:"added:"v3",default:"zitadel"` - Username string `configure:"added:"v3",default:"zitadel"` + DatabaseName string + Username string } // Fields implements configure.StructUpdater. func (v Step001) Fields() []configure.Updater { return []configure.Updater{ - configure.Field[string]{ + &configure.Field[string]{ FieldName: "databaseName", - Default: "zitadel", - Value: &v.DatabaseName, + Value: "zitadel", Description: "The name of the database Zitadel will store its data in", - Version: config.V3, + Version: semver.MustParse("3"), }, - configure.Field[string]{ + &configure.Field[string]{ FieldName: "username", - Default: "zitadel", - Value: &v.Username, + Value: "zitadel", Description: "The username Zitadel will use to connect to the database", - Version: config.V3, + Version: semver.MustParse("3"), }, } } diff --git a/backend/cmd/prepare/config.go b/backend/cmd/prepare/config.go index e8739f5e03..a9289e3846 100644 --- a/backend/cmd/prepare/config.go +++ b/backend/cmd/prepare/config.go @@ -1,6 +1,7 @@ package prepare import ( + "github.com/Masterminds/semver/v3" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -65,7 +66,10 @@ func (c *Config) Name() string { } // ShouldUpdate implements configure.StructUpdater. -func (c *Config) ShouldUpdate(version config.Version) bool { +func (c *Config) ShouldUpdate(version *semver.Version) bool { + if version == nil { + return true + } for _, field := range c.Fields() { if field.ShouldUpdate(version) { return true @@ -77,12 +81,12 @@ func (c *Config) ShouldUpdate(version config.Version) bool { // Fields implements configure.UpdateConfig. func (c Config) Fields() []configure.Updater { return []configure.Updater{ - configure.Struct{ + &configure.Struct{ FieldName: "step001", Description: "The configuration for the first step of the prepare command", SubFields: c.Step001.Fields(), }, - configure.Struct{ + &configure.Struct{ FieldName: "database", Description: "The configuration for the database connection", SubFields: c.Database.Fields(), diff --git a/backend/cmd/prepare/prepare.go b/backend/cmd/prepare/prepare.go index 0f84cc3bcf..2240177d5c 100644 --- a/backend/cmd/prepare/prepare.go +++ b/backend/cmd/prepare/prepare.go @@ -2,6 +2,7 @@ package prepare import ( "github.com/spf13/cobra" + step001 "github.com/zitadel/zitadel/backend/cmd/prepare/001" ) diff --git a/backend/cmd/test.yaml b/backend/cmd/test.yaml index 5d138e5c13..d590fbd39c 100644 --- a/backend/cmd/test.yaml +++ b/backend/cmd/test.yaml @@ -1,19 +1,6 @@ +configuredversion: 2025.2.23 database: - 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 + postgres: host=local step001: - databasename: zitadel - username: zitadel + databasename: zita + username: adel diff --git a/backend/storage/database/dialect/config.go b/backend/storage/database/dialect/config.go index 9bcf7daca3..e95ab63819 100644 --- a/backend/storage/database/dialect/config.go +++ b/backend/storage/database/dialect/config.go @@ -20,6 +20,7 @@ type Hook struct { Match func(string) bool Decode func(name string, config any) (database.Connector, error) Name string + Field configure.Updater } var hooks = make([]Hook, 0) @@ -30,11 +31,13 @@ func init() { Match: postgres.NameMatcher, Decode: postgres.DecodeConfig, Name: postgres.Name, + Field: postgres.Field, }, Hook{ Match: gosql.NameMatcher, Decode: gosql.DecodeConfig, Name: gosql.Name, + Field: gosql.Field, }, ) } @@ -48,18 +51,17 @@ type Config struct { // 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]{ + if hook.Field == nil { + panic("hook must configure its config fields") + } + dialects.SubFields = append(dialects.SubFields, &configure.Struct{ FieldName: hook.Name, - Default: nil, - Description: fmt.Sprintf("Configuration for connection string for %s", hook.Name), - Version: config.V3, - Value: &value, + Description: fmt.Sprintf("Configuration for %s", hook.Name), + SubFields: []configure.Updater{hook.Field}, }) } diff --git a/backend/storage/database/dialect/gosql/config.go b/backend/storage/database/dialect/gosql/config.go index 001688bbbd..7e2d9b12c7 100644 --- a/backend/storage/database/dialect/gosql/config.go +++ b/backend/storage/database/dialect/gosql/config.go @@ -6,12 +6,25 @@ import ( "errors" "strings" + "github.com/Masterminds/semver/v3" + "github.com/jackc/pgx/v5/pgxpool" + + "github.com/zitadel/zitadel/backend/cmd/configure" "github.com/zitadel/zitadel/backend/storage/database" ) var ( - _ database.Connector = (*Config)(nil) - Name = "gosql" + _ database.Connector = (*Config)(nil) + + Name = "gosql" + Field = &configure.Field[string]{ + Description: "Connection string", + Version: semver.MustParse("v3"), + Validate: func(s string) error { + _, err := pgxpool.ParseConfig(s) + return err + }, + } ) type Config struct { diff --git a/backend/storage/database/dialect/postgres/config.go b/backend/storage/database/dialect/postgres/config.go index 285a764644..5cf60ed5d3 100644 --- a/backend/storage/database/dialect/postgres/config.go +++ b/backend/storage/database/dialect/postgres/config.go @@ -6,14 +6,91 @@ import ( "slices" "strings" + "github.com/Masterminds/semver/v3" "github.com/jackc/pgx/v5/pgxpool" + "github.com/zitadel/zitadel/backend/cmd/configure" "github.com/zitadel/zitadel/backend/storage/database" ) var ( _ database.Connector = (*Config)(nil) Name = "postgres" + + Field = &configure.OneOf{ + Description: "Configuring postgres using one of the following options", + SubFields: []configure.Updater{ + &configure.Field[string]{ + Description: "Connection string", + Version: semver.MustParse("v3"), + Validate: func(s string) error { + _, err := pgxpool.ParseConfig(s) + return err + }, + }, + &configure.Struct{ + Description: "Configuration for the connection", + SubFields: []configure.Updater{ + &configure.Field[string]{ + FieldName: "host", + Value: "localhost", + Description: "The host to connect to", + Version: semver.MustParse("3"), + }, + &configure.Field[uint32]{ + FieldName: "port", + Value: 5432, + Description: "The port to connect to", + Version: semver.MustParse("3"), + }, + &configure.Field[string]{ + FieldName: "database", + Value: "zitadel", + Description: "The database to connect to", + Version: semver.MustParse("3"), + }, + &configure.Field[string]{ + FieldName: "user", + Description: "The user to connect as", + Value: "zitadel", + Version: semver.MustParse("3"), + }, + &configure.Field[string]{ + FieldName: "password", + Description: "The password to connect with", + Version: semver.MustParse("3"), + HideInput: true, + }, + &configure.OneOf{ + FieldName: "sslMode", + Description: "The SSL mode to use", + SubFields: []configure.Updater{ + &configure.Constant[string]{ + Description: "Disable", + Constant: "disable", + Version: semver.MustParse("3"), + }, + &configure.Constant[string]{ + Description: "Require", + Constant: "require", + Version: semver.MustParse("3"), + }, + &configure.Constant[string]{ + Description: "Verify CA", + Constant: "verify-ca", + Version: semver.MustParse("3"), + }, + &configure.Constant[string]{ + Description: "Verify Full", + Constant: "verify-full", + Version: semver.MustParse("3"), + }, + }, + }, + }, + }, + }, + } ) type Config struct{ *pgxpool.Config } diff --git a/backend/storage/database/dialect/postgres/conn.go b/backend/storage/database/dialect/postgres/conn.go index 5fe024c347..134bb3e5f5 100644 --- a/backend/storage/database/dialect/postgres/conn.go +++ b/backend/storage/database/dialect/postgres/conn.go @@ -4,6 +4,7 @@ import ( "context" "github.com/jackc/pgx/v5/pgxpool" + "github.com/zitadel/zitadel/backend/storage/database" ) diff --git a/backend/storage/database/dialect/postgres/pool.go b/backend/storage/database/dialect/postgres/pool.go index 35af1e50db..a3e6033e2e 100644 --- a/backend/storage/database/dialect/postgres/pool.go +++ b/backend/storage/database/dialect/postgres/pool.go @@ -4,6 +4,7 @@ import ( "context" "github.com/jackc/pgx/v5/pgxpool" + "github.com/zitadel/zitadel/backend/storage/database" ) diff --git a/backend/storage/database/dialect/postgres/rows.go b/backend/storage/database/dialect/postgres/rows.go index 2dbd38732a..91ee2fc27a 100644 --- a/backend/storage/database/dialect/postgres/rows.go +++ b/backend/storage/database/dialect/postgres/rows.go @@ -2,6 +2,7 @@ package postgres import ( "github.com/jackc/pgx/v5" + "github.com/zitadel/zitadel/backend/storage/database" ) diff --git a/backend/storage/database/dialect/postgres/tx.go b/backend/storage/database/dialect/postgres/tx.go index 85707fdb21..767111b324 100644 --- a/backend/storage/database/dialect/postgres/tx.go +++ b/backend/storage/database/dialect/postgres/tx.go @@ -4,6 +4,7 @@ import ( "context" "github.com/jackc/pgx/v5" + "github.com/zitadel/zitadel/backend/storage/database" ) diff --git a/go.mod b/go.mod index b6e17886fc..71ab06daf1 100644 --- a/go.mod +++ b/go.mod @@ -100,6 +100,7 @@ require ( cloud.google.com/go/auth v0.6.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.0 // indirect + github.com/Masterminds/semver/v3 v3.3.1 // 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 diff --git a/go.sum b/go.sum index 41334ccb18..2eeb66441e 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.0 h1:ng6QH9Z4bAXCf0Z1cjR5hKESyc1BUiOrfIOhN+nHfRU= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.0/go.mod h1:ZC7rjqRzdhRKDK223jQ7Tsz89ZtrSSLH/VFzf7k5Sb0= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=