init configure

This commit is contained in:
adlerhurst
2025-02-17 07:55:12 +01:00
parent 77ab8226a5
commit 308bcda8a0
28 changed files with 820 additions and 367 deletions

View File

@@ -1,45 +1,74 @@
/*
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
*/
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())
}

View File

@@ -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(),
},
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 = "<Exit>"
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
}

View File

@@ -0,0 +1 @@
CREATE USER IF NOT EXISTS {{ .Username}};

View File

@@ -0,0 +1 @@
CREATE DATABASE IF NOT EXISTS {{ .DatabaseName }} WITH OWNER {{ .Username }};

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -1,18 +1,15 @@
/*
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
*/
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{
var (
// PrepareCmd represents the prepare command
PrepareCmd = &cobra.Command{
Use: "prepare",
Short: "Prepares the environment before starting Zitadel",
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:
@@ -20,18 +17,18 @@ var PrepareCmd = &cobra.Command{
// 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")
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)
}
},
}
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")
}
)
type Migration interface {
}

View File

@@ -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
}

View File

@@ -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())
}
}

View File

@@ -7,7 +7,7 @@ import (
)
type Config struct {
Database dialect.Config
Database dialect.Config `version:"v3"`
}
func (c Config) Hooks() []viper.DecoderConfigOption {

View File

@@ -1,16 +1,16 @@
/*
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
*/
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{
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
@@ -22,7 +22,11 @@ var StartCmd = &cobra.Command{
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.

View File

@@ -1,5 +1,19 @@
database:
postgres: 'something'
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

View File

@@ -1,6 +1,3 @@
/*
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
*/
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

View File

@@ -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)
}

View File

@@ -12,7 +12,7 @@ type Row interface {
type Rows interface {
Row
Next() bool
Close()
Close() error
Err() error
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 }

View File

@@ -1,4 +0,0 @@
package postgres
type Config struct {
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}
}

BIN
backend/zitadel Executable file

Binary file not shown.

2
go.mod
View File

@@ -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

10
go.sum
View File

@@ -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=